diff --git a/.env.example b/.env.example index 6823a9d0..d465ad2f 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ POSTGRES_PASSWORD=postgres POSTGRES_PORT=5432 # --- backend settings (see backend/.env.example for full list) --- +# For remote access, set this to your UI origin (e.g. http://:3000 or https://mc.example.com). CORS_ORIGINS=http://localhost:3000 DB_AUTO_MIGRATE=true LOG_LEVEL=INFO @@ -22,6 +23,6 @@ LOCAL_AUTH_TOKEN= # --- frontend settings --- # REQUIRED: Public URL used by the browser to reach the API. -# If this is missing/blank, frontend API calls (e.g. Activity feed) will break. -# Example (local dev / compose on your machine): -NEXT_PUBLIC_API_URL=http://localhost:8000 +# Use `auto` to target the same host currently serving Mission Control on port 8000. +# Example (explicit override): NEXT_PUBLIC_API_URL=https://mc.example.com +NEXT_PUBLIC_API_URL=auto diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..b961c3fa --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,39 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `backend/`: FastAPI service. Main app code lives in `backend/app/` with API routes in `backend/app/api/`, data models in `backend/app/models/`, schemas in `backend/app/schemas/`, and service logic in `backend/app/services/`. +- `backend/migrations/`: Alembic migrations (`backend/migrations/versions/` for generated revisions). +- `backend/tests/`: pytest suite (`test_*.py` naming). +- `backend/templates/`: backend-shipped templates used by gateway flows. +- `frontend/`: Next.js app. Routes under `frontend/src/app/`, shared components under `frontend/src/components/`, utilities under `frontend/src/lib/`. +- `frontend/src/api/generated/`: generated API client; regenerate instead of editing by hand. +- `docs/`: contributor and operations docs (start at `docs/README.md`). + +## Build, Test, and Development Commands +- `make setup`: install/sync backend and frontend dependencies. +- `make check`: closest CI parity run (lint, typecheck, tests/coverage, frontend build). +- `docker compose -f compose.yml --env-file .env up -d --build`: run full stack. +- Fast local loop: + - `docker compose -f compose.yml --env-file .env up -d db` + - `cd backend && uv run uvicorn app.main:app --reload --port 8000` + - `cd frontend && npm run dev` +- `make api-gen`: regenerate frontend API client (backend must be on `127.0.0.1:8000`). + +## Coding Style & Naming Conventions +- Python: Black + isort + flake8 + strict mypy. Max line length is 100. Use `snake_case`. +- TypeScript/React: ESLint + Prettier. Components use `PascalCase`; variables/functions use `camelCase`. +- For intentionally unused destructured TS variables, prefix with `_` to satisfy lint config. + +## Testing Guidelines +- Backend: pytest via `make backend-test`; coverage policy via `make backend-coverage` (writes `backend/coverage.xml` and `backend/coverage.json`). +- Frontend: vitest + Testing Library via `make frontend-test` (coverage in `frontend/coverage/`). +- Add or update tests whenever behavior changes. + +## Commit & Pull Request Guidelines +- Follow Conventional Commits (seen in history), e.g. `feat: ...`, `fix: ...`, `docs: ...`, `test(core): ...`. +- Keep PRs focused and based on latest `master`. +- Include: what changed, why, test evidence (`make check` or targeted commands), linked issue, and screenshots/logs when UI or operator workflow changes. + +## Security & Configuration Tips +- Never commit secrets. Copy from `.env.example` and keep real values in local `.env`. +- Report vulnerabilities privately via GitHub security advisories, not public issues. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d545ce7a..4da05055 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,22 +102,18 @@ jobs: - name: Run backend checks env: # Keep CI builds deterministic. - NEXT_TELEMETRY_DISABLED: "1" - AUTH_MODE: "clerk" - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} + AUTH_MODE: "local" + LOCAL_AUTH_TOKEN: "ci-local-auth-token-0123456789-0123456789-0123456789x" run: | make backend-lint - make backend-typecheck make backend-coverage - name: Run frontend checks env: # Keep CI builds deterministic. NEXT_TELEMETRY_DISABLED: "1" - NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} - NEXT_PUBLIC_AUTH_MODE: "clerk" - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} + NEXT_PUBLIC_API_URL: "http://localhost:8000" + NEXT_PUBLIC_AUTH_MODE: "local" run: | make frontend-lint make frontend-typecheck @@ -125,7 +121,7 @@ jobs: make frontend-build - - name: Docs quality gates (lint + relative link check) + - name: Docs quality gates run: | make docs-check @@ -140,8 +136,19 @@ jobs: frontend/coverage/** installer: - runs-on: ubuntu-latest + name: Installer (${{ matrix.os }}) + runs-on: ${{ matrix.os }} needs: [check] + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + run_linux_smoke_tests: true + run_macos_local_smoke_test: false + - os: macos-latest + run_linux_smoke_tests: false + run_macos_local_smoke_test: true steps: - name: Checkout @@ -150,7 +157,52 @@ jobs: - name: Validate installer shell syntax run: bash -n install.sh + - name: Set up Python for macOS installer smoke test + if: ${{ matrix.run_macos_local_smoke_test }} + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv for macOS installer smoke test + if: ${{ matrix.run_macos_local_smoke_test }} + run: python -m pip install --upgrade pip uv + + - name: Set up Node for macOS installer smoke test + if: ${{ matrix.run_macos_local_smoke_test }} + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Start PostgreSQL for macOS installer smoke test + if: ${{ matrix.run_macos_local_smoke_test }} + run: | + brew install postgresql@16 + PG_BIN="$(brew --prefix postgresql@16)/bin" + "$PG_BIN/initdb" -D "$RUNNER_TEMP/pgdata" + "$PG_BIN/pg_ctl" -D "$RUNNER_TEMP/pgdata" -l "$RUNNER_TEMP/postgres.log" -o "-p 55432" start + "$PG_BIN/createdb" -p 55432 mission_control + + - name: Installer smoke test (macOS local mode + external db) + if: ${{ matrix.run_macos_local_smoke_test }} + run: | + PGUSER="$(whoami)" + ./install.sh \ + --mode local \ + --backend-port 18002 \ + --frontend-port 13002 \ + --public-host localhost \ + --api-url http://localhost:18002 \ + --token-mode generate \ + --db-mode external \ + --database-url "postgresql+psycopg://${PGUSER}@localhost:55432/mission_control" \ + --start-services no + test -f .env + test -f backend/.env + test -f frontend/.env + test -f frontend/.next/BUILD_ID + - name: Installer smoke test (docker mode) + if: ${{ matrix.run_linux_smoke_tests }} run: | ./install.sh \ --mode docker \ @@ -159,15 +211,41 @@ jobs: --public-host localhost \ --api-url http://localhost:18000 \ --token-mode generate - curl -fsS http://127.0.0.1:18000/healthz >/dev/null - curl -fsS http://127.0.0.1:13000 >/dev/null + + backend_ready=0 + for _ in {1..120}; do + if curl -fsS http://127.0.0.1:18000/healthz >/dev/null; then + backend_ready=1 + break + fi + sleep 2 + done + + frontend_ready=0 + for _ in {1..120}; do + if curl -fsS http://127.0.0.1:13000 >/dev/null; then + frontend_ready=1 + break + fi + sleep 2 + done + + if [ "$backend_ready" -ne 1 ] || [ "$frontend_ready" -ne 1 ]; then + echo "Installer docker smoke readiness failed: backend_ready=$backend_ready frontend_ready=$frontend_ready" + docker compose -f compose.yml --env-file .env ps || true + docker compose -f compose.yml --env-file .env logs --no-color --tail=200 backend db redis frontend webhook-worker || true + exit 1 + fi - name: Cleanup docker stack after docker mode - if: always() + if: ${{ always() && matrix.run_linux_smoke_tests }} run: | docker compose -f compose.yml --env-file .env down -v --remove-orphans || true - name: Installer smoke test (local mode) + if: ${{ matrix.run_linux_smoke_tests }} + env: + XDG_STATE_HOME: ${{ github.workspace }}/.installer-state run: | ./install.sh \ --mode local \ @@ -178,16 +256,66 @@ jobs: --token-mode generate \ --db-mode docker \ --start-services yes - curl -fsS http://127.0.0.1:18001/healthz >/dev/null - curl -fsS http://127.0.0.1:13001 >/dev/null + + backend_ready=0 + for _ in {1..120}; do + if curl -fsS http://127.0.0.1:18001/healthz >/dev/null; then + backend_ready=1 + break + fi + sleep 2 + done + + frontend_ready=0 + for _ in {1..120}; do + if curl -fsS http://127.0.0.1:13001 >/dev/null; then + frontend_ready=1 + break + fi + sleep 2 + done + + if [ "$backend_ready" -ne 1 ] || [ "$frontend_ready" -ne 1 ]; then + echo "Installer local smoke readiness failed: backend_ready=$backend_ready frontend_ready=$frontend_ready" + LOG_DIR="$XDG_STATE_HOME/openclaw-mission-control-install" + if [ -f "$LOG_DIR/backend.log" ]; then + echo "----- backend log (tail) -----" + tail -n 200 "$LOG_DIR/backend.log" || true + fi + if [ -f "$LOG_DIR/frontend.log" ]; then + echo "----- frontend log (tail) -----" + tail -n 200 "$LOG_DIR/frontend.log" || true + fi + exit 1 + fi - name: Cleanup local processes and docker resources - if: always() + if: ${{ always() && matrix.run_linux_smoke_tests }} + env: + XDG_STATE_HOME: ${{ github.workspace }}/.installer-state run: | - if [ -f .install-logs/backend.pid ]; then kill "$(cat .install-logs/backend.pid)" || true; fi - if [ -f .install-logs/frontend.pid ]; then kill "$(cat .install-logs/frontend.pid)" || true; fi + LOG_DIR="$XDG_STATE_HOME/openclaw-mission-control-install" + if [ -f "$LOG_DIR/backend.pid" ]; then kill "$(cat "$LOG_DIR/backend.pid")" || true; fi + if [ -f "$LOG_DIR/frontend.pid" ]; then kill "$(cat "$LOG_DIR/frontend.pid")" || true; fi docker compose -f compose.yml --env-file .env down -v --remove-orphans || true + - name: Cleanup macOS PostgreSQL + if: ${{ always() && matrix.run_macos_local_smoke_test }} + run: | + if ! command -v brew >/dev/null 2>&1; then + exit 0 + fi + + PG_PREFIX="$(brew --prefix postgresql@16 2>/dev/null || true)" + if [ -z "$PG_PREFIX" ] || [ ! -d "$PG_PREFIX" ]; then + exit 0 + fi + + PG_BIN="$PG_PREFIX/bin" + if [ -d "$RUNNER_TEMP/pgdata" ] && [ -x "$PG_BIN/pg_ctl" ]; then + "$PG_BIN/pg_ctl" -D "$RUNNER_TEMP/pgdata" -m fast stop || true + fi + e2e: runs-on: ubuntu-latest needs: [check] @@ -219,11 +347,9 @@ jobs: - name: Start frontend (dev server) env: - NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} - NEXT_PUBLIC_AUTH_MODE: "clerk" + NEXT_PUBLIC_API_URL: "http://localhost:8000" + NEXT_PUBLIC_AUTH_MODE: "local" NEXT_TELEMETRY_DISABLED: "1" - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} run: | cd frontend npm run dev -- --hostname 0.0.0.0 --port 3000 & @@ -236,13 +362,9 @@ jobs: - name: Run Cypress E2E env: - NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} - NEXT_PUBLIC_AUTH_MODE: "clerk" + NEXT_PUBLIC_API_URL: "http://localhost:8000" + NEXT_PUBLIC_AUTH_MODE: "local" NEXT_TELEMETRY_DISABLED: "1" - # Clerk testing tokens (official @clerk/testing Cypress integration) - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - # Also set for the app itself. - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} run: | cd frontend npm run e2e -- --browser chrome diff --git a/.gitignore b/.gitignore index addb7216..e9bd9489 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ node_modules/ backend/~/ backend/coverage.* backend/.coverage -frontend/coverage \ No newline at end of file +frontend/coverage +backend/app/services/openclaw/.device-keys \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c5303a1..c7d8d95c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,8 +11,9 @@ This repo welcomes contributions in three broad categories: ## Where to start - Docs landing page: [Docs landing](./docs/README.md) -- Development workflow: [Development workflow](./docs/03-development.md) -- Testing guide: [Testing guide](./docs/testing/README.md) +- Development workflow: [Development](./docs/development/README.md) +- Testing guide: [Testing](./docs/testing/README.md) +- Release checklist: [Release checklist](./docs/release/README.md) ## Filing issues diff --git a/Makefile b/Makefile index f254fb52..cb3f5ba3 100644 --- a/Makefile +++ b/Makefile @@ -55,10 +55,10 @@ frontend-format-check: frontend-tooling ## Check frontend formatting (prettier) $(NODE_WRAP) --cwd $(FRONTEND_DIR) npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,css,md}" "*.{ts,js,json,md,mdx}" .PHONY: lint -lint: backend-lint frontend-lint ## Lint backend + frontend +lint: backend-lint frontend-lint docs-lint ## Lint backend + frontend + docs .PHONY: backend-lint -backend-lint: ## Lint backend (flake8) +backend-lint: backend-format-check backend-typecheck ## Lint backend (isort/black checks + flake8 + mypy) cd $(BACKEND_DIR) && uv run flake8 --config .flake8 .PHONY: frontend-lint @@ -142,6 +142,22 @@ frontend-build: frontend-tooling ## Build frontend (next build) api-gen: frontend-tooling ## Regenerate TS API client (requires backend running at 127.0.0.1:8000) $(NODE_WRAP) --cwd $(FRONTEND_DIR) npm run api:gen +.PHONY: docker-up +docker-up: ## Start full Docker stack with image rebuild + docker compose -f compose.yml --env-file .env up -d --build + +.PHONY: docker-watch +docker-watch: ## Start stack in watch mode (auto rebuild frontend on UI changes) + docker compose -f compose.yml --env-file .env up --build --watch + +.PHONY: docker-watch-only +docker-watch-only: ## Attach file watch to an already-running stack + docker compose -f compose.yml --env-file .env watch + +.PHONY: docker-down +docker-down: ## Stop full Docker stack + docker compose -f compose.yml --env-file .env down + .PHONY: rq-worker rq-worker: ## Run background queue worker loop cd $(BACKEND_DIR) && uv run python ../scripts/rq worker diff --git a/README.md b/README.md index edfc844f..b08e041e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenClaw Mission Control -[![CI](https://github.com/abhi1693/openclaw-mission-control/actions/workflows/ci.yml/badge.svg)](https://github.com/abhi1693/openclaw-mission-control/actions/workflows/ci.yml) +[![CI](https://github.com/abhi1693/openclaw-mission-control/actions/workflows/ci.yml/badge.svg)](https://github.com/abhi1693/openclaw-mission-control/actions/workflows/ci.yml) ![Static Badge](https://img.shields.io/badge/Join-Slack-active?style=flat&color=blue&link=https%3A%2F%2Fjoin.slack.com%2Ft%2Foc-mission-control%2Fshared_invite%2Fzt-3qpcm57xh-AI9C~smc3MDBVzEhvwf7gg) OpenClaw Mission Control is the centralized operations and governance platform for running OpenClaw across teams and organizations, with unified visibility, approval controls, and gateway-aware orchestration. It gives operators a single interface for work orchestration, agent and gateway management, approval-driven governance, and API-backed automation. @@ -57,6 +57,8 @@ If you haven't cloned the repo yet, you can run the installer in one line: curl -fsSL https://raw.githubusercontent.com/abhi1693/openclaw-mission-control/master/install.sh | bash ``` +This clones the repository into `./openclaw-mission-control` if no local checkout is found in your current directory. + If you already cloned the repo: ```bash @@ -76,6 +78,7 @@ Installer support matrix: [`docs/installer-support.md`](./docs/installer-support ### Prerequisites +- **Supported platforms**: Linux and macOS. On macOS, Docker mode requires [Docker Desktop](https://www.docker.com/products/docker-desktop/); local mode requires [Homebrew](https://brew.sh) and Node.js 22+. - Docker Engine - Docker Compose v2 (`docker compose`) @@ -88,7 +91,8 @@ cp .env.example .env Before startup: - Set `LOCAL_AUTH_TOKEN` to a non-placeholder value (minimum 50 characters) when `AUTH_MODE=local`. -- Ensure `NEXT_PUBLIC_API_URL` is reachable from your browser. +- `NEXT_PUBLIC_API_URL=auto` (default) resolves to `http(s)://:8000`. + - Set an explicit URL when your API is behind a reverse proxy or non-default port. ### 2. Start Mission Control @@ -96,6 +100,36 @@ Before startup: docker compose -f compose.yml --env-file .env up -d --build ``` +If you are iterating on the UI in Docker and want automatic frontend rebuilds on +source changes, run: + +```bash +docker compose -f compose.yml --env-file .env up --build --watch +``` + +Notes: + +- Compose Watch requires Docker Compose **2.22.0+**. +- You can also run watch separately after startup: + +```bash +docker compose -f compose.yml --env-file .env up -d --build +docker compose -f compose.yml --env-file .env watch +``` + +After pulling new changes, rebuild and recreate all services: + +```bash +docker compose -f compose.yml --env-file .env up -d --build --force-recreate +``` + +For a fully clean rebuild (no cached build layers): + +```bash +docker compose -f compose.yml --env-file .env build --no-cache --pull +docker compose -f compose.yml --env-file .env up -d --force-recreate +``` + ### 3. Open the application - Mission Control UI: http://localhost:3000 diff --git a/backend/.env.example b/backend/.env.example index dd3f52c6..aad21354 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -5,8 +5,15 @@ LOG_USE_UTC=false REQUEST_LOG_SLOW_MS=1000 REQUEST_LOG_INCLUDE_HEALTH=false DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/mission_control +# For remote access, set this to your UI origin (e.g. http://:3000 or https://mc.example.com). CORS_ORIGINS=http://localhost:3000 +# REQUIRED for gateway provisioning/agent heartbeats. Must be reachable by gateway runtime. BASE_URL= +# Security response headers (blank values disable each header). +SECURITY_HEADER_X_CONTENT_TYPE_OPTIONS= +SECURITY_HEADER_X_FRAME_OPTIONS= +SECURITY_HEADER_REFERRER_POLICY= +SECURITY_HEADER_PERMISSIONS_POLICY= # Auth mode: clerk or local. AUTH_MODE=local diff --git a/backend/.env.test b/backend/.env.test new file mode 100644 index 00000000..2d22385a --- /dev/null +++ b/backend/.env.test @@ -0,0 +1,38 @@ +# Commit-safe backend test environment. +# Usage: +# cd backend +# uv run --env-file .env.test uvicorn app.main:app --reload --port 8000 + +ENVIRONMENT=dev +LOG_LEVEL=INFO +LOG_FORMAT=text +LOG_USE_UTC=false +REQUEST_LOG_SLOW_MS=1000 +REQUEST_LOG_INCLUDE_HEALTH=false + +# Local backend -> local Postgres (adjust host/port if needed) +DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/mission_control_test +CORS_ORIGINS=http://localhost:3000 +BASE_URL=http://localhost:8000 + +# Auth mode: local for test/dev +AUTH_MODE=local +# Must be non-placeholder and >= 50 chars +LOCAL_AUTH_TOKEN=test-local-token-0123456789-0123456789-0123456789x + +# Clerk settings kept empty in local auth mode +CLERK_SECRET_KEY= +CLERK_API_URL=https://api.clerk.com +CLERK_VERIFY_IAT=true +CLERK_LEEWAY=10.0 + +# Database +DB_AUTO_MIGRATE=true + +# Queue / dispatch +RQ_REDIS_URL=redis://localhost:6379/0 +RQ_QUEUE_NAME=default +RQ_DISPATCH_THROTTLE_SECONDS=15.0 +RQ_DISPATCH_MAX_RETRIES=3 + +GATEWAY_MIN_VERSION=2026.02.9 diff --git a/backend/Dockerfile b/backend/Dockerfile index 18c7e478..e521b909 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,7 +9,7 @@ WORKDIR /app # System deps (keep minimal) RUN apt-get update \ - && apt-get install -y --no-install-recommends curl ca-certificates \ + && apt-get install -y --no-install-recommends curl ca-certificates git \ && rm -rf /var/lib/apt/lists/* # Install uv (https://github.com/astral-sh/uv) @@ -42,6 +42,10 @@ COPY backend/app ./app # In-repo these live at `backend/templates/`; runtime path is `/app/templates`. COPY backend/templates ./templates +# Copy worker scripts. +# In-repo these live at `scripts/`; runtime path is `/app/scripts`. +COPY scripts ./scripts + # Default API port EXPOSE 8000 diff --git a/backend/README.md b/backend/README.md index 9f6a48e9..6343cebd 100644 --- a/backend/README.md +++ b/backend/README.md @@ -57,7 +57,7 @@ A starter file exists at `backend/.env.example`. `postgresql+psycopg://postgres:postgres@localhost:5432/mission_control` - `CORS_ORIGINS` (comma-separated) - Example: `http://localhost:3000` -- `BASE_URL` (optional) +- `BASE_URL` (required for gateway provisioning/agent heartbeat templates; no fallback) ### Database lifecycle @@ -101,17 +101,20 @@ Notes: From repo root (recommended): ```bash -make backend-test make backend-lint -make backend-typecheck +make backend-test make backend-coverage ``` +`make backend-lint` runs backend format checks (`isort`, `black`), lint (`flake8`), and typecheck (`mypy`) in one command. + Or from `backend/`: ```bash cd backend uv run pytest +uv run isort . --check-only --diff +uv run black . --check --diff uv run flake8 --config .flake8 uv run mypy ``` diff --git a/backend/app/api/activity.py b/backend/app/api/activity.py index a5a1633d..fb68ed40 100644 --- a/backend/app/api/activity.py +++ b/backend/app/api/activity.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request, status -from sqlalchemy import asc, desc, func +from sqlalchemy import and_, asc, desc, func, or_ from sqlmodel import col, select from sse_starlette.sse import EventSourceResponse @@ -78,6 +78,46 @@ def _agent_role(agent: Agent | None) -> str | None: return None +def _build_activity_route( + *, + event: ActivityEvent, + board_id: UUID | None, +) -> tuple[str, dict[str, str]]: + if board_id is not None: + board_id_str = str(board_id) + board_params = {"boardId": board_id_str} + + if event.event_type == "task.comment" and event.task_id is not None: + return ( + "board", + { + **board_params, + "taskId": str(event.task_id), + "commentId": str(event.id), + }, + ) + + if event.event_type.startswith("approval."): + return ("board.approvals", board_params) + + if event.event_type.startswith("board."): + return ("board", {**board_params, "panel": "chat"}) + + if event.task_id is not None: + return ("board", {**board_params, "taskId": str(event.task_id)}) + + return ("board", board_params) + + fallback_params = { + "eventId": str(event.id), + "eventType": event.event_type, + "createdAt": event.created_at.isoformat(), + } + if event.task_id is not None: + fallback_params["taskId"] = str(event.task_id) + return ("activity", fallback_params) + + def _feed_item( event: ActivityEvent, task: Task, @@ -141,6 +181,46 @@ def _coerce_task_comment_rows( return rows +def _coerce_activity_rows( + items: Sequence[Any], +) -> list[tuple[ActivityEvent, UUID | None, UUID | None]]: + rows: list[tuple[ActivityEvent, UUID | None, UUID | None]] = [] + for item in items: + first: Any + second: Any + third: Any + + if isinstance(item, tuple): + if len(item) != 3: + msg = "Expected (ActivityEvent, event_board_id, task_board_id) rows" + raise TypeError(msg) + first, second, third = item + else: + try: + row_len = len(item) + first = item[0] + second = item[1] + third = item[2] + except (IndexError, KeyError, TypeError): + msg = "Expected (ActivityEvent, event_board_id, task_board_id) rows" + raise TypeError(msg) from None + if row_len != 3: + msg = "Expected (ActivityEvent, event_board_id, task_board_id) rows" + raise TypeError(msg) + + if not isinstance(first, ActivityEvent): + msg = "Expected (ActivityEvent, event_board_id, task_board_id) rows" + raise TypeError(msg) + if second is not None and not isinstance(second, UUID): + msg = "Expected (ActivityEvent, event_board_id, task_board_id) rows" + raise TypeError(msg) + if third is not None and not isinstance(third, UUID): + msg = "Expected (ActivityEvent, event_board_id, task_board_id) rows" + raise TypeError(msg) + rows.append((first, second, third)) + return rows + + async def _fetch_task_comment_events( session: AsyncSession, since: datetime, @@ -168,9 +248,13 @@ async def list_activity( actor: ActorContext = ACTOR_DEP, ) -> LimitOffsetPage[ActivityEventRead]: """List activity events visible to the calling actor.""" - statement = select(ActivityEvent) + statement: Any = select( + ActivityEvent, + col(ActivityEvent.board_id).label("event_board_id"), + col(Task.board_id).label("task_board_id"), + ).outerjoin(Task, col(ActivityEvent.task_id) == col(Task.id)) if actor.actor_type == "agent" and actor.agent: - statement = statement.where(ActivityEvent.agent_id == actor.agent.id) + statement = statement.where(col(ActivityEvent.agent_id) == actor.agent.id) elif actor.actor_type == "user" and actor.user: member = await get_active_membership(session, actor.user) if member is None: @@ -179,12 +263,34 @@ async def list_activity( if not board_ids: statement = statement.where(col(ActivityEvent.id).is_(None)) else: - statement = statement.join( - Task, - col(ActivityEvent.task_id) == col(Task.id), - ).where(col(Task.board_id).in_(board_ids)) + statement = statement.where( + or_( + col(ActivityEvent.board_id).in_(board_ids), + and_( + col(ActivityEvent.board_id).is_(None), + col(Task.board_id).in_(board_ids), + ), + ), + ) statement = statement.order_by(desc(col(ActivityEvent.created_at))) - return await paginate(session, statement) + + def _transform(items: Sequence[Any]) -> Sequence[Any]: + rows = _coerce_activity_rows(items) + events: list[ActivityEventRead] = [] + for event, event_board_id, task_board_id in rows: + payload = ActivityEventRead.model_validate(event, from_attributes=True) + resolved_board_id = event_board_id or task_board_id + payload.board_id = resolved_board_id + route_name, route_params = _build_activity_route( + event=event, + board_id=resolved_board_id, + ) + payload.route_name = route_name + payload.route_params = route_params + events.append(payload) + return events + + return await paginate(session, statement, transformer=_transform) @router.get( diff --git a/backend/app/api/agent.py b/backend/app/api/agent.py index a290420b..fbff168f 100644 --- a/backend/app/api/agent.py +++ b/backend/app/api/agent.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from enum import Enum from typing import TYPE_CHECKING, Any, cast from uuid import UUID @@ -20,6 +21,7 @@ from app.core.agent_auth import AgentAuthContext, get_agent_auth_context from app.db.pagination import paginate from app.db.session import get_session from app.models.agents import Agent +from app.models.board_webhook_payloads import BoardWebhookPayload from app.models.boards import Board from app.models.tags import Tag from app.models.task_dependencies import TaskDependency @@ -33,6 +35,7 @@ from app.schemas.agents import ( from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalStatus from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead from app.schemas.board_onboarding import BoardOnboardingAgentUpdate, BoardOnboardingRead +from app.schemas.board_webhooks import BoardWebhookPayloadRead from app.schemas.boards import BoardRead from app.schemas.common import OkResponse from app.schemas.errors import LLMErrorResponse @@ -167,6 +170,53 @@ def _agent_board_openapi_hints( } +def _truncate_preview(raw: str, max_chars: int) -> str: + if len(raw) <= max_chars: + return raw + if max_chars <= 3: + return raw[:max_chars] + return f"{raw[: max_chars - 3]}..." + + +def _payload_preview_with_limit( + value: dict[str, object] | list[object] | str | int | float | bool | None, + *, + max_chars: int, +) -> tuple[str, bool]: + if isinstance(value, str): + return _truncate_preview(value, max_chars), len(value) > max_chars + + try: + # Stream JSON chunks so we can stop once we know truncation is required. + encoder = json.JSONEncoder(ensure_ascii=True) + parts: list[str] = [] + current_len = 0 + truncated = False + for chunk in encoder.iterencode(value): + remaining = (max_chars + 1) - current_len + if remaining <= 0: + truncated = True + break + if len(chunk) <= remaining: + parts.append(chunk) + current_len += len(chunk) + continue + parts.append(chunk[:remaining]) + current_len += remaining + truncated = True + break + raw = "".join(parts) + except TypeError: + raw = str(value) + return _truncate_preview(raw, max_chars), len(raw) > max_chars + + if len(raw) > max_chars: + truncated = True + if not truncated: + return raw, False + return _truncate_preview(raw, max_chars), True + + def _guard_board_access(agent_ctx: AgentAuthContext, board: Board) -> None: allowed = not (agent_ctx.agent.board_id and agent_ctx.agent.board_id != board.id) OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed) @@ -572,6 +622,73 @@ async def list_tags( ] +@router.get( + "/boards/{board_id}/webhooks/{webhook_id}/payloads/{payload_id}", + response_model=BoardWebhookPayloadRead, + tags=AGENT_BOARD_TAGS, + openapi_extra=_agent_board_openapi_hints( + intent="agent_board_webhook_payload_read", + when_to_use=[ + "Agent needs to inspect a previously captured webhook payload for this board.", + "Agent is reconciling missed webhook events or deduping inbound processing.", + ], + routing_examples=[ + { + "input": { + "intent": "inspect stored webhook payload by id", + "required_privilege": "any_agent", + }, + "decision": "agent_board_webhook_payload_read", + }, + { + "input": { + "intent": "list tasks for planning", + "required_privilege": "any_agent", + }, + "decision": "agent_board_task_discovery", + }, + ], + ), +) +async def get_webhook_payload( + webhook_id: UUID, + payload_id: UUID, + max_chars: int | None = Query(default=None, ge=1, le=1_000_000), + board: Board = BOARD_DEP, + session: AsyncSession = SESSION_DEP, + agent_ctx: AgentAuthContext = AGENT_CTX_DEP, +) -> BoardWebhookPayloadRead: + """Fetch a stored webhook payload (agent-accessible, read-only). + + This enables board-scoped agents to backfill dropped webhook events and enforce + idempotency by inspecting previously received payloads. + + If `max_chars` is provided and the serialized payload exceeds the limit, + the response payload is returned as a truncated string preview. + """ + + _guard_board_access(agent_ctx, board) + + payload = ( + await session.exec( + select(BoardWebhookPayload) + .where(col(BoardWebhookPayload.id) == payload_id) + .where(col(BoardWebhookPayload.board_id) == board.id) + .where(col(BoardWebhookPayload.webhook_id) == webhook_id), + ) + ).first() + if payload is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + response = BoardWebhookPayloadRead.model_validate(payload, from_attributes=True) + if max_chars is not None and response.payload is not None: + preview, was_truncated = _payload_preview_with_limit(response.payload, max_chars=max_chars) + if was_truncated: + response.payload = preview + + return response + + @router.post( "/boards/{board_id}/tasks", response_model=TaskRead, @@ -742,6 +859,7 @@ async def create_task( task_id=task.id, message=f"Task created by lead: {task.title}.", agent_id=agent_ctx.agent.id, + board_id=task.board_id, ) await session.commit() if task.assigned_agent_id: @@ -1429,11 +1547,25 @@ async def get_agent_soul( target_agent_id=agent_id, ) coordination = GatewayCoordinationService(session) - return await coordination.get_agent_soul( - board=board, - target_agent_id=agent_id, - correlation_id=f"soul.read:{board.id}:{agent_id}", - ) + try: + return await coordination.get_agent_soul( + board=board, + target_agent_id=agent_id, + correlation_id=f"soul.read:{board.id}:{agent_id}", + ) + except HTTPException as exc: + # Keep explicit auth/not-found responses, but avoid relaying internal 5xx details. + if exc.status_code >= status.HTTP_500_INTERNAL_SERVER_ERROR: + raise HTTPException( + status_code=exc.status_code, + detail="Gateway SOUL read failed", + ) from exc + raise + except Exception as exc: # pragma: no cover - defensive API boundary guard + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail="Gateway SOUL read failed", + ) from exc @router.put( diff --git a/backend/app/api/approvals.py b/backend/app/api/approvals.py index 723444c0..43a37f0c 100644 --- a/backend/app/api/approvals.py +++ b/backend/app/api/approvals.py @@ -266,6 +266,7 @@ async def _notify_lead_on_approval_resolution( message=f"Lead agent notified for {approval.status} approval {approval.id}.", agent_id=lead.id, task_id=approval.task_id, + board_id=approval.board_id, ) else: record_activity( @@ -274,6 +275,7 @@ async def _notify_lead_on_approval_resolution( message=f"Lead notify failed for approval {approval.id}: {error}", agent_id=lead.id, task_id=approval.task_id, + board_id=approval.board_id, ) await session.commit() diff --git a/backend/app/api/board_group_memory.py b/backend/app/api/board_group_memory.py index d965c718..b9b1fd63 100644 --- a/backend/app/api/board_group_memory.py +++ b/backend/app/api/board_group_memory.py @@ -331,7 +331,7 @@ async def _notify_group_memory_targets( if len(snippet) > MAX_SNIPPET_LENGTH: snippet = f"{snippet[: MAX_SNIPPET_LENGTH - 3]}..." - base_url = settings.base_url or "http://localhost:8000" + base_url = settings.base_url context = _NotifyGroupContext( session=session, diff --git a/backend/app/api/board_memory.py b/backend/app/api/board_memory.py index bf234be3..d04ea3f5 100644 --- a/backend/app/api/board_memory.py +++ b/backend/app/api/board_memory.py @@ -191,7 +191,7 @@ async def _notify_chat_targets( snippet = memory.content.strip() if len(snippet) > MAX_SNIPPET_LENGTH: snippet = f"{snippet[: MAX_SNIPPET_LENGTH - 3]}..." - base_url = settings.base_url or "http://localhost:8000" + base_url = settings.base_url for agent in targets.values(): if not agent.openclaw_session_id: continue diff --git a/backend/app/api/board_onboarding.py b/backend/app/api/board_onboarding.py index e0ac1a7b..51be333e 100644 --- a/backend/app/api/board_onboarding.py +++ b/backend/app/api/board_onboarding.py @@ -229,7 +229,7 @@ async def start_onboarding( return onboarding dispatcher = BoardOnboardingMessagingService(session) - base_url = settings.base_url or "http://localhost:8000" + base_url = settings.base_url prompt = ( "BOARD ONBOARDING REQUEST\n\n" f"Board Name: {board.name}\n" diff --git a/backend/app/api/boards.py b/backend/app/api/boards.py index 01d874d1..d7b7e870 100644 --- a/backend/app/api/boards.py +++ b/backend/app/api/boards.py @@ -2,6 +2,8 @@ from __future__ import annotations +import json +from datetime import datetime from enum import Enum from typing import TYPE_CHECKING, Literal, cast from uuid import UUID @@ -63,6 +65,43 @@ _ERR_GATEWAY_MAIN_AGENT_REQUIRED = ( ) +def _format_board_field_value(value: object) -> str: + if isinstance(value, datetime): + return value.isoformat() + if isinstance(value, UUID): + return str(value) + if isinstance(value, dict): + return json.dumps(value, sort_keys=True, default=str) + if isinstance(value, bool): + return "true" if value else "false" + if value is None: + return "null" + return str(value) + + +def _board_update_message( + *, + board: Board, + changed_fields: dict[str, tuple[object, object]], +) -> str: + lines = [ + "BOARD UPDATED", + f"Board: {board.name}", + f"Board ID: {board.id}", + "", + "Changed fields:", + ] + for field_name in sorted(changed_fields): + previous, current = changed_fields[field_name] + lines.append( + f"- {field_name}: {_format_board_field_value(previous)}" + f" -> {_format_board_field_value(current)}" + ) + lines.append("") + lines.append("Take action: review the board changes and adjust plan/assignments as needed.") + return "\n".join(lines) + + async def _require_gateway_main_agent(session: AsyncSession, gateway: Gateway) -> None: main_agent = ( await Agent.objects.filter_by(gateway_id=gateway.id) @@ -306,6 +345,7 @@ async def _notify_agents_on_board_group_change( f"{recipient_board.name} related to {board.name} and {group.name}." ), agent_id=agent.id, + board_id=recipient_board.id, ) else: failed += 1 @@ -317,6 +357,7 @@ async def _notify_agents_on_board_group_change( f"{recipient_board.name}: {error}" ), agent_id=agent.id, + board_id=recipient_board.id, ) if notified or failed: @@ -366,6 +407,55 @@ async def _notify_agents_on_board_group_removal( ) +async def _notify_lead_on_board_update( + *, + session: AsyncSession, + board: Board, + changed_fields: dict[str, tuple[object, object]], +) -> None: + if not changed_fields: + return + lead = ( + await Agent.objects.filter_by(board_id=board.id) + .filter(col(Agent.is_board_lead).is_(True)) + .first(session) + ) + if lead is None or not lead.openclaw_session_id: + return + dispatch = GatewayDispatchService(session) + config = await dispatch.optional_gateway_config_for_board(board) + if config is None: + return + message = _board_update_message( + board=board, + changed_fields=changed_fields, + ) + error = await dispatch.try_send_agent_message( + session_key=lead.openclaw_session_id, + config=config, + agent_name=lead.name, + message=message, + deliver=False, + ) + if error is None: + record_activity( + session, + event_type="board.lead_notified", + message=f"Lead agent notified for board update: {board.name}.", + agent_id=lead.id, + board_id=board.id, + ) + else: + record_activity( + session, + event_type="board.lead_notify_failed", + message=f"Lead board update notify failed for {board.name}: {error}", + agent_id=lead.id, + board_id=board.id, + ) + await session.commit() + + @router.get("", response_model=DefaultLimitOffsetPage[BoardRead]) async def list_boards( gateway_id: UUID | None = GATEWAY_ID_QUERY, @@ -450,8 +540,19 @@ async def update_board( board: Board = BOARD_USER_WRITE_DEP, ) -> Board: """Update mutable board properties.""" + requested_updates = payload.model_dump(exclude_unset=True) + previous_values = { + field_name: getattr(board, field_name) + for field_name in requested_updates + if hasattr(board, field_name) + } previous_group_id = board.board_group_id updated = await _apply_board_update(payload=payload, session=session, board=board) + changed_fields = { + field_name: (previous_value, getattr(updated, field_name)) + for field_name, previous_value in previous_values.items() + if previous_value != getattr(updated, field_name) + } new_group_id = updated.board_group_id if previous_group_id is not None and previous_group_id != new_group_id: previous_group = await crud.get_by_id(session, BoardGroup, previous_group_id) @@ -483,6 +584,19 @@ async def update_board( updated.id, new_group_id, ) + if changed_fields: + try: + await _notify_lead_on_board_update( + session=session, + board=updated, + changed_fields=changed_fields, + ) + except (OpenClawGatewayError, OSError, RuntimeError, ValueError): + logger.exception( + "board.update.notify_lead_unexpected board_id=%s changed_fields=%s", + updated.id, + sorted(changed_fields), + ) return updated diff --git a/backend/app/api/gateway.py b/backend/app/api/gateway.py index 12b24117..32fea432 100644 --- a/backend/app/api/gateway.py +++ b/backend/app/api/gateway.py @@ -37,11 +37,15 @@ def _query_to_resolve_input( board_id: str | None = Query(default=None), gateway_url: str | None = Query(default=None), gateway_token: str | None = Query(default=None), + gateway_disable_device_pairing: bool | None = Query(default=None), + gateway_allow_insecure_tls: bool | None = Query(default=None), ) -> GatewayResolveQuery: return GatewaySessionService.to_resolve_query( board_id=board_id, gateway_url=gateway_url, gateway_token=gateway_token, + gateway_disable_device_pairing=gateway_disable_device_pairing, + gateway_allow_insecure_tls=gateway_allow_insecure_tls, ) diff --git a/backend/app/api/gateways.py b/backend/app/api/gateways.py index 3f756ce7..6c579930 100644 --- a/backend/app/api/gateways.py +++ b/backend/app/api/gateways.py @@ -94,7 +94,12 @@ async def create_gateway( ) -> Gateway: """Create a gateway and provision or refresh its main agent.""" service = GatewayAdminLifecycleService(session) - await service.assert_gateway_runtime_compatible(url=payload.url, token=payload.token) + await service.assert_gateway_runtime_compatible( + url=payload.url, + token=payload.token, + allow_insecure_tls=payload.allow_insecure_tls, + disable_device_pairing=payload.disable_device_pairing, + ) data = payload.model_dump() gateway_id = uuid4() data["id"] = gateway_id @@ -134,12 +139,28 @@ async def update_gateway( organization_id=ctx.organization.id, ) updates = payload.model_dump(exclude_unset=True) - if "url" in updates or "token" in updates: + if ( + "url" in updates + or "token" in updates + or "allow_insecure_tls" in updates + or "disable_device_pairing" in updates + ): raw_next_url = updates.get("url", gateway.url) next_url = raw_next_url.strip() if isinstance(raw_next_url, str) else "" next_token = updates.get("token", gateway.token) + next_allow_insecure_tls = bool( + updates.get("allow_insecure_tls", gateway.allow_insecure_tls), + ) + next_disable_device_pairing = bool( + updates.get("disable_device_pairing", gateway.disable_device_pairing), + ) if next_url: - await service.assert_gateway_runtime_compatible(url=next_url, token=next_token) + await service.assert_gateway_runtime_compatible( + url=next_url, + token=next_token, + allow_insecure_tls=next_allow_insecure_tls, + disable_device_pairing=next_disable_device_pairing, + ) await crud.patch(session, gateway, updates) await service.ensure_main_agent(gateway, auth, action="update") return gateway diff --git a/backend/app/api/metrics.py b/backend/app/api/metrics.py index 5b242d95..cd716952 100644 --- a/backend/app/api/metrics.py +++ b/backend/app/api/metrics.py @@ -18,12 +18,15 @@ from app.core.time import utcnow from app.db.session import get_session from app.models.activity_events import ActivityEvent from app.models.agents import Agent +from app.models.approvals import Approval from app.models.boards import Board from app.models.tasks import Task from app.schemas.metrics import ( DashboardBucketKey, DashboardKpis, DashboardMetrics, + DashboardPendingApproval, + DashboardPendingApprovals, DashboardRangeKey, DashboardRangeSeries, DashboardSeriesPoint, @@ -169,7 +172,7 @@ async def _query_throughput( bucket_col = func.date_trunc(range_spec.bucket, Task.updated_at).label("bucket") statement = ( select(bucket_col, func.count()) - .where(col(Task.status) == "review") + .where(col(Task.status) == "done") .where(col(Task.updated_at) >= range_spec.start) .where(col(Task.updated_at) <= range_spec.end) ) @@ -370,22 +373,79 @@ async def _active_agents( return int(result) -async def _tasks_in_progress( +async def _task_status_counts( session: AsyncSession, - range_spec: RangeSpec, board_ids: list[UUID], -) -> int: +) -> dict[str, int]: if not board_ids: - return 0 + return { + "inbox": 0, + "in_progress": 0, + "review": 0, + "done": 0, + } statement = ( - select(func.count()) - .where(col(Task.status) == "in_progress") - .where(col(Task.updated_at) >= range_spec.start) - .where(col(Task.updated_at) <= range_spec.end) + select(col(Task.status), func.count()) .where(col(Task.board_id).in_(board_ids)) + .group_by(col(Task.status)) ) - result = (await session.exec(statement)).one() - return int(result) + results = (await session.exec(statement)).all() + counts = { + "inbox": 0, + "in_progress": 0, + "review": 0, + "done": 0, + } + for status_value, total in results: + key = str(status_value) + if key in counts: + counts[key] = int(total or 0) + return counts + + +async def _pending_approvals_snapshot( + session: AsyncSession, + board_ids: list[UUID], + *, + limit: int = 10, +) -> DashboardPendingApprovals: + if not board_ids: + return DashboardPendingApprovals(total=0, items=[]) + + total_statement = ( + select(func.count(col(Approval.id))) + .where(col(Approval.board_id).in_(board_ids)) + .where(col(Approval.status) == "pending") + ) + total = int((await session.exec(total_statement)).one() or 0) + if total == 0: + return DashboardPendingApprovals(total=0, items=[]) + + rows = ( + await session.exec( + select(Approval, Board, Task) + .join(Board, col(Board.id) == col(Approval.board_id)) + .outerjoin(Task, col(Task.id) == col(Approval.task_id)) + .where(col(Approval.board_id).in_(board_ids)) + .where(col(Approval.status) == "pending") + .order_by(col(Approval.created_at).desc()) + .limit(limit) + ) + ).all() + + items = [ + DashboardPendingApproval( + approval_id=approval.id, + board_id=approval.board_id, + board_name=board.name, + action_type=approval.action_type, + confidence=float(approval.confidence), + created_at=approval.created_at, + task_title=task.title if task is not None else None, + ) + for approval, board, task in rows + ] + return DashboardPendingApprovals(total=total, items=items) async def _resolve_dashboard_board_ids( @@ -461,10 +521,16 @@ async def dashboard_metrics( primary=wip_primary, comparison=wip_comparison, ) + task_status_counts = await _task_status_counts(session, board_ids) + pending_approvals = await _pending_approvals_snapshot(session, board_ids, limit=10) kpis = DashboardKpis( active_agents=await _active_agents(session, primary, board_ids), - tasks_in_progress=await _tasks_in_progress(session, primary, board_ids), + tasks_in_progress=task_status_counts["in_progress"], + inbox_tasks=task_status_counts["inbox"], + in_progress_tasks=task_status_counts["in_progress"], + review_tasks=task_status_counts["review"], + done_tasks=task_status_counts["done"], error_rate_pct=await _error_rate_kpi(session, primary, board_ids), median_cycle_time_hours_7d=await _median_cycle_time_for_range( session, @@ -481,4 +547,5 @@ async def dashboard_metrics( cycle_time=cycle_time, error_rate=error_rate, wip=wip, + pending_approvals=pending_approvals, ) diff --git a/backend/app/api/skills_marketplace.py b/backend/app/api/skills_marketplace.py index d6e552c2..9ac7db7b 100644 --- a/backend/app/api/skills_marketplace.py +++ b/backend/app/api/skills_marketplace.py @@ -50,7 +50,7 @@ ORG_ADMIN_DEP = Depends(require_org_admin) GATEWAY_ID_QUERY = Query(...) ALLOWED_PACK_SOURCE_SCHEMES = {"https"} -GIT_CLONE_TIMEOUT_SECONDS = 30 +GIT_CLONE_TIMEOUT_SECONDS = 600 GIT_REV_PARSE_TIMEOUT_SECONDS = 10 BRANCH_NAME_ALLOWED_RE = r"^[A-Za-z0-9._/\-]+$" SKILLS_INDEX_READ_CHUNK_BYTES = 16 * 1024 diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index ae1bf981..d7469a1b 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -259,6 +259,19 @@ async def _require_review_before_done_when_enabled( raise _review_required_for_done_error() +async def _require_comment_for_review_when_enabled( + session: AsyncSession, + *, + board_id: UUID, +) -> bool: + requires_comment = ( + await session.exec( + select(col(Board.comment_required_for_review)).where(col(Board.id) == board_id), + ) + ).first() + return bool(requires_comment) + + async def _require_no_pending_approval_for_status_change_when_enabled( session: AsyncSession, *, @@ -318,22 +331,41 @@ async def has_valid_recent_comment( def _parse_since(value: str | None) -> datetime | None: + """Parse an optional ISO-8601 timestamp into a naive UTC `datetime`. + + The API accepts either naive timestamps (treated as UTC) or timezone-aware values. + Returning naive UTC simplifies SQLModel comparisons against stored naive UTC values. + """ + if not value: return None + normalized = value.strip() if not normalized: return None + + # Allow common ISO-8601 `Z` suffix (UTC) even though `datetime.fromisoformat` expects `+00:00`. normalized = normalized.replace("Z", "+00:00") + try: parsed = datetime.fromisoformat(normalized) except ValueError: return None + if parsed.tzinfo is not None: return parsed.astimezone(UTC).replace(tzinfo=None) + + # No tzinfo: interpret as UTC for consistency with other API timestamps. return parsed def _coerce_task_items(items: Sequence[object]) -> list[Task]: + """Validate/convert paginated query results to a concrete `list[Task]`. + + SQLModel pagination helpers return `Sequence[object]`; we validate types early so the + rest of the route logic can assume real `Task` instances. + """ + tasks: list[Task] = [] for item in items: if not isinstance(item, Task): @@ -346,6 +378,15 @@ def _coerce_task_items(items: Sequence[object]) -> list[Task]: def _coerce_task_event_rows( items: Sequence[object], ) -> list[tuple[ActivityEvent, Task | None]]: + """Normalize DB rows into `(ActivityEvent, Task | None)` tuples. + + Depending on the SQLAlchemy/SQLModel execution path, result rows may arrive as: + - real Python tuples, or + - row-like objects supporting `__len__` and `__getitem__`. + + This helper centralizes validation so SSE/event-stream logic can assume a stable shape. + """ + rows: list[tuple[ActivityEvent, Task | None]] = [] for item in items: first: object @@ -382,6 +423,12 @@ async def _lead_was_mentioned( task: Task, lead: Agent, ) -> bool: + """Return `True` if the lead agent is mentioned in any comment on the task. + + This is used to avoid redundant lead pings (especially in auto-created tasks) while still + ensuring escalation happens when explicitly requested. + """ + statement = ( select(ActivityEvent.message) .where(col(ActivityEvent.task_id) == task.id) @@ -398,6 +445,8 @@ async def _lead_was_mentioned( def _lead_created_task(task: Task, lead: Agent) -> bool: + """Return `True` if `task` was auto-created by the lead agent.""" + if not task.auto_created or not task.auto_reason: return False return task.auto_reason == f"lead_agent:{lead.id}" @@ -411,6 +460,13 @@ async def _reconcile_dependents_for_dependency_toggle( previous_status: str, actor_agent_id: UUID | None, ) -> None: + """Apply dependency side-effects when a dependency task toggles done/undone. + + The UI models dependencies as a DAG: when a dependency is reopened, dependents that were + previously marked done may need to be reopened or flagged. This helper keeps dependent state + consistent with the dependency graph without duplicating logic across endpoints. + """ + done_toggled = (previous_status == "done") != (dependency_task.status == "done") if not done_toggled: return @@ -455,6 +511,7 @@ async def _reconcile_dependents_for_dependency_toggle( "Task returned to inbox: dependency reopened " f"({dependency_task.title})." ), agent_id=actor_agent_id, + board_id=dependent.board_id, ) else: record_activity( @@ -463,6 +520,7 @@ async def _reconcile_dependents_for_dependency_toggle( task_id=dependent.id, message=f"Dependency completion changed: {dependency_task.title}.", agent_id=actor_agent_id, + board_id=dependent.board_id, ) else: record_activity( @@ -471,6 +529,7 @@ async def _reconcile_dependents_for_dependency_toggle( task_id=dependent.id, message=f"Dependency completion changed: {dependency_task.title}.", agent_id=actor_agent_id, + board_id=dependent.board_id, ) @@ -533,6 +592,75 @@ async def _send_agent_task_message( ) +def _assignment_notification_message(*, board: Board, task: Task, agent: Agent) -> str: + description = _truncate_snippet(task.description or "") + details = [ + f"Board: {board.name}", + f"Task: {task.title}", + f"Task ID: {task.id}", + f"Status: {task.status}", + ] + if description: + details.append(f"Description: {description}") + if task.status == "review" and agent.is_board_lead: + action = ( + "Take action: review the deliverables now. " + "Approve by moving to done or return to inbox with clear feedback." + ) + return "TASK READY FOR LEAD REVIEW\n" + "\n".join(details) + f"\n\n{action}" + return ( + "TASK ASSIGNED\n" + + "\n".join(details) + + ("\n\nTake action: open the task and begin work. " "Post updates as task comments.") + ) + + +def _rework_notification_message( + *, + board: Board, + task: Task, + feedback: str | None, +) -> str: + description = _truncate_snippet(task.description or "") + details = [ + f"Board: {board.name}", + f"Task: {task.title}", + f"Task ID: {task.id}", + f"Status: {task.status}", + ] + if description: + details.append(f"Description: {description}") + requested_changes = ( + _truncate_snippet(feedback) + if feedback and feedback.strip() + else "Lead requested changes. Review latest task comments for exact required updates." + ) + return ( + "CHANGES REQUESTED\n" + + "\n".join(details) + + "\n\nRequested changes:\n" + + requested_changes + + "\n\nTake action: address the requested changes, then move the task back to review." + ) + + +async def _latest_task_comment_by_agent( + session: AsyncSession, + *, + task_id: UUID, + agent_id: UUID, +) -> str | None: + statement = ( + select(col(ActivityEvent.message)) + .where(col(ActivityEvent.task_id) == task_id) + .where(col(ActivityEvent.event_type) == "task.comment") + .where(col(ActivityEvent.agent_id) == agent_id) + .order_by(desc(col(ActivityEvent.created_at))) + .limit(1) + ) + return (await session.exec(statement)).first() + + async def _notify_agent_on_task_assign( *, session: AsyncSession, @@ -546,20 +674,7 @@ async def _notify_agent_on_task_assign( config = await dispatch.optional_gateway_config_for_board(board) if config is None: return - description = _truncate_snippet(task.description or "") - details = [ - f"Board: {board.name}", - f"Task: {task.title}", - f"Task ID: {task.id}", - f"Status: {task.status}", - ] - if description: - details.append(f"Description: {description}") - message = ( - "TASK ASSIGNED\n" - + "\n".join(details) - + ("\n\nTake action: open the task and begin work. " "Post updates as task comments.") - ) + message = _assignment_notification_message(board=board, task=task, agent=agent) error = await _send_agent_task_message( dispatch=dispatch, session_key=agent.openclaw_session_id, @@ -574,6 +689,7 @@ async def _notify_agent_on_task_assign( message=f"Agent notified for assignment: {agent.name}.", agent_id=agent.id, task_id=task.id, + board_id=board.id, ) await session.commit() else: @@ -583,6 +699,60 @@ async def _notify_agent_on_task_assign( message=f"Assignee notify failed: {error}", agent_id=agent.id, task_id=task.id, + board_id=board.id, + ) + await session.commit() + + +async def _notify_agent_on_task_rework( + *, + session: AsyncSession, + board: Board, + task: Task, + agent: Agent, + lead: Agent, +) -> None: + if not agent.openclaw_session_id: + return + dispatch = GatewayDispatchService(session) + config = await dispatch.optional_gateway_config_for_board(board) + if config is None: + return + feedback = await _latest_task_comment_by_agent( + session, + task_id=task.id, + agent_id=lead.id, + ) + message = _rework_notification_message( + board=board, + task=task, + feedback=feedback, + ) + error = await _send_agent_task_message( + dispatch=dispatch, + session_key=agent.openclaw_session_id, + config=config, + agent_name=agent.name, + message=message, + ) + if error is None: + record_activity( + session, + event_type="task.rework_notified", + message=f"Assignee notified about requested changes: {agent.name}.", + agent_id=agent.id, + task_id=task.id, + board_id=board.id, + ) + await session.commit() + else: + record_activity( + session, + event_type="task.rework_notify_failed", + message=f"Rework notify failed: {error}", + agent_id=agent.id, + task_id=task.id, + board_id=board.id, ) await session.commit() @@ -647,6 +817,7 @@ async def _notify_lead_on_task_create( message=f"Lead agent notified for task: {task.title}.", agent_id=lead.id, task_id=task.id, + board_id=board.id, ) await session.commit() else: @@ -656,6 +827,7 @@ async def _notify_lead_on_task_create( message=f"Lead notify failed: {error}", agent_id=lead.id, task_id=task.id, + board_id=board.id, ) await session.commit() @@ -704,6 +876,7 @@ async def _notify_lead_on_task_unassigned( message=f"Lead notified task returned to inbox: {task.title}.", agent_id=lead.id, task_id=task.id, + board_id=board.id, ) await session.commit() else: @@ -713,6 +886,7 @@ async def _notify_lead_on_task_unassigned( message=f"Lead notify failed: {error}", agent_id=lead.id, task_id=task.id, + board_id=board.id, ) await session.commit() @@ -1137,7 +1311,10 @@ def _task_event_payload( resolved_custom_field_values_by_task_id = custom_field_values_by_task_id or {} payload: dict[str, object] = { "type": event.event_type, - "activity": ActivityEventRead.model_validate(event).model_dump(mode="json"), + "activity": ActivityEventRead.model_validate(event).model_dump( + mode="json", + exclude={"board_id", "route_name", "route_params"}, + ), } if event.event_type == "task.comment": payload["comment"] = _serialize_comment(event) @@ -1337,6 +1514,7 @@ async def create_task( event_type="task.created", task_id=task.id, message=f"Task created: {task.title}.", + board_id=board.id, ) await session.commit() await _notify_lead_on_task_create(session=session, board=board, task=task) @@ -1905,7 +2083,42 @@ async def _lead_apply_assignment( update.task.assigned_agent_id = agent.id -def _lead_apply_status(update: _TaskUpdateInput) -> None: +async def _last_worker_who_moved_task_to_review( + session: AsyncSession, + *, + task_id: UUID, + board_id: UUID, + lead_agent_id: UUID, +) -> UUID | None: + statement = ( + select(col(ActivityEvent.agent_id)) + .where(col(ActivityEvent.task_id) == task_id) + .where(col(ActivityEvent.event_type) == "task.status_changed") + .where(col(ActivityEvent.message).like("Task moved to review:%")) + .where(col(ActivityEvent.agent_id).is_not(None)) + .order_by(desc(col(ActivityEvent.created_at))) + ) + candidate_ids = list(await session.exec(statement)) + for candidate_id in candidate_ids: + if candidate_id is None or candidate_id == lead_agent_id: + continue + candidate = await Agent.objects.by_id(candidate_id).first(session) + if candidate is None: + continue + if candidate.board_id != board_id or candidate.is_board_lead: + continue + return candidate.id + return None + + +async def _lead_apply_status( + session: AsyncSession, + *, + update: _TaskUpdateInput, +) -> None: + if update.actor.actor_type != "agent" or update.actor.agent is None: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + lead_agent = update.actor.agent if "status" not in update.updates: return if update.task.status != "review": @@ -1926,7 +2139,12 @@ def _lead_apply_status(update: _TaskUpdateInput) -> None: ), ) if target_status == "inbox": - update.task.assigned_agent_id = None + update.task.assigned_agent_id = await _last_worker_who_moved_task_to_review( + session, + task_id=update.task.id, + board_id=update.board_id, + lead_agent_id=lead_agent.id, + ) update.task.in_progress_at = None update.task.status = target_status @@ -1958,6 +2176,21 @@ async def _lead_notify_new_assignee( else None ) if board: + if ( + update.previous_status == "review" + and update.task.status == "inbox" + and update.actor.actor_type == "agent" + and update.actor.agent + and update.actor.agent.is_board_lead + ): + await _notify_agent_on_task_rework( + session=session, + board=board, + task=update.task, + agent=assigned_agent, + lead=update.actor.agent, + ) + return await _notify_agent_on_task_assign( session=session, board=board, @@ -1994,7 +2227,7 @@ async def _apply_lead_task_update( raise _blocked_task_error(blocked_by) await _lead_apply_assignment(session, update=update) - _lead_apply_status(update) + await _lead_apply_status(session, update=update) await _require_no_pending_approval_for_status_change_when_enabled( session, board_id=update.board_id, @@ -2040,6 +2273,7 @@ async def _apply_lead_task_update( task_id=update.task.id, message=message, agent_id=update.actor.agent.id, + board_id=update.board_id, ) await _reconcile_dependents_for_dependency_toggle( session, @@ -2225,6 +2459,7 @@ async def _record_task_comment_from_update( event_type="task.comment", message=update.comment, task_id=update.task.id, + board_id=update.task.board_id, agent_id=( update.actor.agent.id if update.actor.actor_type == "agent" and update.actor.agent @@ -2252,6 +2487,7 @@ async def _record_task_update_activity( task_id=update.task.id, message=message, agent_id=actor_agent_id, + board_id=update.board_id, ) await _reconcile_dependents_for_dependency_toggle( session, @@ -2263,6 +2499,23 @@ async def _record_task_update_activity( await session.commit() +async def _assign_review_task_to_lead( + session: AsyncSession, + *, + update: _TaskUpdateInput, +) -> None: + if update.task.status != "review" or update.previous_status == "review": + return + lead = ( + await Agent.objects.filter_by(board_id=update.board_id) + .filter(col(Agent.is_board_lead).is_(True)) + .first(session) + ) + if lead is None: + return + update.task.assigned_agent_id = lead.id + + async def _notify_task_update_assignment_changes( session: AsyncSession, *, @@ -2290,12 +2543,6 @@ async def _notify_task_update_assignment_changes( or update.task.assigned_agent_id == update.previous_assigned ): return - if ( - update.actor.actor_type == "agent" - and update.actor.agent - and update.task.assigned_agent_id == update.actor.agent.id - ): - return assigned_agent = await Agent.objects.by_id(update.task.assigned_agent_id).first( session, ) @@ -2306,6 +2553,28 @@ async def _notify_task_update_assignment_changes( if update.task.board_id else None ) + if ( + update.previous_status == "review" + and update.task.status == "inbox" + and update.actor.actor_type == "agent" + and update.actor.agent + and update.actor.agent.is_board_lead + ): + if board: + await _notify_agent_on_task_rework( + session=session, + board=board, + task=update.task, + agent=assigned_agent, + lead=update.actor.agent, + ) + return + if ( + update.actor.actor_type == "agent" + and update.actor.agent + and update.task.assigned_agent_id == update.actor.agent.id + ): + return if board: await _notify_agent_on_task_assign( session=session, @@ -2346,9 +2615,12 @@ async def _finalize_updated_task( update.task.updated_at = utcnow() status_raw = update.updates.get("status") - # Entering review requires either a new comment or a valid recent one to - # ensure reviewers get context on readiness. - if status_raw == "review": + # Entering review can require a new comment or valid recent context when + # the board-level rule is enabled. + if status_raw == "review" and await _require_comment_for_review_when_enabled( + session, + board_id=update.board_id, + ): comment_text = (update.comment or "").strip() review_comment_author = update.task.assigned_agent_id or update.previous_assigned review_comment_since = ( @@ -2363,6 +2635,7 @@ async def _finalize_updated_task( review_comment_since, ): raise _comment_validation_error() + await _assign_review_task_to_lead(session, update=update) if update.tag_ids is not None: normalized = ( @@ -2414,6 +2687,7 @@ async def create_task_comment( event_type="task.comment", message=payload.message, task_id=task.id, + board_id=task.board_id, agent_id=_comment_actor_id(actor), ) session.add(event) diff --git a/backend/app/core/agent_auth.py b/backend/app/core/agent_auth.py index 1bd2b7eb..97d92ad6 100644 --- a/backend/app/core/agent_auth.py +++ b/backend/app/core/agent_auth.py @@ -143,11 +143,19 @@ async def get_agent_auth_context_optional( authorization: str | None = Header(default=None, alias="Authorization"), session: AsyncSession = SESSION_DEP, ) -> AgentAuthContext | None: - """Optionally resolve agent auth context from `X-Agent-Token` only.""" + """Optionally resolve agent auth context from `X-Agent-Token` or `Authorization: Bearer`. + + Both `X-Agent-Token` and `Authorization: Bearer ` are accepted so that + routes depending on this function (e.g. board/task dependency resolvers) behave + consistently with `get_agent_auth_context`, which also accepts both headers. + Previously, `accept_authorization=False` caused 401 on any route that resolved + a board or task via the shared `ACTOR_DEP` chain (e.g. PATCH /tasks/{id}, + POST /tasks/{id}/comments) when the caller used `Authorization: Bearer`. + """ resolved = _resolve_agent_token( agent_token, authorization, - accept_authorization=False, + accept_authorization=True, ) if not resolved: if agent_token: @@ -160,11 +168,12 @@ async def get_agent_auth_context_optional( return None agent = await _find_agent_for_token(session, resolved) if agent is None: - logger.warning( - "agent auth optional invalid token path=%s token_prefix=%s", - request.url.path, - resolved[:6], - ) - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + if agent_token: + logger.warning( + "agent auth optional invalid token path=%s token_prefix=%s", + request.url.path, + resolved[:6], + ) + return None await _touch_agent_presence(request, session, agent) return AgentAuthContext(actor_type="agent", agent=agent) diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index 256e2bd8..21d4178d 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -66,6 +66,13 @@ class AuthContext: def _extract_bearer_token(authorization: str | None) -> str | None: + """Extract the bearer token from an `Authorization` header. + + Returns `None` for missing/empty headers or non-bearer schemes. + + Note: we do *not* validate the token here; this helper is only responsible for parsing. + """ + if not authorization: return None value = authorization.strip() @@ -92,6 +99,14 @@ def _normalize_email(value: object) -> str | None: def _extract_claim_email(claims: dict[str, object]) -> str | None: + """Best-effort extraction of an email address from Clerk/JWT-like claims. + + Clerk payloads vary depending on token type and SDK version. We try common flat keys first, + then fall back to an `email_addresses` list (either strings or dict-like entries). + + Returns a normalized lowercase email or `None`. + """ + for key in ("email", "email_address", "primary_email_address"): email = _normalize_email(claims.get(key)) if email: @@ -119,10 +134,13 @@ def _extract_claim_email(claims: dict[str, object]) -> str | None: return candidate if fallback_email is None: fallback_email = candidate + return fallback_email def _extract_claim_name(claims: dict[str, object]) -> str | None: + """Best-effort extraction of a display name from Clerk/JWT-like claims.""" + for key in ("name", "full_name"): text = _non_empty_str(claims.get(key)) if text: @@ -137,6 +155,17 @@ def _extract_claim_name(claims: dict[str, object]) -> str | None: def _extract_clerk_profile(profile: ClerkUser | None) -> tuple[str | None, str | None]: + """Extract `(email, name)` from a Clerk user profile. + + The Clerk SDK surface is not perfectly consistent across environments: + - some fields may be absent, + - email addresses may be represented as strings or objects, + - the "primary" email may be identified by id. + + This helper implements a defensive, best-effort extraction strategy and returns `(None, None)` + when the profile is unavailable. + """ + if profile is None: return None, None @@ -208,7 +237,6 @@ async def _authenticate_clerk_request(request: Request) -> RequestState: async def _fetch_clerk_profile(clerk_user_id: str) -> tuple[str | None, str | None]: secret = settings.clerk_secret_key.strip() - secret_kind = secret.split("_", maxsplit=1)[0] if "_" in secret else "unknown" server_url = _normalize_clerk_server_url(settings.clerk_api_url or "") clerk_user_id_log = clerk_user_id[-6:] if clerk_user_id else "" @@ -223,28 +251,24 @@ async def _fetch_clerk_profile(clerk_user_id: str) -> tuple[str | None, str | No return email, name except ClerkErrors as exc: logger.warning( - "auth.clerk.profile.fetch_failed clerk_user_id=%s reason=clerk_errors " - "secret_kind=%s error_type=%s", + "auth.clerk.profile.fetch_failed clerk_user_id=%s reason=clerk_errors " "error_type=%s", clerk_user_id_log, - secret_kind, exc.__class__.__name__, ) except SDKError as exc: logger.warning( "auth.clerk.profile.fetch_failed clerk_user_id=%s status=%s reason=sdk_error " - "server_url=%s secret_kind=%s", + "server_url=%s", clerk_user_id_log, exc.status_code, server_url, - secret_kind, ) except httpx.TimeoutException as exc: logger.warning( "auth.clerk.profile.fetch_failed clerk_user_id=%s reason=timeout " - "server_url=%s secret_kind=%s error=%s", + "server_url=%s error=%s", clerk_user_id_log, server_url, - secret_kind, str(exc) or exc.__class__.__name__, ) except Exception as exc: @@ -264,7 +288,6 @@ async def delete_clerk_user(clerk_user_id: str) -> None: return secret = settings.clerk_secret_key.strip() - secret_kind = secret.split("_", maxsplit=1)[0] if "_" in secret else "unknown" server_url = _normalize_clerk_server_url(settings.clerk_api_url or "") clerk_user_id_log = clerk_user_id[-6:] if clerk_user_id else "" @@ -278,10 +301,8 @@ async def delete_clerk_user(clerk_user_id: str) -> None: logger.info("auth.clerk.user.delete clerk_user_id=%s", clerk_user_id_log) except ClerkErrors as exc: logger.warning( - "auth.clerk.user.delete_failed clerk_user_id=%s reason=clerk_errors " - "secret_kind=%s error_type=%s", + "auth.clerk.user.delete_failed clerk_user_id=%s reason=clerk_errors " "error_type=%s", clerk_user_id_log, - secret_kind, exc.__class__.__name__, ) raise HTTPException( @@ -294,11 +315,10 @@ async def delete_clerk_user(clerk_user_id: str) -> None: return logger.warning( "auth.clerk.user.delete_failed clerk_user_id=%s status=%s reason=sdk_error " - "server_url=%s secret_kind=%s", + "server_url=%s", clerk_user_id_log, exc.status_code, server_url, - secret_kind, ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, diff --git a/backend/app/core/config.py b/backend/app/core/config.py index a6df6a01..46d20683 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -4,6 +4,7 @@ from __future__ import annotations from pathlib import Path from typing import Self +from urllib.parse import urlparse from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -48,7 +49,12 @@ class Settings(BaseSettings): clerk_leeway: float = 10.0 cors_origins: str = "" - base_url: str = "" + base_url: str + # Security response headers (blank disables header injection) + security_header_x_content_type_options: str = "" + security_header_x_frame_options: str = "" + security_header_referrer_policy: str = "" + security_header_permissions_policy: str = "" # Database lifecycle db_auto_migrate: bool = False @@ -88,6 +94,15 @@ class Settings(BaseSettings): raise ValueError( "LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local.", ) + base_url = self.base_url.strip() + if not base_url: + raise ValueError("BASE_URL must be set and non-empty.") + parsed_base_url = urlparse(base_url) + if parsed_base_url.scheme not in {"http", "https"} or not parsed_base_url.netloc: + raise ValueError( + "BASE_URL must be an absolute http(s) URL (e.g. http://localhost:8000).", + ) + self.base_url = base_url.rstrip("/") # In dev, default to applying Alembic migrations at startup to avoid # schema drift (e.g. missing newly-added columns). if "db_auto_migrate" not in self.model_fields_set and self.environment == "dev": diff --git a/backend/app/core/security_headers.py b/backend/app/core/security_headers.py new file mode 100644 index 00000000..758c36b4 --- /dev/null +++ b/backend/app/core/security_headers.py @@ -0,0 +1,81 @@ +"""ASGI middleware for configurable security response headers.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from starlette.types import ASGIApp, Message, Receive, Scope, Send + + +class SecurityHeadersMiddleware: + """Inject configured security headers into every HTTP response.""" + + _X_CONTENT_TYPE_OPTIONS = b"x-content-type-options" + _X_FRAME_OPTIONS = b"x-frame-options" + _REFERRER_POLICY = b"referrer-policy" + _PERMISSIONS_POLICY = b"permissions-policy" + + def __init__( + self, + app: ASGIApp, + *, + x_content_type_options: str = "", + x_frame_options: str = "", + referrer_policy: str = "", + permissions_policy: str = "", + ) -> None: + self._app = app + self._configured_headers = self._build_configured_headers( + x_content_type_options=x_content_type_options, + x_frame_options=x_frame_options, + referrer_policy=referrer_policy, + permissions_policy=permissions_policy, + ) + + @classmethod + def _build_configured_headers( + cls, + *, + x_content_type_options: str, + x_frame_options: str, + referrer_policy: str, + permissions_policy: str, + ) -> tuple[tuple[bytes, bytes, bytes], ...]: + configured: list[tuple[bytes, bytes, bytes]] = [] + for header_name, value in ( + (cls._X_CONTENT_TYPE_OPTIONS, x_content_type_options), + (cls._X_FRAME_OPTIONS, x_frame_options), + (cls._REFERRER_POLICY, referrer_policy), + (cls._PERMISSIONS_POLICY, permissions_policy), + ): + normalized = value.strip() + if not normalized: + continue + configured.append( + ( + header_name.lower(), + header_name, + normalized.encode("latin-1"), + ) + ) + return tuple(configured) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """Append configured security headers unless already present.""" + if scope["type"] != "http" or not self._configured_headers: + await self._app(scope, receive, send) + return + + async def send_with_security_headers(message: Message) -> None: + if message["type"] == "http.response.start": + # Starlette uses `list[tuple[bytes, bytes]]` for raw headers. + headers: list[tuple[bytes, bytes]] = message.setdefault("headers", []) + existing = {key.lower() for key, _ in headers} + for key_lower, key, value in self._configured_headers: + if key_lower not in existing: + headers.append((key, value)) + existing.add(key_lower) + await send(message) + + await self._app(scope, receive, send_with_security_headers) diff --git a/backend/app/main.py b/backend/app/main.py index 0499eddb..4761c24f 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -34,6 +34,7 @@ from app.api.users import router as users_router from app.core.config import settings from app.core.error_handling import install_error_handling from app.core.logging import configure_logging, get_logger +from app.core.security_headers import SecurityHeadersMiddleware from app.db.session import init_db from app.schemas.health import HealthStatusResponse @@ -464,6 +465,13 @@ if origins: else: logger.info("app.cors.disabled") +app.add_middleware( + SecurityHeadersMiddleware, + x_content_type_options=settings.security_header_x_content_type_options, + x_frame_options=settings.security_header_x_frame_options, + referrer_policy=settings.security_header_referrer_policy, + permissions_policy=settings.security_header_permissions_policy, +) install_error_handling(app) diff --git a/backend/app/models/activity_events.py b/backend/app/models/activity_events.py index d5315fe8..4eb1db95 100644 --- a/backend/app/models/activity_events.py +++ b/backend/app/models/activity_events.py @@ -14,7 +14,7 @@ RUNTIME_ANNOTATION_TYPES = (datetime,) class ActivityEvent(QueryModel, table=True): - """Discrete activity event tied to tasks and agents.""" + """Discrete activity event tied to board/task/agent context.""" __tablename__ = "activity_events" # pyright: ignore[reportAssignmentType] @@ -23,4 +23,5 @@ class ActivityEvent(QueryModel, table=True): message: str | None = None agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True) task_id: UUID | None = Field(default=None, foreign_key="tasks.id", index=True) + board_id: UUID | None = Field(default=None, foreign_key="boards.id", index=True) created_at: datetime = Field(default_factory=utcnow) diff --git a/backend/app/models/agents.py b/backend/app/models/agents.py index 1648e98f..9c97e970 100644 --- a/backend/app/models/agents.py +++ b/backend/app/models/agents.py @@ -43,6 +43,11 @@ class Agent(QueryModel, table=True): delete_requested_at: datetime | None = Field(default=None) delete_confirm_token_hash: str | None = Field(default=None, index=True) last_seen_at: datetime | None = Field(default=None) + lifecycle_generation: int = Field(default=0) + wake_attempts: int = Field(default=0) + last_wake_sent_at: datetime | None = Field(default=None) + checkin_deadline_at: datetime | None = Field(default=None) + last_provision_error: str | None = Field(default=None, sa_column=Column(Text)) is_board_lead: bool = Field(default=False, index=True) created_at: datetime = Field(default_factory=utcnow) updated_at: datetime = Field(default_factory=utcnow) diff --git a/backend/app/models/boards.py b/backend/app/models/boards.py index 5d26340b..2023c9c3 100644 --- a/backend/app/models/boards.py +++ b/backend/app/models/boards.py @@ -41,6 +41,7 @@ class Board(TenantScoped, table=True): goal_source: str | None = None require_approval_for_done: bool = Field(default=True) require_review_before_done: bool = Field(default=False) + comment_required_for_review: bool = Field(default=False) block_status_changes_with_pending_approval: bool = Field(default=False) only_lead_can_change_status: bool = Field(default=False) max_agents: int = Field(default=1) diff --git a/backend/app/models/gateways.py b/backend/app/models/gateways.py index 954f144f..3451a10c 100644 --- a/backend/app/models/gateways.py +++ b/backend/app/models/gateways.py @@ -23,6 +23,8 @@ class Gateway(QueryModel, table=True): name: str url: str token: str | None = Field(default=None) + disable_device_pairing: bool = Field(default=False) workspace_root: str + allow_insecure_tls: bool = Field(default=False) created_at: datetime = Field(default_factory=utcnow) updated_at: datetime = Field(default_factory=utcnow) diff --git a/backend/app/schemas/activity_events.py b/backend/app/schemas/activity_events.py index 8c02dd4b..6d5df20d 100644 --- a/backend/app/schemas/activity_events.py +++ b/backend/app/schemas/activity_events.py @@ -18,6 +18,9 @@ class ActivityEventRead(SQLModel): message: str | None agent_id: UUID | None task_id: UUID | None + board_id: UUID | None = None + route_name: str | None = None + route_params: dict[str, str] | None = None created_at: datetime diff --git a/backend/app/schemas/boards.py b/backend/app/schemas/boards.py index c954a1d5..3d2cdef2 100644 --- a/backend/app/schemas/boards.py +++ b/backend/app/schemas/boards.py @@ -31,6 +31,7 @@ class BoardBase(SQLModel): goal_source: str | None = None require_approval_for_done: bool = True require_review_before_done: bool = False + comment_required_for_review: bool = False block_status_changes_with_pending_approval: bool = False only_lead_can_change_status: bool = False max_agents: int = Field(default=1, ge=0) @@ -75,6 +76,7 @@ class BoardUpdate(SQLModel): goal_source: str | None = None require_approval_for_done: bool | None = None require_review_before_done: bool | None = None + comment_required_for_review: bool | None = None block_status_changes_with_pending_approval: bool | None = None only_lead_can_change_status: bool | None = None max_agents: int | None = Field(default=None, ge=0) diff --git a/backend/app/schemas/errors.py b/backend/app/schemas/errors.py index 7eab0618..14fd529c 100644 --- a/backend/app/schemas/errors.py +++ b/backend/app/schemas/errors.py @@ -55,6 +55,7 @@ class BlockedTaskDetail(SQLModel): """Error detail payload listing blocking dependency task identifiers.""" message: str + code: str | None = None blocked_by_task_ids: list[str] = Field(default_factory=list) diff --git a/backend/app/schemas/gateway_api.py b/backend/app/schemas/gateway_api.py index 2ae97692..f2dd097f 100644 --- a/backend/app/schemas/gateway_api.py +++ b/backend/app/schemas/gateway_api.py @@ -21,6 +21,8 @@ class GatewayResolveQuery(SQLModel): board_id: str | None = None gateway_url: str | None = None gateway_token: str | None = None + gateway_disable_device_pairing: bool | None = None + gateway_allow_insecure_tls: bool | None = None class GatewaysStatusResponse(SQLModel): diff --git a/backend/app/schemas/gateways.py b/backend/app/schemas/gateways.py index 233a44d5..9d306991 100644 --- a/backend/app/schemas/gateways.py +++ b/backend/app/schemas/gateways.py @@ -17,6 +17,8 @@ class GatewayBase(SQLModel): name: str url: str workspace_root: str + allow_insecure_tls: bool = False + disable_device_pairing: bool = False class GatewayCreate(GatewayBase): @@ -43,6 +45,8 @@ class GatewayUpdate(SQLModel): url: str | None = None token: str | None = None workspace_root: str | None = None + allow_insecure_tls: bool | None = None + disable_device_pairing: bool | None = None @field_validator("token", mode="before") @classmethod diff --git a/backend/app/schemas/metrics.py b/backend/app/schemas/metrics.py index 6a21ce48..23c6e139 100644 --- a/backend/app/schemas/metrics.py +++ b/backend/app/schemas/metrics.py @@ -4,10 +4,11 @@ from __future__ import annotations from datetime import datetime from typing import Literal +from uuid import UUID from sqlmodel import SQLModel -RUNTIME_ANNOTATION_TYPES = (datetime,) +RUNTIME_ANNOTATION_TYPES = (datetime, UUID) DashboardRangeKey = Literal["24h", "3d", "7d", "14d", "1m", "3m", "6m", "1y"] DashboardBucketKey = Literal["hour", "day", "week", "month"] @@ -64,10 +65,33 @@ class DashboardKpis(SQLModel): active_agents: int tasks_in_progress: int + inbox_tasks: int + in_progress_tasks: int + review_tasks: int + done_tasks: int error_rate_pct: float median_cycle_time_hours_7d: float | None +class DashboardPendingApproval(SQLModel): + """Single pending approval item for cross-board dashboard listing.""" + + approval_id: UUID + board_id: UUID + board_name: str + action_type: str + confidence: float + created_at: datetime + task_title: str | None = None + + +class DashboardPendingApprovals(SQLModel): + """Pending approval snapshot used on the dashboard.""" + + total: int + items: list[DashboardPendingApproval] + + class DashboardMetrics(SQLModel): """Complete dashboard metrics response payload.""" @@ -78,3 +102,4 @@ class DashboardMetrics(SQLModel): cycle_time: DashboardSeriesSet error_rate: DashboardSeriesSet wip: DashboardWipSeriesSet + pending_approvals: DashboardPendingApprovals diff --git a/backend/app/schemas/task_custom_fields.py b/backend/app/schemas/task_custom_fields.py index ba2e4232..947d7878 100644 --- a/backend/app/schemas/task_custom_fields.py +++ b/backend/app/schemas/task_custom_fields.py @@ -4,6 +4,7 @@ from __future__ import annotations import re from datetime import date, datetime +from functools import lru_cache from typing import Literal, Self from urllib.parse import urlparse from uuid import UUID @@ -297,6 +298,12 @@ def _parse_iso_datetime(value: str) -> datetime: return datetime.fromisoformat(normalized) +@lru_cache(maxsize=256) +def _compiled_validation_regex(pattern: str) -> re.Pattern[str]: + """Compile and cache validation regex patterns for value checks.""" + return re.compile(pattern) + + def validate_custom_field_value( *, field_type: TaskCustomFieldType, @@ -346,7 +353,11 @@ def validate_custom_field_value( if validation_regex is not None and field_type in STRING_FIELD_TYPES: if not isinstance(value, str): raise ValueError("must be a string for regex validation") - if re.fullmatch(validation_regex, value) is None: + try: + pattern = _compiled_validation_regex(validation_regex) + except re.error as exc: + raise ValueError(f"validation_regex is invalid: {exc}") from exc + if pattern.fullmatch(value) is None: raise ValueError("does not match validation_regex") diff --git a/backend/app/services/activity_log.py b/backend/app/services/activity_log.py index 9d7f7d77..04b382ed 100644 --- a/backend/app/services/activity_log.py +++ b/backend/app/services/activity_log.py @@ -19,6 +19,7 @@ def record_activity( message: str, agent_id: UUID | None = None, task_id: UUID | None = None, + board_id: UUID | None = None, ) -> ActivityEvent: """Create and attach an activity event row to the current DB session.""" event = ActivityEvent( @@ -26,6 +27,7 @@ def record_activity( message=message, agent_id=agent_id, task_id=task_id, + board_id=board_id, ) session.add(event) return event diff --git a/backend/app/services/board_lifecycle.py b/backend/app/services/board_lifecycle.py index a7ec2d74..fffbca64 100644 --- a/backend/app/services/board_lifecycle.py +++ b/backend/app/services/board_lifecycle.py @@ -91,6 +91,12 @@ async def delete_board(session: AsyncSession, *, board: Board) -> OkResponse: col(TaskCustomFieldValue.task_id).in_(task_ids), commit=False, ) + await crud.delete_where( + session, + ActivityEvent, + col(ActivityEvent.board_id) == board.id, + commit=False, + ) # Keep teardown ordered around FK/reference chains so dependent rows are gone # before deleting their parent task/agent/board records. await crud.delete_where( diff --git a/backend/app/services/openclaw/admin_service.py b/backend/app/services/openclaw/admin_service.py index dfc0c2b6..5a58e18f 100644 --- a/backend/app/services/openclaw/admin_service.py +++ b/backend/app/services/openclaw/admin_service.py @@ -21,23 +21,18 @@ from app.models.gateways import Gateway from app.models.tasks import Task from app.schemas.gateways import GatewayTemplatesSyncResult from app.services.openclaw.constants import DEFAULT_HEARTBEAT_CONFIG -from app.services.openclaw.db_agent_state import ( - mark_provision_complete, - mark_provision_requested, - mint_agent_token, -) from app.services.openclaw.db_service import OpenClawDBService -from app.services.openclaw.gateway_compat import check_gateway_runtime_compatibility +from app.services.openclaw.error_messages import normalize_gateway_error_message +from app.services.openclaw.gateway_compat import check_gateway_version_compatibility from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig from app.services.openclaw.gateway_rpc import OpenClawGatewayError, openclaw_call -from app.services.openclaw.provisioning import OpenClawGatewayProvisioner +from app.services.openclaw.lifecycle_orchestrator import AgentLifecycleOrchestrator from app.services.openclaw.provisioning_db import ( GatewayTemplateSyncOptions, OpenClawProvisioningService, ) from app.services.openclaw.session_service import GatewayTemplateSyncQuery from app.services.openclaw.shared import GatewayAgentIdentity -from app.services.organizations import get_org_owner_user if TYPE_CHECKING: from sqlmodel.ext.asyncio.session import AsyncSession @@ -167,7 +162,12 @@ class GatewayAdminLifecycleService(OpenClawDBService): async def gateway_has_main_agent_entry(self, gateway: Gateway) -> bool: if not gateway.url: return False - config = GatewayClientConfig(url=gateway.url, token=gateway.token) + config = GatewayClientConfig( + url=gateway.url, + token=gateway.token, + allow_insecure_tls=gateway.allow_insecure_tls, + disable_device_pairing=gateway.disable_device_pairing, + ) target_id = GatewayAgentIdentity.openclaw_agent_id(gateway) try: await openclaw_call("agents.files.list", {"agentId": target_id}, config=config) @@ -178,15 +178,28 @@ class GatewayAdminLifecycleService(OpenClawDBService): return True return True - async def assert_gateway_runtime_compatible(self, *, url: str, token: str | None) -> None: + async def assert_gateway_runtime_compatible( + self, + *, + url: str, + token: str | None, + allow_insecure_tls: bool = False, + disable_device_pairing: bool = False, + ) -> None: """Validate that a gateway runtime meets minimum supported version.""" - config = GatewayClientConfig(url=url, token=token) + config = GatewayClientConfig( + url=url, + token=token, + allow_insecure_tls=allow_insecure_tls, + disable_device_pairing=disable_device_pairing, + ) try: - result = await check_gateway_runtime_compatibility(config) + result = await check_gateway_version_compatibility(config) except OpenClawGatewayError as exc: + detail = normalize_gateway_error_message(str(exc)) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, - detail=f"Gateway compatibility check failed: {exc}", + detail=f"Gateway compatibility check failed: {detail}", ) from exc if not result.compatible: raise HTTPException( @@ -203,69 +216,38 @@ class GatewayAdminLifecycleService(OpenClawDBService): action: str, notify: bool, ) -> Agent: - template_user = user or await get_org_owner_user( - self.session, - organization_id=gateway.organization_id, - ) - if template_user is None: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, - detail="Organization owner not found (required for gateway agent USER.md rendering).", - ) - raw_token = mint_agent_token(agent) - mark_provision_requested( - agent, - action=action, - status="updating" if action == "update" else "provisioning", - ) - await self.add_commit_refresh(agent) - if not gateway.url: - return agent - + orchestrator = AgentLifecycleOrchestrator(self.session) try: - await OpenClawGatewayProvisioner().apply_agent_lifecycle( - agent=agent, + provisioned = await orchestrator.run_lifecycle( gateway=gateway, + agent_id=agent.id, board=None, - auth_token=raw_token, - user=template_user, + user=user, action=action, + auth_token=None, + force_bootstrap=False, + reset_session=False, wake=notify, deliver_wakeup=True, + wakeup_verb=None, + clear_confirm_token=False, + raise_gateway_errors=True, ) - except OpenClawGatewayError as exc: + except HTTPException: self.logger.error( - "gateway.main_agent.provision_failed_gateway gateway_id=%s agent_id=%s error=%s", + "gateway.main_agent.provision_failed gateway_id=%s agent_id=%s action=%s", gateway.id, agent.id, - str(exc), + action, ) - raise HTTPException( - status_code=status.HTTP_502_BAD_GATEWAY, - detail=f"Gateway {action} failed: {exc}", - ) from exc - except (OSError, RuntimeError, ValueError) as exc: - self.logger.error( - "gateway.main_agent.provision_failed gateway_id=%s agent_id=%s error=%s", - gateway.id, - agent.id, - str(exc), - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Unexpected error {action}ing gateway provisioning.", - ) from exc - - mark_provision_complete(agent, status="online") - await self.add_commit_refresh(agent) - + raise self.logger.info( "gateway.main_agent.provision_success gateway_id=%s agent_id=%s action=%s", gateway.id, - agent.id, + provisioned.id, action, ) - return agent + return provisioned async def ensure_main_agent( self, diff --git a/backend/app/services/openclaw/constants.py b/backend/app/services/openclaw/constants.py index e7c3e433..48816537 100644 --- a/backend/app/services/openclaw/constants.py +++ b/backend/app/services/openclaw/constants.py @@ -18,6 +18,11 @@ DEFAULT_HEARTBEAT_CONFIG: dict[str, Any] = { } OFFLINE_AFTER = timedelta(minutes=10) +# Provisioning convergence policy: +# - require first heartbeat/check-in within 30s of wake +# - allow up to 3 wake attempts before giving up +CHECKIN_DEADLINE_AFTER_WAKE = timedelta(seconds=30) +MAX_WAKE_ATTEMPTS_WITHOUT_CHECKIN = 3 AGENT_SESSION_PREFIX = "agent" DEFAULT_CHANNEL_HEARTBEAT_VISIBILITY: dict[str, bool] = { diff --git a/backend/app/services/openclaw/coordination_service.py b/backend/app/services/openclaw/coordination_service.py index 8c9817c5..6384ecd8 100644 --- a/backend/app/services/openclaw/coordination_service.py +++ b/backend/app/services/openclaw/coordination_service.py @@ -93,7 +93,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): reply_tags: list[str] | None, reply_source: str | None, ) -> str: - base_url = settings.base_url or "http://localhost:8000" + base_url = settings.base_url header = "GATEWAY MAIN QUESTION" if kind == "question" else "GATEWAY MAIN HANDOFF" correlation = correlation_id.strip() if correlation_id else "" correlation_line = f"Correlation ID: {correlation}\n" if correlation else "" @@ -204,6 +204,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): event_type="agent.nudge.failed", message=f"Nudge failed for {target.name}: {exc}", agent_id=actor_agent.id, + board_id=board.id, ) await self.session.commit() self.logger.error( @@ -233,6 +234,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): event_type="agent.nudge.sent", message=f"Nudge sent to {target.name}.", agent_id=actor_agent.id, + board_id=board.id, ) await self.session.commit() self.logger.info( @@ -397,6 +399,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): event_type="agent.soul.updated", message=note, agent_id=actor_agent_id, + board_id=board.id, ) await self.session.commit() self.logger.info( @@ -437,7 +440,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): tags = payload.reply_tags or ["gateway_main", "user_reply"] tags_json = json.dumps(tags) reply_source = payload.reply_source or "user_via_gateway_main" - base_url = settings.base_url or "http://localhost:8000" + base_url = settings.base_url message = ( "LEAD REQUEST: ASK USER\n" f"Board: {board.name}\n" @@ -470,6 +473,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): event_type="gateway.lead.ask_user.failed", message=f"Lead user question failed for {board.name}: {exc}", agent_id=actor_agent.id, + board_id=board.id, ) await self.session.commit() self.logger.error( @@ -501,6 +505,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): event_type="gateway.lead.ask_user.sent", message=f"Lead requested user info via gateway agent for board: {board.name}.", agent_id=actor_agent.id, + board_id=board.id, ) main_agent = await Agent.objects.filter_by(gateway_id=gateway.id, board_id=None).first( self.session, @@ -595,6 +600,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): event_type="gateway.main.lead_message.failed", message=f"Lead message failed for {board.name}: {exc}", agent_id=actor_agent.id, + board_id=board.id, ) await self.session.commit() self.logger.error( @@ -626,6 +632,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): event_type="gateway.main.lead_message.sent", message=f"Sent {payload.kind} to lead for board: {board.name}.", agent_id=actor_agent.id, + board_id=board.id, ) await self.session.commit() self.logger.info( diff --git a/backend/app/services/openclaw/device_identity.py b/backend/app/services/openclaw/device_identity.py new file mode 100644 index 00000000..e0652a93 --- /dev/null +++ b/backend/app/services/openclaw/device_identity.py @@ -0,0 +1,167 @@ +"""OpenClaw-compatible device identity and connect-signature helpers.""" + +from __future__ import annotations + +import hashlib +import json +import os +from dataclasses import dataclass +from pathlib import Path +from time import time +from typing import Any, cast + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, +) + +DEFAULT_DEVICE_IDENTITY_PATH = Path.home() / ".openclaw" / "identity" / "device.json" + + +@dataclass(frozen=True) +class DeviceIdentity: + """Persisted gateway device identity used for connect signatures.""" + + device_id: str + public_key_pem: str + private_key_pem: str + + +def _identity_path() -> Path: + raw = os.getenv("OPENCLAW_GATEWAY_DEVICE_IDENTITY_PATH", "").strip() + if raw: + return Path(raw).expanduser().resolve() + return DEFAULT_DEVICE_IDENTITY_PATH + + +def _base64url_encode(raw: bytes) -> str: + import base64 + + return base64.urlsafe_b64encode(raw).decode("utf-8").rstrip("=") + + +def _derive_public_key_raw(public_key_pem: str) -> bytes: + loaded = serialization.load_pem_public_key(public_key_pem.encode("utf-8")) + if not isinstance(loaded, Ed25519PublicKey): + msg = "device identity public key is not Ed25519" + raise ValueError(msg) + return loaded.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + + +def _derive_device_id(public_key_pem: str) -> str: + return hashlib.sha256(_derive_public_key_raw(public_key_pem)).hexdigest() + + +def _write_identity(path: Path, identity: DeviceIdentity) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + payload = { + "version": 1, + "deviceId": identity.device_id, + "publicKeyPem": identity.public_key_pem, + "privateKeyPem": identity.private_key_pem, + "createdAtMs": int(time() * 1000), + } + path.write_text(f"{json.dumps(payload, indent=2)}\n", encoding="utf-8") + try: + path.chmod(0o600) + except OSError: + # Best effort on platforms/filesystems that ignore chmod. + pass + + +def _generate_identity() -> DeviceIdentity: + private_key = Ed25519PrivateKey.generate() + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode("utf-8") + public_key_pem = ( + private_key.public_key() + .public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + .decode("utf-8") + ) + device_id = _derive_device_id(public_key_pem) + return DeviceIdentity( + device_id=device_id, + public_key_pem=public_key_pem, + private_key_pem=private_key_pem, + ) + + +def load_or_create_device_identity() -> DeviceIdentity: + """Load persisted device identity or create a new one when missing/invalid.""" + path = _identity_path() + try: + if path.exists(): + payload = cast(dict[str, Any], json.loads(path.read_text(encoding="utf-8"))) + device_id = str(payload.get("deviceId") or "").strip() + public_key_pem = str(payload.get("publicKeyPem") or "").strip() + private_key_pem = str(payload.get("privateKeyPem") or "").strip() + if device_id and public_key_pem and private_key_pem: + derived_id = _derive_device_id(public_key_pem) + identity = DeviceIdentity( + device_id=derived_id, + public_key_pem=public_key_pem, + private_key_pem=private_key_pem, + ) + if derived_id != device_id: + _write_identity(path, identity) + return identity + except (OSError, ValueError, json.JSONDecodeError): + # Fall through to regenerate. + pass + + identity = _generate_identity() + _write_identity(path, identity) + return identity + + +def public_key_raw_base64url_from_pem(public_key_pem: str) -> str: + """Return raw Ed25519 public key in base64url form expected by OpenClaw.""" + return _base64url_encode(_derive_public_key_raw(public_key_pem)) + + +def sign_device_payload(private_key_pem: str, payload: str) -> str: + """Sign a device payload with Ed25519 and return base64url signature.""" + loaded = serialization.load_pem_private_key(private_key_pem.encode("utf-8"), password=None) + if not isinstance(loaded, Ed25519PrivateKey): + msg = "device identity private key is not Ed25519" + raise ValueError(msg) + signature = loaded.sign(payload.encode("utf-8")) + return _base64url_encode(signature) + + +def build_device_auth_payload( + *, + device_id: str, + client_id: str, + client_mode: str, + role: str, + scopes: list[str], + signed_at_ms: int, + token: str | None, + nonce: str | None, +) -> str: + """Build the OpenClaw canonical payload string for device signatures.""" + version = "v2" if nonce else "v1" + parts = [ + version, + device_id, + client_id, + client_mode, + role, + ",".join(scopes), + str(signed_at_ms), + token or "", + ] + if version == "v2": + parts.append(nonce or "") + return "|".join(parts) diff --git a/backend/app/services/openclaw/error_messages.py b/backend/app/services/openclaw/error_messages.py new file mode 100644 index 00000000..c604eedd --- /dev/null +++ b/backend/app/services/openclaw/error_messages.py @@ -0,0 +1,31 @@ +"""Normalization helpers for user-facing OpenClaw gateway errors.""" + +from __future__ import annotations + +import re + +_MISSING_SCOPE_PATTERN = re.compile( + r"missing\s+scope\s*:\s*(?P[A-Za-z0-9._:-]+)", + re.IGNORECASE, +) + + +def normalize_gateway_error_message(message: str) -> str: + """Return a user-friendly message for common gateway auth failures.""" + raw_message = message.strip() + if not raw_message: + return "Gateway authentication failed. Verify gateway token and operator scopes." + + missing_scope = _MISSING_SCOPE_PATTERN.search(raw_message) + if missing_scope is not None: + scope = missing_scope.group("scope") + return ( + f"Gateway token is missing required scope `{scope}`. " + "Update the gateway token scopes and retry." + ) + + lowered = raw_message.lower() + if "unauthorized" in lowered or "forbidden" in lowered: + return "Gateway authentication failed. Verify gateway token and operator scopes." + + return raw_message diff --git a/backend/app/services/openclaw/gateway_compat.py b/backend/app/services/openclaw/gateway_compat.py index ace829c4..9bccb17e 100644 --- a/backend/app/services/openclaw/gateway_compat.py +++ b/backend/app/services/openclaw/gateway_compat.py @@ -4,26 +4,24 @@ from __future__ import annotations import re from dataclasses import dataclass -from typing import Any from app.core.config import settings -from app.services.openclaw.gateway_rpc import GatewayConfig, OpenClawGatewayError, openclaw_call - -_VERSION_PATTERN = re.compile(r"(?i)v?(?P\d+(?:\.\d+)+)") -_PRIMARY_VERSION_PATHS: tuple[tuple[str, ...], ...] = ( - ("version",), - ("gatewayVersion",), - ("appVersion",), - ("buildVersion",), - ("gateway", "version"), - ("app", "version"), - ("server", "version"), - ("runtime", "version"), - ("meta", "version"), - ("build", "version"), - ("info", "version"), +from app.core.logging import get_logger +from app.services.openclaw.gateway_rpc import ( + GatewayConfig, + OpenClawGatewayError, + openclaw_call, + openclaw_connect_metadata, ) +_CALVER_PATTERN = re.compile( + r"^v?(?P\d{4})\.(?P\d{1,2})\.(?P\d{1,2})(?:-(?P\d+))?$", + re.IGNORECASE, +) +_CONNECT_VERSION_PATH: tuple[str, ...] = ("server", "version") +_CONFIG_VERSION_PATH: tuple[str, ...] = ("config", "meta", "lastTouchedVersion") +logger = get_logger(__name__) + @dataclass(frozen=True, slots=True) class GatewayVersionCheckResult: @@ -41,11 +39,18 @@ def _normalized_minimum_version() -> str: def _parse_version_parts(value: str) -> tuple[int, ...] | None: - match = _VERSION_PATTERN.search(value.strip()) + match = _CALVER_PATTERN.match(value.strip()) if match is None: return None - numeric = match.group("version") - return tuple(int(part) for part in numeric.split(".")) + year = int(match.group("year")) + month = int(match.group("month")) + day = int(match.group("day")) + revision = int(match.group("rev") or 0) + if month < 1 or month > 12: + return None + if day < 1 or day > 31: + return None + return (year, month, day, revision) def _compare_versions(left: tuple[int, ...], right: tuple[int, ...]) -> int: @@ -79,36 +84,14 @@ def _coerce_version_string(value: object) -> str | None: return None -def _iter_fallback_version_values(payload: object) -> list[str]: - if not isinstance(payload, dict): - return [] - stack: list[dict[str, Any]] = [payload] - discovered: list[str] = [] - while stack: - node = stack.pop() - for key, value in node.items(): - if isinstance(value, dict): - stack.append(value) - key_lower = key.lower() - if "version" not in key_lower or "protocol" in key_lower: - continue - candidate = _coerce_version_string(value) - if candidate is not None: - discovered.append(candidate) - return discovered +def extract_connect_server_version(payload: object) -> str | None: + """Extract the canonical runtime version from connect metadata.""" + return _coerce_version_string(_value_at_path(payload, _CONNECT_VERSION_PATH)) -def extract_gateway_version(payload: object) -> str | None: - """Extract a gateway runtime version string from status/health payloads.""" - for path in _PRIMARY_VERSION_PATHS: - candidate = _coerce_version_string(_value_at_path(payload, path)) - if candidate is not None: - return candidate - - for candidate in _iter_fallback_version_values(payload): - if _parse_version_parts(candidate) is not None: - return candidate - return None +def extract_config_last_touched_version(payload: object) -> str | None: + """Extract a runtime version hint from config.get payload.""" + return _coerce_version_string(_value_at_path(payload, _CONFIG_VERSION_PATH)) def evaluate_gateway_version( @@ -122,7 +105,7 @@ def evaluate_gateway_version( if min_parts is None: msg = ( "Server configuration error: GATEWAY_MIN_VERSION is invalid. " - f"Expected a dotted numeric version, got '{min_version}'." + f"Expected CalVer 'YYYY.M.D' or 'YYYY.M.D-REV', got '{min_version}'." ) return GatewayVersionCheckResult( compatible=False, @@ -172,49 +155,26 @@ def evaluate_gateway_version( ) -async def _fetch_runtime_metadata(config: GatewayConfig) -> object: - last_error: OpenClawGatewayError | None = None - for method in ("status", "health"): - try: - return await openclaw_call(method, config=config) - except OpenClawGatewayError as exc: - last_error = exc - continue - if last_error is not None: - raise last_error - return {} - - -async def _fetch_schema_metadata(config: GatewayConfig) -> object | None: - try: - return await openclaw_call("config.schema", config=config) - except OpenClawGatewayError: - return None - - -async def check_gateway_runtime_compatibility( +async def check_gateway_version_compatibility( config: GatewayConfig, *, minimum_version: str | None = None, ) -> GatewayVersionCheckResult: - """Fetch runtime metadata and evaluate gateway version compatibility.""" - schema_payload = await _fetch_schema_metadata(config) - current_version = extract_gateway_version(schema_payload) - if current_version is not None: - return evaluate_gateway_version( - current_version=current_version, - minimum_version=minimum_version, - ) - - payload = await _fetch_runtime_metadata(config) - current_version = extract_gateway_version(payload) - if current_version is None: + """Evaluate gateway compatibility using connect metadata with config fallback.""" + connect_payload = await openclaw_connect_metadata(config=config) + current_version = extract_connect_server_version(connect_payload) + if current_version is None or _parse_version_parts(current_version) is None: try: - health_payload = await openclaw_call("health", config=config) - except OpenClawGatewayError: - health_payload = None - if health_payload is not None: - current_version = extract_gateway_version(health_payload) + config_payload = await openclaw_call("config.get", config=config) + except OpenClawGatewayError as exc: + logger.debug( + "gateway.compat.config_get_fallback_unavailable reason=%s", + str(exc), + ) + else: + fallback_version = extract_config_last_touched_version(config_payload) + if fallback_version is not None: + current_version = fallback_version return evaluate_gateway_version( current_version=current_version, minimum_version=minimum_version, diff --git a/backend/app/services/openclaw/gateway_resolver.py b/backend/app/services/openclaw/gateway_resolver.py index 7e31814f..f883f262 100644 --- a/backend/app/services/openclaw/gateway_resolver.py +++ b/backend/app/services/openclaw/gateway_resolver.py @@ -32,7 +32,12 @@ def gateway_client_config(gateway: Gateway) -> GatewayClientConfig: detail="Gateway url is required", ) token = (gateway.token or "").strip() or None - return GatewayClientConfig(url=url, token=token) + return GatewayClientConfig( + url=url, + token=token, + allow_insecure_tls=gateway.allow_insecure_tls, + disable_device_pairing=gateway.disable_device_pairing, + ) def optional_gateway_client_config(gateway: Gateway | None) -> GatewayClientConfig | None: @@ -43,7 +48,12 @@ def optional_gateway_client_config(gateway: Gateway | None) -> GatewayClientConf if not url: return None token = (gateway.token or "").strip() or None - return GatewayClientConfig(url=url, token=token) + return GatewayClientConfig( + url=url, + token=token, + allow_insecure_tls=gateway.allow_insecure_tls, + disable_device_pairing=gateway.disable_device_pairing, + ) def require_gateway_workspace_root(gateway: Gateway) -> str: diff --git a/backend/app/services/openclaw/gateway_rpc.py b/backend/app/services/openclaw/gateway_rpc.py index 4807282f..85a67124 100644 --- a/backend/app/services/openclaw/gateway_rpc.py +++ b/backend/app/services/openclaw/gateway_rpc.py @@ -9,9 +9,10 @@ from __future__ import annotations import asyncio import json +import ssl from dataclasses import dataclass -from time import perf_counter -from typing import Any +from time import perf_counter, time +from typing import Any, Literal from urllib.parse import urlencode, urlparse, urlunparse from uuid import uuid4 @@ -19,14 +20,26 @@ import websockets from websockets.exceptions import WebSocketException from app.core.logging import TRACE_LEVEL, get_logger +from app.services.openclaw.device_identity import ( + build_device_auth_payload, + load_or_create_device_identity, + public_key_raw_base64url_from_pem, + sign_device_payload, +) PROTOCOL_VERSION = 3 logger = get_logger(__name__) GATEWAY_OPERATOR_SCOPES = ( + "operator.read", "operator.admin", "operator.approvals", "operator.pairing", ) +DEFAULT_GATEWAY_CLIENT_ID = "gateway-client" +DEFAULT_GATEWAY_CLIENT_MODE = "backend" +CONTROL_UI_CLIENT_ID = "openclaw-control-ui" +CONTROL_UI_CLIENT_MODE = "ui" +GatewayConnectMode = Literal["device", "control_ui"] # NOTE: These are the base gateway methods from the OpenClaw gateway repo. # The gateway can expose additional methods at runtime via channel plugins. @@ -159,6 +172,8 @@ class GatewayConfig: url: str token: str | None = None + allow_insecure_tls: bool = False + disable_device_pairing: bool = False def _build_gateway_url(config: GatewayConfig) -> str: @@ -179,6 +194,78 @@ def _redacted_url_for_log(raw_url: str) -> str: return str(urlunparse(parsed._replace(query="", fragment=""))) +def _create_ssl_context(config: GatewayConfig) -> ssl.SSLContext | None: + """Create an insecure SSL context override for explicit opt-in TLS bypass. + + This behavior is intentionally host-agnostic: when ``allow_insecure_tls`` is + enabled for a ``wss://`` gateway, certificate and hostname verification are + disabled for that gateway connection. + """ + parsed = urlparse(config.url) + if parsed.scheme != "wss": + return None + if not config.allow_insecure_tls: + return None + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + return ssl_context + + +def _build_control_ui_origin(gateway_url: str) -> str | None: + parsed = urlparse(gateway_url) + if not parsed.hostname: + return None + if parsed.scheme in {"ws", "http"}: + origin_scheme = "http" + elif parsed.scheme in {"wss", "https"}: + origin_scheme = "https" + else: + return None + host = parsed.hostname + if ":" in host and not host.startswith("["): + host = f"[{host}]" + if parsed.port is not None: + host = f"{host}:{parsed.port}" + return f"{origin_scheme}://{host}" + + +def _resolve_connect_mode(config: GatewayConfig) -> GatewayConnectMode: + return "control_ui" if config.disable_device_pairing else "device" + + +def _build_device_connect_payload( + *, + client_id: str, + client_mode: str, + role: str, + scopes: list[str], + auth_token: str | None, + connect_nonce: str | None, +) -> dict[str, Any]: + identity = load_or_create_device_identity() + signed_at_ms = int(time() * 1000) + payload = build_device_auth_payload( + device_id=identity.device_id, + client_id=client_id, + client_mode=client_mode, + role=role, + scopes=scopes, + signed_at_ms=signed_at_ms, + token=auth_token, + nonce=connect_nonce, + ) + device_payload: dict[str, Any] = { + "id": identity.device_id, + "publicKey": public_key_raw_base64url_from_pem(identity.public_key_pem), + "signature": sign_device_payload(identity.private_key_pem, payload), + "signedAt": signed_at_ms, + } + if connect_nonce: + device_payload["nonce"] = connect_nonce + return device_payload + + async def _await_response( ws: websockets.ClientConnection, request_id: str, @@ -230,19 +317,36 @@ async def _send_request( return await _await_response(ws, request_id) -def _build_connect_params(config: GatewayConfig) -> dict[str, Any]: +def _build_connect_params( + config: GatewayConfig, + *, + connect_nonce: str | None = None, +) -> dict[str, Any]: + role = "operator" + scopes = list(GATEWAY_OPERATOR_SCOPES) + connect_mode = _resolve_connect_mode(config) + use_control_ui = connect_mode == "control_ui" params: dict[str, Any] = { "minProtocol": PROTOCOL_VERSION, "maxProtocol": PROTOCOL_VERSION, - "role": "operator", - "scopes": list(GATEWAY_OPERATOR_SCOPES), + "role": role, + "scopes": scopes, "client": { - "id": "gateway-client", + "id": CONTROL_UI_CLIENT_ID if use_control_ui else DEFAULT_GATEWAY_CLIENT_ID, "version": "1.0.0", - "platform": "web", - "mode": "ui", + "platform": "python", + "mode": CONTROL_UI_CLIENT_MODE if use_control_ui else DEFAULT_GATEWAY_CLIENT_MODE, }, } + if not use_control_ui: + params["device"] = _build_device_connect_payload( + client_id=DEFAULT_GATEWAY_CLIENT_ID, + client_mode=DEFAULT_GATEWAY_CLIENT_MODE, + role=role, + scopes=scopes, + auth_token=config.token, + connect_nonce=connect_nonce, + ) if config.token: params["auth"] = {"token": config.token} return params @@ -252,12 +356,19 @@ async def _ensure_connected( ws: websockets.ClientConnection, first_message: str | bytes | None, config: GatewayConfig, -) -> None: +) -> object: + connect_nonce: str | None = None if first_message: if isinstance(first_message, bytes): first_message = first_message.decode("utf-8") data = json.loads(first_message) - if data.get("type") != "event" or data.get("event") != "connect.challenge": + if data.get("type") == "event" and data.get("event") == "connect.challenge": + payload = data.get("payload") + if isinstance(payload, dict): + nonce = payload.get("nonce") + if isinstance(nonce, str) and nonce.strip(): + connect_nonce = nonce.strip() + else: logger.warning( "gateway.rpc.connect.unexpected_first_message type=%s event=%s", data.get("type"), @@ -268,10 +379,56 @@ async def _ensure_connected( "type": "req", "id": connect_id, "method": "connect", - "params": _build_connect_params(config), + "params": _build_connect_params(config, connect_nonce=connect_nonce), } await ws.send(json.dumps(response)) - await _await_response(ws, connect_id) + return await _await_response(ws, connect_id) + + +async def _recv_first_message_or_none( + ws: websockets.ClientConnection, +) -> str | bytes | None: + try: + return await asyncio.wait_for(ws.recv(), timeout=2) + except TimeoutError: + return None + + +async def _openclaw_call_once( + method: str, + params: dict[str, Any] | None, + *, + config: GatewayConfig, + gateway_url: str, +) -> object: + origin = _build_control_ui_origin(gateway_url) if config.disable_device_pairing else None + ssl_context = _create_ssl_context(config) + connect_kwargs: dict[str, Any] = {"ping_interval": None} + if origin is not None: + connect_kwargs["origin"] = origin + if ssl_context is not None: + connect_kwargs["ssl"] = ssl_context + async with websockets.connect(gateway_url, **connect_kwargs) as ws: + first_message = await _recv_first_message_or_none(ws) + await _ensure_connected(ws, first_message, config) + return await _send_request(ws, method, params) + + +async def _openclaw_connect_metadata_once( + *, + config: GatewayConfig, + gateway_url: str, +) -> object: + origin = _build_control_ui_origin(gateway_url) if config.disable_device_pairing else None + ssl_context = _create_ssl_context(config) + connect_kwargs: dict[str, Any] = {"ping_interval": None} + if origin is not None: + connect_kwargs["origin"] = origin + if ssl_context is not None: + connect_kwargs["ssl"] = ssl_context + async with websockets.connect(gateway_url, **connect_kwargs) as ws: + first_message = await _recv_first_message_or_none(ws) + return await _ensure_connected(ws, first_message, config) async def openclaw_call( @@ -284,25 +441,28 @@ async def openclaw_call( gateway_url = _build_gateway_url(config) started_at = perf_counter() logger.debug( - "gateway.rpc.call.start method=%s gateway_url=%s", + ( + "gateway.rpc.call.start method=%s gateway_url=%s allow_insecure_tls=%s " + "disable_device_pairing=%s" + ), method, _redacted_url_for_log(gateway_url), + config.allow_insecure_tls, + config.disable_device_pairing, ) try: - async with websockets.connect(gateway_url, ping_interval=None) as ws: - first_message = None - try: - first_message = await asyncio.wait_for(ws.recv(), timeout=2) - except TimeoutError: - first_message = None - await _ensure_connected(ws, first_message, config) - payload = await _send_request(ws, method, params) - logger.debug( - "gateway.rpc.call.success method=%s duration_ms=%s", - method, - int((perf_counter() - started_at) * 1000), - ) - return payload + payload = await _openclaw_call_once( + method, + params, + config=config, + gateway_url=gateway_url, + ) + logger.debug( + "gateway.rpc.call.success method=%s duration_ms=%s", + method, + int((perf_counter() - started_at) * 1000), + ) + return payload except OpenClawGatewayError: logger.warning( "gateway.rpc.call.gateway_error method=%s duration_ms=%s", @@ -326,6 +486,45 @@ async def openclaw_call( raise OpenClawGatewayError(str(exc)) from exc +async def openclaw_connect_metadata(*, config: GatewayConfig) -> object: + """Open a gateway connection and return the connect/hello payload.""" + gateway_url = _build_gateway_url(config) + started_at = perf_counter() + logger.debug( + "gateway.rpc.connect_metadata.start gateway_url=%s", + _redacted_url_for_log(gateway_url), + ) + try: + metadata = await _openclaw_connect_metadata_once( + config=config, + gateway_url=gateway_url, + ) + logger.debug( + "gateway.rpc.connect_metadata.success duration_ms=%s", + int((perf_counter() - started_at) * 1000), + ) + return metadata + except OpenClawGatewayError: + logger.warning( + "gateway.rpc.connect_metadata.gateway_error duration_ms=%s", + int((perf_counter() - started_at) * 1000), + ) + raise + except ( + TimeoutError, + ConnectionError, + OSError, + ValueError, + WebSocketException, + ) as exc: # pragma: no cover - network/protocol errors + logger.error( + "gateway.rpc.connect_metadata.transport_error duration_ms=%s error_type=%s", + int((perf_counter() - started_at) * 1000), + exc.__class__.__name__, + ) + raise OpenClawGatewayError(str(exc)) from exc + + async def send_message( message: str, *, diff --git a/backend/app/services/openclaw/lifecycle_orchestrator.py b/backend/app/services/openclaw/lifecycle_orchestrator.py new file mode 100644 index 00000000..64bc9466 --- /dev/null +++ b/backend/app/services/openclaw/lifecycle_orchestrator.py @@ -0,0 +1,167 @@ +"""Unified agent lifecycle orchestration. + +This module centralizes DB-backed lifecycle transitions so call sites do not +duplicate provisioning/wake/state logic. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from uuid import UUID + +from fastapi import HTTPException, status +from sqlmodel import col, select + +from app.core.time import utcnow +from app.models.agents import Agent +from app.models.boards import Board +from app.models.gateways import Gateway +from app.services.openclaw.constants import CHECKIN_DEADLINE_AFTER_WAKE +from app.services.openclaw.db_agent_state import ( + mark_provision_complete, + mark_provision_requested, + mint_agent_token, +) +from app.services.openclaw.db_service import OpenClawDBService +from app.services.openclaw.gateway_rpc import OpenClawGatewayError +from app.services.openclaw.lifecycle_queue import ( + QueuedAgentLifecycleReconcile, + enqueue_lifecycle_reconcile, +) +from app.services.openclaw.provisioning import OpenClawGatewayProvisioner +from app.services.organizations import get_org_owner_user + +if TYPE_CHECKING: + from sqlmodel.ext.asyncio.session import AsyncSession + + from app.models.users import User + + +class AgentLifecycleOrchestrator(OpenClawDBService): + """Single lifecycle writer for agent provision/update transitions.""" + + def __init__(self, session: AsyncSession) -> None: + super().__init__(session) + + async def _lock_agent(self, *, agent_id: UUID) -> Agent: + statement = select(Agent).where(col(Agent.id) == agent_id).with_for_update() + agent = (await self.session.exec(statement)).first() + if agent is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found") + return agent + + async def run_lifecycle( + self, + *, + gateway: Gateway, + agent_id: UUID, + board: Board | None, + user: User | None, + action: str, + auth_token: str | None = None, + force_bootstrap: bool = False, + reset_session: bool = False, + wake: bool = True, + deliver_wakeup: bool = True, + wakeup_verb: str | None = None, + clear_confirm_token: bool = False, + raise_gateway_errors: bool = True, + ) -> Agent: + """Provision or update any agent under a per-agent lock.""" + + locked = await self._lock_agent(agent_id=agent_id) + template_user = user + if board is None and template_user is None: + template_user = await get_org_owner_user( + self.session, + organization_id=gateway.organization_id, + ) + if template_user is None: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, + detail=( + "Organization owner not found " + "(required for gateway agent USER.md rendering)." + ), + ) + + raw_token = auth_token or mint_agent_token(locked) + mark_provision_requested( + locked, + action=action, + status="updating" if action == "update" else "provisioning", + ) + locked.lifecycle_generation += 1 + locked.last_provision_error = None + locked.checkin_deadline_at = utcnow() + CHECKIN_DEADLINE_AFTER_WAKE if wake else None + if wake: + locked.wake_attempts += 1 + locked.last_wake_sent_at = utcnow() + self.session.add(locked) + await self.session.flush() + + if not gateway.url: + await self.session.commit() + await self.session.refresh(locked) + return locked + + try: + await OpenClawGatewayProvisioner().apply_agent_lifecycle( + agent=locked, + gateway=gateway, + board=board, + auth_token=raw_token, + user=template_user, + action=action, + force_bootstrap=force_bootstrap, + reset_session=reset_session, + wake=wake, + deliver_wakeup=deliver_wakeup, + wakeup_verb=wakeup_verb, + ) + except OpenClawGatewayError as exc: + locked.last_provision_error = str(exc) + locked.updated_at = utcnow() + self.session.add(locked) + await self.session.commit() + await self.session.refresh(locked) + if raise_gateway_errors: + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail=f"Gateway {action} failed: {exc}", + ) from exc + return locked + except (OSError, RuntimeError, ValueError) as exc: + locked.last_provision_error = str(exc) + locked.updated_at = utcnow() + self.session.add(locked) + await self.session.commit() + await self.session.refresh(locked) + if raise_gateway_errors: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error {action}ing gateway provisioning.", + ) from exc + return locked + + mark_provision_complete( + locked, + status="online", + clear_confirm_token=clear_confirm_token, + ) + locked.last_provision_error = None + locked.checkin_deadline_at = utcnow() + CHECKIN_DEADLINE_AFTER_WAKE if wake else None + self.session.add(locked) + await self.session.commit() + await self.session.refresh(locked) + if wake and locked.checkin_deadline_at is not None: + enqueue_lifecycle_reconcile( + QueuedAgentLifecycleReconcile( + agent_id=locked.id, + gateway_id=locked.gateway_id, + board_id=locked.board_id, + generation=locked.lifecycle_generation, + checkin_deadline_at=locked.checkin_deadline_at, + ) + ) + return locked diff --git a/backend/app/services/openclaw/lifecycle_queue.py b/backend/app/services/openclaw/lifecycle_queue.py new file mode 100644 index 00000000..3a7f2b54 --- /dev/null +++ b/backend/app/services/openclaw/lifecycle_queue.py @@ -0,0 +1,122 @@ +"""Queue payload helpers for stuck-agent lifecycle reconciliation.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any +from uuid import UUID + +from app.core.config import settings +from app.core.logging import get_logger +from app.core.time import utcnow +from app.services.queue import QueuedTask, enqueue_task_with_delay +from app.services.queue import requeue_if_failed as generic_requeue_if_failed + +logger = get_logger(__name__) +TASK_TYPE = "agent_lifecycle_reconcile" + + +@dataclass(frozen=True) +class QueuedAgentLifecycleReconcile: + """Queued payload metadata for lifecycle reconciliation checks.""" + + agent_id: UUID + gateway_id: UUID + board_id: UUID | None + generation: int + checkin_deadline_at: datetime + attempts: int = 0 + + +def _task_from_payload(payload: QueuedAgentLifecycleReconcile) -> QueuedTask: + return QueuedTask( + task_type=TASK_TYPE, + payload={ + "agent_id": str(payload.agent_id), + "gateway_id": str(payload.gateway_id), + "board_id": str(payload.board_id) if payload.board_id is not None else None, + "generation": payload.generation, + "checkin_deadline_at": payload.checkin_deadline_at.isoformat(), + }, + created_at=utcnow(), + attempts=payload.attempts, + ) + + +def decode_lifecycle_task(task: QueuedTask) -> QueuedAgentLifecycleReconcile: + if task.task_type not in {TASK_TYPE, "legacy"}: + raise ValueError(f"Unexpected task_type={task.task_type!r}; expected {TASK_TYPE!r}") + payload: dict[str, Any] = task.payload + raw_board_id = payload.get("board_id") + board_id = UUID(raw_board_id) if isinstance(raw_board_id, str) and raw_board_id else None + raw_deadline = payload.get("checkin_deadline_at") + if not isinstance(raw_deadline, str): + raise ValueError("checkin_deadline_at is required") + return QueuedAgentLifecycleReconcile( + agent_id=UUID(str(payload["agent_id"])), + gateway_id=UUID(str(payload["gateway_id"])), + board_id=board_id, + generation=int(payload["generation"]), + checkin_deadline_at=datetime.fromisoformat(raw_deadline), + attempts=int(payload.get("attempts", task.attempts)), + ) + + +def enqueue_lifecycle_reconcile(payload: QueuedAgentLifecycleReconcile) -> bool: + """Enqueue a delayed reconcile check keyed to the expected check-in deadline.""" + now = utcnow() + delay_seconds = max(0.0, (payload.checkin_deadline_at - now).total_seconds()) + queued = _task_from_payload(payload) + ok = enqueue_task_with_delay( + queued, + settings.rq_queue_name, + delay_seconds=delay_seconds, + redis_url=settings.rq_redis_url, + ) + if ok: + logger.info( + "lifecycle.queue.enqueued", + extra={ + "agent_id": str(payload.agent_id), + "generation": payload.generation, + "delay_seconds": delay_seconds, + "attempt": payload.attempts, + }, + ) + return ok + + +def defer_lifecycle_reconcile( + task: QueuedTask, + *, + delay_seconds: float, +) -> bool: + """Defer a reconcile task without incrementing retry attempts.""" + payload = decode_lifecycle_task(task) + deferred = QueuedAgentLifecycleReconcile( + agent_id=payload.agent_id, + gateway_id=payload.gateway_id, + board_id=payload.board_id, + generation=payload.generation, + checkin_deadline_at=payload.checkin_deadline_at, + attempts=task.attempts, + ) + queued = _task_from_payload(deferred) + return enqueue_task_with_delay( + queued, + settings.rq_queue_name, + delay_seconds=max(0.0, delay_seconds), + redis_url=settings.rq_redis_url, + ) + + +def requeue_lifecycle_queue_task(task: QueuedTask, *, delay_seconds: float = 0) -> bool: + """Requeue a failed lifecycle task with capped retries.""" + return generic_requeue_if_failed( + task, + settings.rq_queue_name, + max_retries=settings.rq_dispatch_max_retries, + redis_url=settings.rq_redis_url, + delay_seconds=max(0.0, delay_seconds), + ) diff --git a/backend/app/services/openclaw/lifecycle_reconcile.py b/backend/app/services/openclaw/lifecycle_reconcile.py new file mode 100644 index 00000000..6d1aebeb --- /dev/null +++ b/backend/app/services/openclaw/lifecycle_reconcile.py @@ -0,0 +1,140 @@ +"""Worker handlers for lifecycle reconciliation tasks.""" + +from __future__ import annotations + +import asyncio + +from app.core.logging import get_logger +from app.core.time import utcnow +from app.db.session import async_session_maker +from app.models.agents import Agent +from app.models.boards import Board +from app.models.gateways import Gateway +from app.services.openclaw.constants import MAX_WAKE_ATTEMPTS_WITHOUT_CHECKIN +from app.services.openclaw.lifecycle_orchestrator import AgentLifecycleOrchestrator +from app.services.openclaw.lifecycle_queue import decode_lifecycle_task, defer_lifecycle_reconcile +from app.services.queue import QueuedTask + +logger = get_logger(__name__) +_RECONCILE_TIMEOUT_SECONDS = 60.0 + + +def _has_checked_in_since_wake(agent: Agent) -> bool: + if agent.last_seen_at is None: + return False + if agent.last_wake_sent_at is None: + return True + return agent.last_seen_at >= agent.last_wake_sent_at + + +async def process_lifecycle_queue_task(task: QueuedTask) -> None: + """Re-run lifecycle provisioning when an agent misses post-provision check-in.""" + payload = decode_lifecycle_task(task) + now = utcnow() + + async with async_session_maker() as session: + agent = await Agent.objects.by_id(payload.agent_id).first(session) + if agent is None: + logger.info( + "lifecycle.reconcile.skip_missing_agent", + extra={"agent_id": str(payload.agent_id)}, + ) + return + + # Ignore stale queue messages after a newer lifecycle generation. + if agent.lifecycle_generation != payload.generation: + logger.info( + "lifecycle.reconcile.skip_stale_generation", + extra={ + "agent_id": str(agent.id), + "queued_generation": payload.generation, + "current_generation": agent.lifecycle_generation, + }, + ) + return + + if _has_checked_in_since_wake(agent): + logger.info( + "lifecycle.reconcile.skip_not_stuck", + extra={"agent_id": str(agent.id), "status": agent.status}, + ) + return + + deadline = agent.checkin_deadline_at or payload.checkin_deadline_at + if agent.status == "deleting": + logger.info( + "lifecycle.reconcile.skip_deleting", + extra={"agent_id": str(agent.id)}, + ) + return + + if now < deadline: + delay = max(0.0, (deadline - now).total_seconds()) + if not defer_lifecycle_reconcile(task, delay_seconds=delay): + msg = "Failed to defer lifecycle reconcile task" + raise RuntimeError(msg) + logger.info( + "lifecycle.reconcile.deferred", + extra={"agent_id": str(agent.id), "delay_seconds": delay}, + ) + return + + if agent.wake_attempts >= MAX_WAKE_ATTEMPTS_WITHOUT_CHECKIN: + agent.status = "offline" + agent.checkin_deadline_at = None + agent.last_provision_error = ( + "Agent did not check in after wake; max wake attempts reached" + ) + agent.updated_at = utcnow() + session.add(agent) + await session.commit() + logger.warning( + "lifecycle.reconcile.max_attempts_reached", + extra={ + "agent_id": str(agent.id), + "wake_attempts": agent.wake_attempts, + "max_attempts": MAX_WAKE_ATTEMPTS_WITHOUT_CHECKIN, + }, + ) + return + + gateway = await Gateway.objects.by_id(agent.gateway_id).first(session) + if gateway is None: + logger.warning( + "lifecycle.reconcile.skip_missing_gateway", + extra={"agent_id": str(agent.id), "gateway_id": str(agent.gateway_id)}, + ) + return + board: Board | None = None + if agent.board_id is not None: + board = await Board.objects.by_id(agent.board_id).first(session) + if board is None: + logger.warning( + "lifecycle.reconcile.skip_missing_board", + extra={"agent_id": str(agent.id), "board_id": str(agent.board_id)}, + ) + return + + orchestrator = AgentLifecycleOrchestrator(session) + await asyncio.wait_for( + orchestrator.run_lifecycle( + gateway=gateway, + agent_id=agent.id, + board=board, + user=None, + action="update", + auth_token=None, + force_bootstrap=False, + reset_session=True, + wake=True, + deliver_wakeup=True, + wakeup_verb="updated", + clear_confirm_token=True, + raise_gateway_errors=True, + ), + timeout=_RECONCILE_TIMEOUT_SECONDS, + ) + logger.info( + "lifecycle.reconcile.retriggered", + extra={"agent_id": str(agent.id), "generation": payload.generation}, + ) diff --git a/backend/app/services/openclaw/provisioning.py b/backend/app/services/openclaw/provisioning.py index 0d2534a3..f49fecf6 100644 --- a/backend/app/services/openclaw/provisioning.py +++ b/backend/app/services/openclaw/provisioning.py @@ -7,6 +7,7 @@ DB-backed workflows (template sync, lead-agent record creation) live in from __future__ import annotations +import asyncio import json import re from abc import ABC, abstractmethod @@ -17,6 +18,7 @@ from typing import TYPE_CHECKING, Any from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape from app.core.config import settings +from app.core.logging import get_logger from app.models.agents import Agent from app.models.boards import Board from app.models.gateways import Gateway @@ -54,6 +56,8 @@ from app.services.openclaw.shared import GatewayAgentIdentity if TYPE_CHECKING: from app.models.users import User +logger = get_logger(__name__) + @dataclass(frozen=True, slots=True) class ProvisionOptions: @@ -109,28 +113,62 @@ def _heartbeat_config(agent: Agent) -> dict[str, Any]: return merged +def _tools_exec_host_patch(config_data: dict[str, Any]) -> dict[str, Any] | None: + """Ensure ``tools.exec.host`` is set to ``"gateway"`` so agents can run commands. + + Without this, heartbeat-driven agents cannot execute ``curl``, ``bash``, or + any other shell command — making HEARTBEAT.md instructions unexecutable. + Returns a partial ``tools`` dict to merge into ``config.patch``, or ``None`` + if the setting is already present. + """ + tools = config_data.get("tools") + if not isinstance(tools, dict): + return {"exec": {"host": "gateway"}} + exec_cfg = tools.get("exec") + if not isinstance(exec_cfg, dict): + return {"exec": {"host": "gateway"}} + if exec_cfg.get("host"): + return None # Already configured — don't override user choice. + return {"exec": {"host": "gateway"}} + + def _channel_heartbeat_visibility_patch(config_data: dict[str, Any]) -> dict[str, Any] | None: + """Build a minimal patch ensuring channel default heartbeat visibility is configured. + + Gateways may have existing channel config; we only want to fill missing keys rather than + overwrite operator intent. Returns `None` if no change is needed, otherwise returns a shallow + patch dict suitable for a config merge.""" channels = config_data.get("channels") if not isinstance(channels, dict): return {"defaults": {"heartbeat": DEFAULT_CHANNEL_HEARTBEAT_VISIBILITY.copy()}} + defaults = channels.get("defaults") if not isinstance(defaults, dict): return {"defaults": {"heartbeat": DEFAULT_CHANNEL_HEARTBEAT_VISIBILITY.copy()}} + heartbeat = defaults.get("heartbeat") if not isinstance(heartbeat, dict): return {"defaults": {"heartbeat": DEFAULT_CHANNEL_HEARTBEAT_VISIBILITY.copy()}} + merged = dict(heartbeat) changed = False for key, value in DEFAULT_CHANNEL_HEARTBEAT_VISIBILITY.items(): if key not in merged: merged[key] = value changed = True + if not changed: return None + return {"defaults": {"heartbeat": merged}} def _template_env() -> Environment: + """Create the Jinja environment used for gateway template rendering. + + Note: we intentionally disable auto-escaping so markdown/plaintext templates render verbatim. + """ + return Environment( loader=FileSystemLoader(_templates_root()), # Render markdown verbatim (HTML escaping makes it harder for agents to read). @@ -145,19 +183,34 @@ def _heartbeat_template_name(agent: Agent) -> str: def _workspace_path(agent: Agent, workspace_root: str) -> str: + """Return the absolute on-disk workspace directory for an agent. + + Why this exists: + - We derive the folder name from a stable *agent key* (ultimately rooted in ids/session keys) + rather than display names to avoid collisions. + - We preserve a historical gateway-main naming quirk to avoid moving existing directories. + + This path is later interpolated into template files (TOOLS.md, etc.) that agents treat as the + source of truth for where to read/write. + """ + if not workspace_root: msg = "gateway_workspace_root is required" raise ValueError(msg) + root = workspace_root.rstrip("/") + # Use agent key derived from session key when possible. This prevents collisions for # lead agents (session key includes board id) even if multiple boards share the same # display name (e.g. "Lead Agent"). key = _agent_key(agent) + # Backwards-compat: gateway-main agents historically used session keys that encoded # "gateway-" while the gateway agent id is "mc-gateway-". # Keep the on-disk workspace path stable so existing provisioned files aren't moved. if key.startswith("mc-gateway-"): key = key.removeprefix("mc-") + return f"{root}/workspace-{slugify(key)}" @@ -317,7 +370,7 @@ def _build_context( workspace_root = gateway.workspace_root workspace_path = _workspace_path(agent, workspace_root) session_key = agent.openclaw_session_id or "" - base_url = settings.base_url or "REPLACE_WITH_BASE_URL" + base_url = settings.base_url main_session_key = GatewayAgentIdentity.session_key(gateway) identity_context = _identity_context(agent) user_context = _user_context(user) @@ -333,6 +386,7 @@ def _build_context( "board_goal_confirmed": str(board.goal_confirmed).lower(), "board_rule_require_approval_for_done": str(board.require_approval_for_done).lower(), "board_rule_require_review_before_done": str(board.require_review_before_done).lower(), + "board_rule_comment_required_for_review": str(board.comment_required_for_review).lower(), "board_rule_block_status_changes_with_pending_approval": str( board.block_status_changes_with_pending_approval ).lower(), @@ -357,7 +411,7 @@ def _build_main_context( auth_token: str, user: User | None, ) -> dict[str, str]: - base_url = settings.base_url or "REPLACE_WITH_BASE_URL" + base_url = settings.base_url identity_context = _identity_context(agent) user_context = _user_context(user) return { @@ -523,6 +577,7 @@ class OpenClawGatewayControlPlane(GatewayControlPlane): # Prefer an idempotent "create then update" flow. # - Avoids enumerating gateway agents for existence checks. # - Ensures we always hit the "create" RPC first, per lifecycle expectations. + agent_just_created = False try: await openclaw_call( "agents.create", @@ -532,21 +587,47 @@ class OpenClawGatewayControlPlane(GatewayControlPlane): }, config=self._config, ) + agent_just_created = True except OpenClawGatewayError as exc: message = str(exc).lower() if not any( marker in message for marker in ("already", "exist", "duplicate", "conflict") ): raise - await openclaw_call( - "agents.update", - { - "agentId": registration.agent_id, - "name": registration.name, - "workspace": registration.workspace_path, - }, - config=self._config, - ) + + # Gateway hot-reload has a ~500ms debounce after agents.create writes to disk. + # agents.update arriving before the reload completes returns "agent not found". + # Wait for the reload window before attempting the update. + if agent_just_created: + await asyncio.sleep(0.75) + + # Retry agents.update only when this call just created the agent. + # If create reported "already exists", "not found" should fail fast. + _update_retries = 5 + _update_delay = 0.5 + for _attempt in range(_update_retries): + try: + await openclaw_call( + "agents.update", + { + "agentId": registration.agent_id, + "name": registration.name, + "workspace": registration.workspace_path, + }, + config=self._config, + ) + break + except OpenClawGatewayError as exc: + should_retry = ( + agent_just_created + and _is_missing_agent_error(exc) + and _attempt < _update_retries - 1 + ) + if should_retry: + await asyncio.sleep(_update_delay) + _update_delay = min(_update_delay * 2, 4.0) + continue + raise await self.patch_agent_heartbeats( [(registration.agent_id, registration.workspace_path, registration.heartbeat)], ) @@ -609,10 +690,20 @@ class OpenClawGatewayControlPlane(GatewayControlPlane): entry_by_id = _heartbeat_entry_map(entries) new_list = _updated_agent_list(raw_list, entry_by_id) - patch: dict[str, Any] = {"agents": {"list": new_list}} channels_patch = _channel_heartbeat_visibility_patch(config_data) + tools_patch = _tools_exec_host_patch(config_data) + + # Skip config.patch entirely when nothing changed — avoids an unnecessary + # gateway SIGUSR1 restart that rotates agent tokens and breaks active sessions. + if new_list == raw_list and channels_patch is None and tools_patch is None: + logger.debug("patch_agent_heartbeats: no changes detected, skipping config.patch") + return + + patch: dict[str, Any] = {"agents": {"list": new_list}} if channels_patch is not None: patch["channels"] = channels_patch + if tools_patch is not None: + patch["tools"] = tools_patch params = {"raw": json.dumps(patch)} if base_hash: params["baseHash"] = base_hash @@ -970,7 +1061,12 @@ def _control_plane_for_gateway(gateway: Gateway) -> OpenClawGatewayControlPlane: msg = "Gateway url is required" raise OpenClawGatewayError(msg) return OpenClawGatewayControlPlane( - GatewayClientConfig(url=gateway.url, token=gateway.token), + GatewayClientConfig( + url=gateway.url, + token=gateway.token, + allow_insecure_tls=gateway.allow_insecure_tls, + disable_device_pairing=gateway.disable_device_pairing, + ), ) @@ -1099,7 +1195,12 @@ class OpenClawGatewayProvisioner: if not wake: return - client_config = GatewayClientConfig(url=gateway.url, token=gateway.token) + client_config = GatewayClientConfig( + url=gateway.url, + token=gateway.token, + allow_insecure_tls=gateway.allow_insecure_tls, + disable_device_pairing=gateway.disable_device_pairing, + ) await ensure_session(session_key, config=client_config, label=agent.name) verb = wakeup_verb or ("provisioned" if action == "provision" else "updated") await send_message( diff --git a/backend/app/services/openclaw/provisioning_db.py b/backend/app/services/openclaw/provisioning_db.py index 97fcb8f6..15f8e684 100644 --- a/backend/app/services/openclaw/provisioning_db.py +++ b/backend/app/services/openclaw/provisioning_db.py @@ -52,8 +52,6 @@ from app.services.openclaw.constants import ( OFFLINE_AFTER, ) from app.services.openclaw.db_agent_state import ( - mark_provision_complete, - mark_provision_requested, mint_agent_token, ) from app.services.openclaw.db_service import OpenClawDBService @@ -74,6 +72,7 @@ from app.services.openclaw.internal.session_keys import ( board_agent_session_key, board_lead_session_key, ) +from app.services.openclaw.lifecycle_orchestrator import AgentLifecycleOrchestrator from app.services.openclaw.policies import OpenClawAuthorizationPolicy from app.services.openclaw.provisioning import ( OpenClawGatewayControlPlane, @@ -143,7 +142,6 @@ class OpenClawProvisioningService(OpenClawDBService): def __init__(self, session: AsyncSession) -> None: super().__init__(session) - self._gateway = OpenClawGatewayProvisioner() @staticmethod def lead_session_key(board: Board) -> str: @@ -213,25 +211,25 @@ class OpenClawProvisioningService(OpenClawDBService): openclaw_session_id=self.lead_session_key(board), ) raw_token = mint_agent_token(agent) - mark_provision_requested(agent, action=config_options.action, status="provisioning") await self.add_commit_refresh(agent) # Strict behavior: provisioning errors surface to the caller. The DB row exists # so a later retry can succeed with the same deterministic identity/session key. - await self._gateway.apply_agent_lifecycle( - agent=agent, + agent = await AgentLifecycleOrchestrator(self.session).run_lifecycle( gateway=request.gateway, + agent_id=agent.id, board=board, - auth_token=raw_token, user=request.user, action=config_options.action, + auth_token=raw_token, + force_bootstrap=False, + reset_session=False, wake=True, deliver_wakeup=True, + wakeup_verb=None, + clear_confirm_token=False, + raise_gateway_errors=True, ) - - mark_provision_complete(agent, status="online") - await self.add_commit_refresh(agent) - return agent, True async def sync_gateway_templates( @@ -285,7 +283,12 @@ class OpenClawProvisioningService(OpenClawDBService): return result control_plane = OpenClawGatewayControlPlane( - GatewayClientConfig(url=gateway.url, token=gateway.token), + GatewayClientConfig( + url=gateway.url, + token=gateway.token, + allow_insecure_tls=gateway.allow_insecure_tls, + disable_device_pairing=gateway.disable_device_pairing, + ), ) ctx = _SyncContext( session=self.session, @@ -293,7 +296,6 @@ class OpenClawProvisioningService(OpenClawDBService): control_plane=control_plane, backoff=GatewayBackoff(timeout_s=10 * 60, timeout_context="template sync"), options=options, - provisioner=self._gateway, ) if not await _ping_gateway(ctx, result): return result @@ -347,7 +349,6 @@ class _SyncContext: control_plane: OpenClawGatewayControlPlane backoff: GatewayBackoff options: GatewayTemplateSyncOptions - provisioner: OpenClawGatewayProvisioner def _parse_tools_md(content: str) -> dict[str, str]: @@ -579,18 +580,26 @@ async def _sync_one_agent( try: async def _do_provision() -> bool: - await ctx.provisioner.apply_agent_lifecycle( - agent=agent, - gateway=ctx.gateway, - board=board, - auth_token=auth_token, - user=ctx.options.user, - action="update", - force_bootstrap=ctx.options.force_bootstrap, - overwrite=ctx.options.overwrite, - reset_session=ctx.options.reset_sessions, - wake=False, - ) + try: + await AgentLifecycleOrchestrator(ctx.session).run_lifecycle( + gateway=ctx.gateway, + agent_id=agent.id, + board=board, + user=ctx.options.user, + action="update", + auth_token=auth_token, + force_bootstrap=ctx.options.force_bootstrap, + reset_session=ctx.options.reset_sessions, + wake=False, + deliver_wakeup=False, + wakeup_verb="updated", + clear_confirm_token=False, + raise_gateway_errors=True, + ) + except HTTPException as exc: + if exc.status_code == status.HTTP_502_BAD_GATEWAY: + raise OpenClawGatewayError(str(exc.detail)) from exc + raise return True await ctx.backoff.run(_do_provision) @@ -608,6 +617,15 @@ async def _sync_one_agent( message=f"Failed to sync templates: {exc}", ) return False + except HTTPException as exc: + result.agents_skipped += 1 + _append_sync_error( + result, + agent=agent, + board=board, + message=f"Failed to sync templates: {exc.detail}", + ) + return False else: return False @@ -650,18 +668,26 @@ async def _sync_main_agent( try: async def _do_provision_main() -> bool: - await ctx.provisioner.apply_agent_lifecycle( - agent=main_agent, - gateway=ctx.gateway, - board=None, - auth_token=token, - user=ctx.options.user, - action="update", - force_bootstrap=ctx.options.force_bootstrap, - overwrite=ctx.options.overwrite, - reset_session=ctx.options.reset_sessions, - wake=False, - ) + try: + await AgentLifecycleOrchestrator(ctx.session).run_lifecycle( + gateway=ctx.gateway, + agent_id=main_agent.id, + board=None, + user=ctx.options.user, + action="update", + auth_token=token, + force_bootstrap=ctx.options.force_bootstrap, + reset_session=ctx.options.reset_sessions, + wake=False, + deliver_wakeup=False, + wakeup_verb="updated", + clear_confirm_token=False, + raise_gateway_errors=True, + ) + except HTTPException as exc: + if exc.status_code == status.HTTP_502_BAD_GATEWAY: + raise OpenClawGatewayError(str(exc.detail)) from exc + raise return True await ctx.backoff.run(_do_provision_main) @@ -674,6 +700,12 @@ async def _sync_main_agent( agent=main_agent, message=f"Failed to sync gateway agent templates: {exc}", ) + except HTTPException as exc: + _append_sync_error( + result, + agent=main_agent, + message=f"Failed to sync gateway agent templates: {exc.detail}", + ) else: result.main_updated = True return stop_sync @@ -910,6 +942,7 @@ class AgentLifecycleService(OpenClawDBService): event_type="agent.heartbeat", message=f"Heartbeat received from {agent.name}.", agent_id=agent.id, + board_id=agent.board_id, ) @staticmethod @@ -925,6 +958,7 @@ class AgentLifecycleService(OpenClawDBService): event_type=f"agent.{action}.failed", message=f"{action_label} message failed: {error}", agent_id=agent.id, + board_id=agent.board_id, ) async def coerce_agent_create_payload( @@ -1033,7 +1067,6 @@ class AgentLifecycleService(OpenClawDBService): ) -> tuple[Agent, str]: agent = Agent.model_validate(data) raw_token = mint_agent_token(agent) - mark_provision_requested(agent, action="provision", status="provisioning") agent.openclaw_session_id = self.resolve_session_key(agent) await self.add_commit_refresh(agent) return agent, raw_token @@ -1063,92 +1096,65 @@ class AgentLifecycleService(OpenClawDBService): status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="board is required for non-main agent provisioning", ) - template_user = user - if target.is_main_agent and template_user is None: - template_user = await get_org_owner_user( - self.session, - organization_id=target.gateway.organization_id, - ) - if template_user is None: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, - detail=( - "User context is required to provision the gateway main agent " - "(org owner not found)." - ), - ) - await OpenClawGatewayProvisioner().apply_agent_lifecycle( - agent=agent, + provisioned = await AgentLifecycleOrchestrator(self.session).run_lifecycle( gateway=target.gateway, + agent_id=agent.id, board=target.board if not target.is_main_agent else None, - auth_token=auth_token, - user=template_user, + user=user, action=action, + auth_token=auth_token, force_bootstrap=force_bootstrap, reset_session=True, wake=True, deliver_wakeup=True, wakeup_verb=wakeup_verb, + clear_confirm_token=True, + raise_gateway_errors=raise_gateway_errors, ) - mark_provision_complete(agent, status="online", clear_confirm_token=True) - self.session.add(agent) - await self.session.commit() record_activity( self.session, event_type=f"agent.{action}.direct", - message=f"{action.capitalize()}d directly for {agent.name}.", - agent_id=agent.id, + message=f"{action.capitalize()}d directly for {provisioned.name}.", + agent_id=provisioned.id, + board_id=provisioned.board_id, ) record_activity( self.session, event_type="agent.wakeup.sent", - message=f"Wakeup message sent to {agent.name}.", - agent_id=agent.id, + message=f"Wakeup message sent to {provisioned.name}.", + agent_id=provisioned.id, + board_id=provisioned.board_id, ) await self.session.commit() self.logger.info( "agent.provision.success action=%s agent_id=%s", action, - agent.id, + provisioned.id, ) - except OpenClawGatewayError as exc: + except HTTPException as exc: self.record_instruction_failure( self.session, agent, - str(exc), + str(exc.detail), action, ) await self.session.commit() - self.logger.error( - "agent.provision.gateway_error action=%s agent_id=%s error=%s", - action, - agent.id, - str(exc), - ) + if exc.status_code == status.HTTP_502_BAD_GATEWAY: + self.logger.error( + "agent.provision.gateway_error action=%s agent_id=%s error=%s", + action, + agent.id, + str(exc.detail), + ) + else: + self.logger.critical( + "agent.provision.runtime_error action=%s agent_id=%s error=%s", + action, + agent.id, + str(exc.detail), + ) if raise_gateway_errors: - raise HTTPException( - status_code=status.HTTP_502_BAD_GATEWAY, - detail=f"Gateway {action} failed: {exc}", - ) from exc - except (OSError, RuntimeError, ValueError) as exc: # pragma: no cover - self.record_instruction_failure( - self.session, - agent, - str(exc), - action, - ) - await self.session.commit() - self.logger.critical( - "agent.provision.runtime_error action=%s agent_id=%s error=%s", - action, - agent.id, - str(exc), - ) - if raise_gateway_errors: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Unexpected error {action}ing agent provisioning.", - ) from exc + raise async def provision_new_agent( self, @@ -1310,7 +1316,6 @@ class AgentLifecycleService(OpenClawDBService): @staticmethod def mark_agent_update_pending(agent: Agent) -> str: raw_token = mint_agent_token(agent) - mark_provision_requested(agent, action="update", status="updating") return raw_token async def provision_updated_agent( @@ -1385,7 +1390,6 @@ class AgentLifecycleService(OpenClawDBService): return raw_token = mint_agent_token(agent) - mark_provision_requested(agent, action="provision", status="provisioning") await self.add_commit_refresh(agent) board = await self.require_board( str(agent.board_id) if agent.board_id else None, @@ -1431,6 +1435,10 @@ class AgentLifecycleService(OpenClawDBService): elif agent.status == "provisioning": agent.status = "online" agent.last_seen_at = utcnow() + # Successful check-in ends the current wake escalation cycle. + agent.wake_attempts = 0 + agent.checkin_deadline_at = None + agent.last_provision_error = None agent.updated_at = utcnow() self.record_heartbeat(self.session, agent) self.session.add(agent) @@ -1814,6 +1822,7 @@ class AgentLifecycleService(OpenClawDBService): event_type="agent.delete.direct", message=f"Deleted agent {agent.name}.", agent_id=None, + board_id=agent.board_id, ) now = utcnow() await crud.update_where( diff --git a/backend/app/services/openclaw/session_service.py b/backend/app/services/openclaw/session_service.py index c42d707a..948fec7b 100644 --- a/backend/app/services/openclaw/session_service.py +++ b/backend/app/services/openclaw/session_service.py @@ -11,6 +11,7 @@ from fastapi import HTTPException, status from app.core.logging import TRACE_LEVEL from app.models.boards import Board +from app.models.gateways import Gateway from app.schemas.gateway_api import ( GatewayResolveQuery, GatewaySessionHistoryResponse, @@ -20,7 +21,8 @@ from app.schemas.gateway_api import ( GatewaysStatusResponse, ) from app.services.openclaw.db_service import OpenClawDBService -from app.services.openclaw.gateway_compat import check_gateway_runtime_compatibility +from app.services.openclaw.error_messages import normalize_gateway_error_message +from app.services.openclaw.gateway_compat import check_gateway_version_compatibility from app.services.openclaw.gateway_resolver import gateway_client_config, require_gateway_for_board from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig from app.services.openclaw.gateway_rpc import ( @@ -64,11 +66,15 @@ class GatewaySessionService(OpenClawDBService): board_id: str | None, gateway_url: str | None, gateway_token: str | None, + gateway_disable_device_pairing: bool | None = None, + gateway_allow_insecure_tls: bool | None = None, ) -> GatewayResolveQuery: return GatewayResolveQuery( board_id=board_id, gateway_url=gateway_url, gateway_token=gateway_token, + gateway_disable_device_pairing=gateway_disable_device_pairing, + gateway_allow_insecure_tls=gateway_allow_insecure_tls, ) @staticmethod @@ -90,6 +96,7 @@ class GatewaySessionService(OpenClawDBService): params: GatewayResolveQuery, *, user: User | None = None, + organization_id: UUID | None = None, ) -> tuple[Board | None, GatewayClientConfig, str | None]: self.logger.log( TRACE_LEVEL, @@ -104,11 +111,34 @@ class GatewaySessionService(OpenClawDBService): status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="board_id or gateway_url is required", ) + token = (params.gateway_token or "").strip() or None + gateway: Gateway | None = None + can_query_saved_gateway = organization_id is not None and hasattr(self.session, "exec") + if can_query_saved_gateway and ( + params.gateway_allow_insecure_tls is None + or params.gateway_disable_device_pairing is None + ): + gateway_query = Gateway.objects.filter_by(url=raw_url) + if organization_id is not None: + gateway_query = gateway_query.filter_by(organization_id=organization_id) + gateway = await gateway_query.first(self.session) + allow_insecure_tls = ( + params.gateway_allow_insecure_tls + if params.gateway_allow_insecure_tls is not None + else (gateway.allow_insecure_tls if gateway is not None else False) + ) + disable_device_pairing = ( + params.gateway_disable_device_pairing + if params.gateway_disable_device_pairing is not None + else (gateway.disable_device_pairing if gateway is not None else False) + ) return ( None, GatewayClientConfig( url=raw_url, - token=(params.gateway_token or "").strip() or None, + token=token, + allow_insecure_tls=allow_insecure_tls, + disable_device_pairing=disable_device_pairing, ), None, ) @@ -187,15 +217,19 @@ class GatewaySessionService(OpenClawDBService): organization_id: UUID, user: User | None, ) -> GatewaysStatusResponse: - board, config, main_session = await self.resolve_gateway(params, user=user) + board, config, main_session = await self.resolve_gateway( + params, + user=user, + organization_id=organization_id, + ) self._require_same_org(board, organization_id) try: - compatibility = await check_gateway_runtime_compatibility(config) + compatibility = await check_gateway_version_compatibility(config) except OpenClawGatewayError as exc: return GatewaysStatusResponse( connected=False, gateway_url=config.url, - error=str(exc), + error=normalize_gateway_error_message(str(exc)), ) if not compatibility.compatible: return GatewaysStatusResponse( @@ -234,7 +268,7 @@ class GatewaySessionService(OpenClawDBService): return GatewaysStatusResponse( connected=False, gateway_url=config.url, - error=str(exc), + error=normalize_gateway_error_message(str(exc)), ) async def get_sessions( diff --git a/backend/app/services/queue.py b/backend/app/services/queue.py index 04529763..e0d6a1a1 100644 --- a/backend/app/services/queue.py +++ b/backend/app/services/queue.py @@ -150,6 +150,32 @@ def enqueue_task( return False +def enqueue_task_with_delay( + task: QueuedTask, + queue_name: str, + *, + delay_seconds: float, + redis_url: str | None = None, +) -> bool: + """Enqueue a task immediately or schedule it for delayed delivery.""" + delay = max(0.0, float(delay_seconds)) + if delay == 0: + return enqueue_task(task, queue_name, redis_url=redis_url) + try: + return _schedule_for_later(task, queue_name, delay, redis_url=redis_url) + except Exception as exc: + logger.warning( + "rq.queue.schedule_failed", + extra={ + "task_type": task.task_type, + "queue_name": queue_name, + "delay_seconds": delay, + "error": str(exc), + }, + ) + return False + + def _coerce_datetime(raw: object | None) -> datetime: if raw is None: return datetime.now(UTC) diff --git a/backend/app/services/queue_worker.py b/backend/app/services/queue_worker.py index c8761f7d..61eb0332 100644 --- a/backend/app/services/queue_worker.py +++ b/backend/app/services/queue_worker.py @@ -9,6 +9,11 @@ from dataclasses import dataclass from app.core.config import settings from app.core.logging import get_logger +from app.services.openclaw.lifecycle_queue import TASK_TYPE as LIFECYCLE_RECONCILE_TASK_TYPE +from app.services.openclaw.lifecycle_queue import ( + requeue_lifecycle_queue_task, +) +from app.services.openclaw.lifecycle_reconcile import process_lifecycle_queue_task from app.services.queue import QueuedTask, dequeue_task from app.services.webhooks.dispatch import ( process_webhook_queue_task, @@ -17,6 +22,7 @@ from app.services.webhooks.dispatch import ( from app.services.webhooks.queue import TASK_TYPE as WEBHOOK_TASK_TYPE logger = get_logger(__name__) +_WORKER_BLOCK_TIMEOUT_SECONDS = 5.0 @dataclass(frozen=True) @@ -27,6 +33,14 @@ class _TaskHandler: _TASK_HANDLERS: dict[str, _TaskHandler] = { + LIFECYCLE_RECONCILE_TASK_TYPE: _TaskHandler( + handler=process_lifecycle_queue_task, + attempts_to_delay=lambda attempts: min( + settings.rq_dispatch_retry_base_seconds * (2 ** max(0, attempts)), + settings.rq_dispatch_retry_max_seconds, + ), + requeue=lambda task, delay: requeue_lifecycle_queue_task(task, delay_seconds=delay), + ), WEBHOOK_TASK_TYPE: _TaskHandler( handler=process_webhook_queue_task, attempts_to_delay=lambda attempts: min( @@ -115,7 +129,8 @@ async def _run_worker_loop() -> None: try: await flush_queue( block=True, - block_timeout=0, + # Keep a finite timeout so scheduled tasks are periodically drained. + block_timeout=_WORKER_BLOCK_TIMEOUT_SECONDS, ) except Exception: logger.exception( diff --git a/backend/migrations/versions/a9b1c2d3e4f7_add_board_id_to_activity_events.py b/backend/migrations/versions/a9b1c2d3e4f7_add_board_id_to_activity_events.py new file mode 100644 index 00000000..ea46c51f --- /dev/null +++ b/backend/migrations/versions/a9b1c2d3e4f7_add_board_id_to_activity_events.py @@ -0,0 +1,66 @@ +"""add board_id to activity_events + +Revision ID: a9b1c2d3e4f7 +Revises: f1b2c3d4e5a6 +Create Date: 2026-03-04 18:20:00.000000 + +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "a9b1c2d3e4f7" +down_revision = "f1b2c3d4e5a6" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("activity_events", sa.Column("board_id", sa.Uuid(), nullable=True)) + op.execute( + """ + UPDATE activity_events AS ae + SET board_id = t.board_id + FROM tasks AS t + WHERE ae.task_id = t.id + AND ae.board_id IS NULL + """ + ) + op.execute( + """ + UPDATE activity_events AS ae + SET board_id = a.board_id + FROM agents AS a + WHERE ae.agent_id = a.id + AND ae.board_id IS NULL + AND a.board_id IS NOT NULL + """ + ) + op.create_foreign_key( + "fk_activity_events_board_id_boards", + "activity_events", + "boards", + ["board_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_index( + op.f("ix_activity_events_board_id"), + "activity_events", + ["board_id"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index(op.f("ix_activity_events_board_id"), table_name="activity_events") + op.drop_constraint( + "fk_activity_events_board_id_boards", + "activity_events", + type_="foreignkey", + ) + op.drop_column("activity_events", "board_id") diff --git a/backend/migrations/versions/b497b348ebb4_add_gateway_allow_insecure_tls_flag.py b/backend/migrations/versions/b497b348ebb4_add_gateway_allow_insecure_tls_flag.py new file mode 100644 index 00000000..1b4f9718 --- /dev/null +++ b/backend/migrations/versions/b497b348ebb4_add_gateway_allow_insecure_tls_flag.py @@ -0,0 +1,38 @@ +"""Add allow_insecure_tls field to gateways. + +Revision ID: b497b348ebb4 +Revises: c5d1a2b3e4f6 +Create Date: 2026-02-22 20:06:54.417968 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "b497b348ebb4" +down_revision = "c5d1a2b3e4f6" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Add gateways.allow_insecure_tls column with default False.""" + op.add_column( + "gateways", + sa.Column( + "allow_insecure_tls", + sa.Boolean(), + nullable=False, + server_default=sa.text("false"), + ), + ) + op.alter_column("gateways", "allow_insecure_tls", server_default=None) + + +def downgrade() -> None: + """Remove gateways.allow_insecure_tls column.""" + op.drop_column("gateways", "allow_insecure_tls") diff --git a/backend/migrations/versions/c5d1a2b3e4f6_add_disable_device_pairing_to_gateways.py b/backend/migrations/versions/c5d1a2b3e4f6_add_disable_device_pairing_to_gateways.py new file mode 100644 index 00000000..b0ce0978 --- /dev/null +++ b/backend/migrations/versions/c5d1a2b3e4f6_add_disable_device_pairing_to_gateways.py @@ -0,0 +1,37 @@ +"""Add disable_device_pairing setting to gateways. + +Revision ID: c5d1a2b3e4f6 +Revises: b7a1d9c3e4f5 +Create Date: 2026-02-22 00:00:00.000000 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "c5d1a2b3e4f6" +down_revision = "b7a1d9c3e4f5" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Add gateway toggle to bypass device pairing handshake.""" + op.add_column( + "gateways", + sa.Column( + "disable_device_pairing", + sa.Boolean(), + nullable=False, + server_default=sa.false(), + ), + ) + op.alter_column("gateways", "disable_device_pairing", server_default=None) + + +def downgrade() -> None: + """Remove gateway toggle to bypass device pairing handshake.""" + op.drop_column("gateways", "disable_device_pairing") diff --git a/backend/migrations/versions/e3a1b2c4d5f6_add_agent_lifecycle_metadata_columns.py b/backend/migrations/versions/e3a1b2c4d5f6_add_agent_lifecycle_metadata_columns.py new file mode 100644 index 00000000..a179fcbb --- /dev/null +++ b/backend/migrations/versions/e3a1b2c4d5f6_add_agent_lifecycle_metadata_columns.py @@ -0,0 +1,45 @@ +"""Add agent lifecycle metadata columns. + +Revision ID: e3a1b2c4d5f6 +Revises: b497b348ebb4 +Create Date: 2026-02-24 00:00:00.000000 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "e3a1b2c4d5f6" +down_revision = "b497b348ebb4" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Add lifecycle generation, wake tracking, and failure metadata.""" + op.add_column( + "agents", + sa.Column("lifecycle_generation", sa.Integer(), nullable=False, server_default="0"), + ) + op.add_column( + "agents", + sa.Column("wake_attempts", sa.Integer(), nullable=False, server_default="0"), + ) + op.add_column("agents", sa.Column("last_wake_sent_at", sa.DateTime(), nullable=True)) + op.add_column("agents", sa.Column("checkin_deadline_at", sa.DateTime(), nullable=True)) + op.add_column("agents", sa.Column("last_provision_error", sa.Text(), nullable=True)) + op.alter_column("agents", "lifecycle_generation", server_default=None) + op.alter_column("agents", "wake_attempts", server_default=None) + + +def downgrade() -> None: + """Remove lifecycle generation, wake tracking, and failure metadata.""" + op.drop_column("agents", "last_provision_error") + op.drop_column("agents", "checkin_deadline_at") + op.drop_column("agents", "last_wake_sent_at") + op.drop_column("agents", "wake_attempts") + op.drop_column("agents", "lifecycle_generation") + diff --git a/backend/migrations/versions/f1b2c3d4e5a6_add_comment_required_for_review_board_rule.py b/backend/migrations/versions/f1b2c3d4e5a6_add_comment_required_for_review_board_rule.py new file mode 100644 index 00000000..ff530d45 --- /dev/null +++ b/backend/migrations/versions/f1b2c3d4e5a6_add_comment_required_for_review_board_rule.py @@ -0,0 +1,43 @@ +"""add comment-required-for-review board rule + +Revision ID: f1b2c3d4e5a6 +Revises: e3a1b2c4d5f6 +Create Date: 2026-02-25 00:00:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "f1b2c3d4e5a6" +down_revision = "e3a1b2c4d5f6" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + board_columns = {column["name"] for column in inspector.get_columns("boards")} + if "comment_required_for_review" not in board_columns: + op.add_column( + "boards", + sa.Column( + "comment_required_for_review", + sa.Boolean(), + nullable=False, + server_default=sa.false(), + ), + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + board_columns = {column["name"] for column in inspector.get_columns("boards")} + if "comment_required_for_review" in board_columns: + op.drop_column("boards", "comment_required_for_review") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c5ce49e5..46261a6b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -14,7 +14,7 @@ requires-python = ">=3.12" dependencies = [ "alembic==1.18.3", "clerk-backend-api==4.2.0", - "fastapi==0.128.6", + "fastapi==0.131.0", "fastapi-pagination==0.15.10", "jinja2==3.1.6", "psycopg[binary]==3.3.2", @@ -27,6 +27,7 @@ dependencies = [ "websockets==16.0", "redis==6.3.0", "rq==2.6.0", + "cryptography==45.0.7", ] [project.optional-dependencies] diff --git a/backend/templates/BOARD_BOOTSTRAP.md.j2 b/backend/templates/BOARD_BOOTSTRAP.md.j2 index ca847b26..63f2fd49 100644 --- a/backend/templates/BOARD_BOOTSTRAP.md.j2 +++ b/backend/templates/BOARD_BOOTSTRAP.md.j2 @@ -35,12 +35,19 @@ curl -fsS "{{ base_url }}/healthz" >/dev/null 5) Ensure today's daily file exists: `memory/YYYY-MM-DD.md`. {% if is_lead %} -6) Initialize current delivery status in `MEMORY.md`: +6) Immediately check in to Mission Control (do this before any task orchestration): + +```bash +curl -s -X POST "{{ base_url }}/api/v1/agent/heartbeat" \ + -H "X-Agent-Token: {{ auth_token }}" +``` + +7) Initialize current delivery status in `MEMORY.md`: - set objective if missing - set state to `Working` (or `Waiting` if external dependency exists) - set one concrete next step -7) Add one line to `MEMORY.md` noting bootstrap completion date. +8) Add one line to `MEMORY.md` noting bootstrap completion date. {% else %} 6) If any fields are blank, leave them blank. Do not invent values. diff --git a/backend/templates/BOARD_HEARTBEAT.md.j2 b/backend/templates/BOARD_HEARTBEAT.md.j2 index 3a56483e..5e9ef6bd 100644 --- a/backend/templates/BOARD_HEARTBEAT.md.j2 +++ b/backend/templates/BOARD_HEARTBEAT.md.j2 @@ -35,12 +35,20 @@ jq -r ' ## Schedule - If a heartbeat schedule is configured, send a lightweight check-in only. +- On first cycle after wake/bootstrap, run heartbeat check-in immediately (do not wait for cadence). - Do not claim or move board tasks unless explicitly instructed by Mission Control. - If you have any pending `LEAD REQUEST: ASK USER` messages in OpenClaw chat, handle them promptly (see AGENTS.md). ## Heartbeat checklist -1) Check in: +1) Check in immediately: - Use the `agent-main` heartbeat endpoint (`POST /api/v1/agent/heartbeat`). +- Startup check-in example: + +```bash +curl -s -X POST "{{ base_url }}/api/v1/agent/heartbeat" \ + -H "X-Agent-Token: {{ auth_token }}" +``` + - If check-in fails due to 5xx/network, stop and retry next heartbeat. - During that failure window, do **not** write memory updates (`MEMORY.md`, daily memory files). @@ -117,6 +125,7 @@ jq -r ' ## Schedule - Heartbeat cadence is controlled by gateway heartbeat config. +- On first cycle after wake/bootstrap, run heartbeat check-in immediately (do not wait for cadence). - Keep cadence conservative unless there is a clear latency need. ## Non-Negotiable Rules @@ -153,6 +162,7 @@ Before execution: ### Board Rule Snapshot - `require_review_before_done`: `{{ board_rule_require_review_before_done }}` - `require_approval_for_done`: `{{ board_rule_require_approval_for_done }}` +- `comment_required_for_review`: `{{ board_rule_comment_required_for_review }}` - `block_status_changes_with_pending_approval`: `{{ board_rule_block_status_changes_with_pending_approval }}` - `only_lead_can_change_status`: `{{ board_rule_only_lead_can_change_status }}` - `max_agents`: `{{ board_rule_max_agents }}` diff --git a/backend/templates/README.md b/backend/templates/README.md index dff27cf1..819f36eb 100644 --- a/backend/templates/README.md +++ b/backend/templates/README.md @@ -133,6 +133,7 @@ This avoids relying on startup hooks to populate `api/openapi.json`. - `workspace_path` - `board_rule_require_approval_for_done` - `board_rule_require_review_before_done` +- `board_rule_comment_required_for_review` - `board_rule_block_status_changes_with_pending_approval` - `board_rule_only_lead_can_change_status` - `board_rule_max_agents` diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index e522fc29..a9022efe 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -13,3 +13,4 @@ if str(ROOT) not in sys.path: # defaults during import-time settings initialization, regardless of shell env. os.environ["AUTH_MODE"] = "local" os.environ["LOCAL_AUTH_TOKEN"] = "test-local-token-0123456789-0123456789-0123456789x" +os.environ["BASE_URL"] = "http://localhost:8000" diff --git a/backend/tests/test_activity_api_rows.py b/backend/tests/test_activity_api_rows.py index a412e4de..d371dd28 100644 --- a/backend/tests/test_activity_api_rows.py +++ b/backend/tests/test_activity_api_rows.py @@ -5,7 +5,7 @@ from uuid import uuid4 import pytest -from app.api.activity import _coerce_task_comment_rows +from app.api.activity import _build_activity_route, _coerce_activity_rows, _coerce_task_comment_rows from app.models.activity_events import ActivityEvent from app.models.agents import Agent from app.models.boards import Board @@ -34,6 +34,25 @@ class _FakeSqlRow4: raise IndexError(index) +@dataclass +class _FakeSqlRow3: + first: object + second: object + third: object + + def __len__(self) -> int: + return 3 + + def __getitem__(self, index: int) -> object: + if index == 0: + return self.first + if index == 1: + return self.second + if index == 2: + return self.third + raise IndexError(index) + + def _make_event() -> ActivityEvent: return ActivityEvent(event_type="task.comment", message="hello") @@ -87,3 +106,69 @@ def test_coerce_task_comment_rows_rejects_invalid_values(): match="Expected \\(ActivityEvent, Task, Board, Agent \\| None\\) rows", ): _coerce_task_comment_rows([(uuid4(), task, board, None)]) + + +def test_coerce_activity_rows_accepts_plain_tuple(): + board_id = uuid4() + event = _make_event() + + rows = _coerce_activity_rows([(event, board_id, None)]) + assert rows == [(event, board_id, None)] + + +def test_coerce_activity_rows_accepts_row_like_values(): + board_id = uuid4() + event = _make_event() + row = _FakeSqlRow3(event, board_id, None) + + rows = _coerce_activity_rows([row]) + assert rows == [(event, board_id, None)] + + +def test_coerce_activity_rows_rejects_invalid_values(): + event = _make_event() + with pytest.raises( + TypeError, + match="Expected \\(ActivityEvent, event_board_id, task_board_id\\) rows", + ): + _coerce_activity_rows([(event, "bad", None)]) + + +def test_build_activity_route_board_comment(): + board_id = uuid4() + task_id = uuid4() + event = ActivityEvent( + event_type="task.comment", + task_id=task_id, + message="hello", + ) + route_name, route_params = _build_activity_route(event=event, board_id=board_id) + assert route_name == "board" + assert route_params == { + "boardId": str(board_id), + "taskId": str(task_id), + "commentId": str(event.id), + } + + +def test_build_activity_route_board_approvals(): + board_id = uuid4() + event = ActivityEvent( + event_type="approval.lead_notified", + message="hello", + ) + route_name, route_params = _build_activity_route(event=event, board_id=board_id) + assert route_name == "board.approvals" + assert route_params == {"boardId": str(board_id)} + + +def test_build_activity_route_global_fallback(): + event = ActivityEvent( + event_type="gateway.main.lead_broadcast.sent", + message="hello", + ) + route_name, route_params = _build_activity_route(event=event, board_id=None) + assert route_name == "activity" + assert route_params["eventId"] == str(event.id) + assert route_params["eventType"] == event.event_type + assert route_params["createdAt"] == event.created_at.isoformat() diff --git a/backend/tests/test_agent_delete_lead_agent.py b/backend/tests/test_agent_delete_lead_agent.py index 0713b8da..17c68a0d 100644 --- a/backend/tests/test_agent_delete_lead_agent.py +++ b/backend/tests/test_agent_delete_lead_agent.py @@ -51,6 +51,8 @@ class _GatewayStub: url: str token: str | None workspace_root: str + allow_insecure_tls: bool = False + disable_device_pairing: bool = False @pytest.mark.asyncio diff --git a/backend/tests/test_agent_delete_main_agent.py b/backend/tests/test_agent_delete_main_agent.py index 3bcaf9c1..68add474 100644 --- a/backend/tests/test_agent_delete_main_agent.py +++ b/backend/tests/test_agent_delete_main_agent.py @@ -43,6 +43,8 @@ class _GatewayStub: url: str token: str | None workspace_root: str + allow_insecure_tls: bool = False + disable_device_pairing: bool = False @pytest.mark.asyncio diff --git a/backend/tests/test_agent_provisioning_utils.py b/backend/tests/test_agent_provisioning_utils.py index f5529e3b..871c62b8 100644 --- a/backend/tests/test_agent_provisioning_utils.py +++ b/backend/tests/test_agent_provisioning_utils.py @@ -119,6 +119,8 @@ class _GatewayStub: url: str token: str | None workspace_root: str + allow_insecure_tls: bool = False + disable_device_pairing: bool = False @pytest.mark.asyncio @@ -229,6 +231,8 @@ async def test_provision_overwrites_user_md_on_first_provision(monkeypatch): url: str token: str | None workspace_root: str + allow_insecure_tls: bool = False + disable_device_pairing: bool = False class _Manager(agent_provisioning.BaseAgentLifecycleManager): def _agent_id(self, agent): @@ -296,6 +300,8 @@ async def test_set_agent_files_update_preserves_user_md_even_when_size_zero(): url: str token: str | None workspace_root: str + allow_insecure_tls: bool = False + disable_device_pairing: bool = False class _Manager(agent_provisioning.BaseAgentLifecycleManager): def _agent_id(self, agent): @@ -360,6 +366,8 @@ async def test_set_agent_files_update_preserves_nonmissing_user_md(): url: str token: str | None workspace_root: str + allow_insecure_tls: bool = False + disable_device_pairing: bool = False class _Manager(agent_provisioning.BaseAgentLifecycleManager): def _agent_id(self, agent): @@ -424,6 +432,8 @@ async def test_set_agent_files_update_overwrite_writes_preserved_user_md(): url: str token: str | None workspace_root: str + allow_insecure_tls: bool = False + disable_device_pairing: bool = False class _Manager(agent_provisioning.BaseAgentLifecycleManager): def _agent_id(self, agent): @@ -520,6 +530,89 @@ async def test_control_plane_upsert_agent_handles_already_exists(monkeypatch): assert calls[1][0] == "agents.update" +@pytest.mark.asyncio +async def test_control_plane_upsert_agent_retries_update_after_create_race(monkeypatch): + calls: list[tuple[str, dict[str, object] | None]] = [] + sleeps: list[float] = [] + update_attempts = 0 + + async def _fake_sleep(seconds: float) -> None: + sleeps.append(seconds) + + async def _fake_openclaw_call(method, params=None, config=None): + nonlocal update_attempts + _ = config + calls.append((method, params)) + if method == "agents.create": + return {"ok": True} + if method == "agents.update": + update_attempts += 1 + if update_attempts < 3: + raise agent_provisioning.OpenClawGatewayError('agent "board-agent-a" not found') + return {"ok": True} + if method == "config.get": + return {"hash": None, "config": {"agents": {"list": []}}} + if method == "config.patch": + return {"ok": True} + raise AssertionError(f"Unexpected method: {method}") + + monkeypatch.setattr(agent_provisioning, "openclaw_call", _fake_openclaw_call) + monkeypatch.setattr(agent_provisioning.asyncio, "sleep", _fake_sleep) + cp = agent_provisioning.OpenClawGatewayControlPlane( + agent_provisioning.GatewayClientConfig(url="ws://gateway.example/ws", token=None), + ) + await cp.upsert_agent( + agent_provisioning.GatewayAgentRegistration( + agent_id="board-agent-a", + name="Board Agent A", + workspace_path="/tmp/workspace-board-agent-a", + heartbeat={"every": "10m", "target": "last", "includeReasoning": False}, + ), + ) + + update_calls = [method for method, _ in calls if method == "agents.update"] + assert len(update_calls) == 3 + assert sleeps == [0.75, 0.5, 1.0] + + +@pytest.mark.asyncio +async def test_control_plane_upsert_agent_missing_after_already_exists_fails_fast(monkeypatch): + calls: list[tuple[str, dict[str, object] | None]] = [] + sleeps: list[float] = [] + + async def _fake_sleep(seconds: float) -> None: + sleeps.append(seconds) + + async def _fake_openclaw_call(method, params=None, config=None): + _ = config + calls.append((method, params)) + if method == "agents.create": + raise agent_provisioning.OpenClawGatewayError("already exists") + if method == "agents.update": + raise agent_provisioning.OpenClawGatewayError('agent "board-agent-a" not found') + raise AssertionError(f"Unexpected method: {method}") + + monkeypatch.setattr(agent_provisioning, "openclaw_call", _fake_openclaw_call) + monkeypatch.setattr(agent_provisioning.asyncio, "sleep", _fake_sleep) + cp = agent_provisioning.OpenClawGatewayControlPlane( + agent_provisioning.GatewayClientConfig(url="ws://gateway.example/ws", token=None), + ) + + with pytest.raises(agent_provisioning.OpenClawGatewayError): + await cp.upsert_agent( + agent_provisioning.GatewayAgentRegistration( + agent_id="board-agent-a", + name="Board Agent A", + workspace_path="/tmp/workspace-board-agent-a", + heartbeat={"every": "10m", "target": "last", "includeReasoning": False}, + ), + ) + + update_calls = [method for method, _ in calls if method == "agents.update"] + assert len(update_calls) == 1 + assert sleeps == [] + + def test_is_missing_agent_error_matches_gateway_agent_not_found() -> None: assert agent_provisioning._is_missing_agent_error( agent_provisioning.OpenClawGatewayError('agent "mc-abc" not found'), diff --git a/backend/tests/test_agent_webhook_payload_read_api.py b/backend/tests/test_agent_webhook_payload_read_api.py new file mode 100644 index 00000000..a4c45ece --- /dev/null +++ b/backend/tests/test_agent_webhook_payload_read_api.py @@ -0,0 +1,287 @@ +# ruff: noqa: INP001 + +from __future__ import annotations + +import json +from uuid import UUID, uuid4 + +import pytest +from fastapi import APIRouter, Depends, FastAPI +from httpx import ASGITransport, AsyncClient +from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine +from sqlmodel import SQLModel +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.api.agent import router as agent_router +from app.api.deps import get_board_or_404 +from app.core.agent_tokens import hash_agent_token +from app.db.session import get_session +from app.models.agents import Agent +from app.models.board_webhook_payloads import BoardWebhookPayload +from app.models.board_webhooks import BoardWebhook +from app.models.boards import Board +from app.models.gateways import Gateway +from app.models.organizations import Organization + + +async def _make_engine() -> AsyncEngine: + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + async with engine.connect() as conn, conn.begin(): + await conn.run_sync(SQLModel.metadata.create_all) + return engine + + +def _build_test_app(session_maker: async_sessionmaker[AsyncSession]) -> FastAPI: + app = FastAPI() + api_v1 = APIRouter(prefix="/api/v1") + api_v1.include_router(agent_router) + app.include_router(api_v1) + + async def _override_get_session() -> AsyncSession: + async with session_maker() as session: + yield session + + async def _override_get_board_or_404( + board_id: str, + session: AsyncSession = Depends(get_session), + ) -> Board: + board = await Board.objects.by_id(UUID(board_id)).first(session) + if board is None: + from fastapi import HTTPException, status + + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return board + + app.dependency_overrides[get_session] = _override_get_session + app.dependency_overrides[get_board_or_404] = _override_get_board_or_404 + return app + + +async def _seed_payload( + session: AsyncSession, + *, + payload_value: dict[str, object] | list[object] | str | int | float | bool | None = None, +) -> tuple[str, Board, BoardWebhook, BoardWebhookPayload]: + token = "test-agent-token-" + uuid4().hex + token_hash = hash_agent_token(token) + + organization_id = uuid4() + gateway_id = uuid4() + board_id = uuid4() + webhook_id = uuid4() + agent_id = uuid4() + payload_id = uuid4() + + session.add(Organization(id=organization_id, name=f"org-{organization_id}")) + session.add( + Gateway( + id=gateway_id, + organization_id=organization_id, + name="gateway", + url="https://gateway.example.local", + workspace_root="/tmp/workspace", + ), + ) + board = Board( + id=board_id, + organization_id=organization_id, + gateway_id=gateway_id, + name="Board", + slug="board", + ) + session.add(board) + session.add( + Agent( + id=agent_id, + board_id=board_id, + gateway_id=gateway_id, + name="Lead Agent", + status="online", + is_board_lead=True, + openclaw_session_id="agent:lead:session", + agent_token_hash=token_hash, + ), + ) + webhook = BoardWebhook( + id=webhook_id, + board_id=board_id, + description="Triage payload", + enabled=True, + ) + session.add(webhook) + payload = BoardWebhookPayload( + id=payload_id, + board_id=board_id, + webhook_id=webhook_id, + payload=payload_value or {"event": "push", "ref": "refs/heads/master"}, + headers={"x-github-event": "push"}, + content_type="application/json", + source_ip="127.0.0.1", + ) + session.add(payload) + await session.commit() + return token, board, webhook, payload + + +@pytest.mark.asyncio +async def test_agent_can_fetch_webhook_payload() -> None: + engine = await _make_engine() + session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + app = _build_test_app(session_maker) + + async with session_maker() as session: + token, board, webhook, payload = await _seed_payload(session) + + try: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get( + f"/api/v1/agent/boards/{board.id}/webhooks/{webhook.id}/payloads/{payload.id}", + headers={"X-Agent-Token": token}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["id"] == str(payload.id) + assert body["board_id"] == str(board.id) + assert body["webhook_id"] == str(webhook.id) + assert body["payload"] == {"event": "push", "ref": "refs/heads/master"} + assert body["headers"]["x-github-event"] == "push" + + finally: + await engine.dispose() + + +@pytest.mark.asyncio +async def test_agent_payload_read_rejects_invalid_token() -> None: + engine = await _make_engine() + session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + app = _build_test_app(session_maker) + + async with session_maker() as session: + _token, board, webhook, payload = await _seed_payload(session) + + try: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get( + f"/api/v1/agent/boards/{board.id}/webhooks/{webhook.id}/payloads/{payload.id}", + headers={"X-Agent-Token": "invalid"}, + ) + + assert response.status_code == 401 + + finally: + await engine.dispose() + + +@pytest.mark.asyncio +async def test_agent_payload_read_truncates_json_preview_with_ellipsis() -> None: + engine = await _make_engine() + session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + app = _build_test_app(session_maker) + + async with session_maker() as session: + payload_value: dict[str, object] = {"event": "push", "ref": "refs/heads/master"} + token, board, webhook, payload = await _seed_payload(session, payload_value=payload_value) + + max_chars = 12 + raw = json.dumps(payload_value, ensure_ascii=True) + expected_preview = f"{raw[: max_chars - 3]}..." + + try: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get( + f"/api/v1/agent/boards/{board.id}/webhooks/{webhook.id}/payloads/{payload.id}", + headers={"X-Agent-Token": token}, + params={"max_chars": max_chars}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["payload"] == expected_preview + + finally: + await engine.dispose() + + +@pytest.mark.asyncio +async def test_agent_payload_read_truncates_string_preview_without_json_quoting() -> None: + engine = await _make_engine() + session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + app = _build_test_app(session_maker) + + async with session_maker() as session: + token, board, webhook, payload = await _seed_payload(session, payload_value="abcdef") + + try: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get( + f"/api/v1/agent/boards/{board.id}/webhooks/{webhook.id}/payloads/{payload.id}", + headers={"X-Agent-Token": token}, + params={"max_chars": 4}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["payload"] == "a..." + + finally: + await engine.dispose() + + +@pytest.mark.asyncio +async def test_agent_payload_read_rejects_cross_board_access() -> None: + engine = await _make_engine() + session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + app = _build_test_app(session_maker) + + async with session_maker() as session: + token, board, webhook, payload = await _seed_payload(session) + + # Second board + payload that should be inaccessible to the first board agent. + organization_id = uuid4() + gateway_id = uuid4() + other_board = Board( + id=uuid4(), + organization_id=organization_id, + gateway_id=gateway_id, + name="Other", + slug="other", + ) + session.add(Organization(id=organization_id, name=f"org-{organization_id}")) + session.add( + Gateway( + id=gateway_id, + organization_id=organization_id, + name="gateway", + url="https://gateway.example.local", + workspace_root="/tmp/workspace", + ), + ) + session.add(other_board) + other_webhook = BoardWebhook( + id=uuid4(), + board_id=other_board.id, + description="Other webhook", + enabled=True, + ) + session.add(other_webhook) + other_payload = BoardWebhookPayload( + id=uuid4(), + board_id=other_board.id, + webhook_id=other_webhook.id, + payload={"event": "push"}, + ) + session.add(other_payload) + await session.commit() + + try: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get( + f"/api/v1/agent/boards/{other_board.id}/webhooks/{other_webhook.id}/payloads/{other_payload.id}", + headers={"X-Agent-Token": token}, + ) + + assert response.status_code == 403 + + finally: + await engine.dispose() diff --git a/backend/tests/test_board_group_assignment_notifications.py b/backend/tests/test_board_group_assignment_notifications.py index 5c24fcc3..e7d481c8 100644 --- a/backend/tests/test_board_group_assignment_notifications.py +++ b/backend/tests/test_board_group_assignment_notifications.py @@ -69,11 +69,15 @@ async def test_update_board_notifies_agents_when_added_to_group( async def _fake_notify(**_kwargs: Any) -> None: calls["notify"] += 1 + async def _fake_lead_notify(**_kwargs: Any) -> None: + return None + async def _fake_get_by_id(*_args: Any, **_kwargs: Any) -> BoardGroup: return group monkeypatch.setattr(boards, "_apply_board_update", _fake_apply_board_update) monkeypatch.setattr(boards, "_notify_agents_on_board_group_addition", _fake_notify) + monkeypatch.setattr(boards, "_notify_lead_on_board_update", _fake_lead_notify) monkeypatch.setattr(boards.crud, "get_by_id", _fake_get_by_id) updated = await boards.update_board( @@ -108,12 +112,16 @@ async def test_update_board_notifies_agents_when_removed_from_group( async def _fake_leave(**_kwargs: Any) -> None: calls["leave"] += 1 + async def _fake_lead_notify(**_kwargs: Any) -> None: + return None + async def _fake_get_by_id(*_args: Any, **_kwargs: Any) -> BoardGroup: return group monkeypatch.setattr(boards, "_apply_board_update", _fake_apply_board_update) monkeypatch.setattr(boards, "_notify_agents_on_board_group_addition", _fake_join) monkeypatch.setattr(boards, "_notify_agents_on_board_group_removal", _fake_leave) + monkeypatch.setattr(boards, "_notify_lead_on_board_update", _fake_lead_notify) monkeypatch.setattr(boards.crud, "get_by_id", _fake_get_by_id) updated = await boards.update_board( @@ -151,6 +159,9 @@ async def test_update_board_notifies_agents_when_moved_between_groups( async def _fake_leave(**_kwargs: Any) -> None: calls["leave"] += 1 + async def _fake_lead_notify(**_kwargs: Any) -> None: + return None + async def _fake_get_by_id(_session: Any, _model: Any, obj_id: UUID) -> BoardGroup | None: if obj_id == old_group_id: return old_group @@ -161,6 +172,7 @@ async def test_update_board_notifies_agents_when_moved_between_groups( monkeypatch.setattr(boards, "_apply_board_update", _fake_apply_board_update) monkeypatch.setattr(boards, "_notify_agents_on_board_group_addition", _fake_join) monkeypatch.setattr(boards, "_notify_agents_on_board_group_removal", _fake_leave) + monkeypatch.setattr(boards, "_notify_lead_on_board_update", _fake_lead_notify) monkeypatch.setattr(boards.crud, "get_by_id", _fake_get_by_id) updated = await boards.update_board( @@ -192,9 +204,13 @@ async def test_update_board_does_not_notify_when_group_unchanged( async def _fake_notify(**_kwargs: Any) -> None: calls["notify"] += 1 + async def _fake_lead_notify(**_kwargs: Any) -> None: + return None + monkeypatch.setattr(boards, "_apply_board_update", _fake_apply_board_update) monkeypatch.setattr(boards, "_notify_agents_on_board_group_addition", _fake_notify) monkeypatch.setattr(boards, "_notify_agents_on_board_group_removal", _fake_notify) + monkeypatch.setattr(boards, "_notify_lead_on_board_update", _fake_lead_notify) updated = await boards.update_board( payload=payload, @@ -206,6 +222,66 @@ async def test_update_board_does_not_notify_when_group_unchanged( assert calls["notify"] == 0 +@pytest.mark.asyncio +async def test_update_board_notifies_lead_when_fields_change( + monkeypatch: pytest.MonkeyPatch, +) -> None: + board = _board(board_group_id=None) + session = _FakeSession() + payload = BoardUpdate(name="Platform X") + calls: dict[str, object] = {"count": 0, "changes": {}} + + async def _fake_apply_board_update(**kwargs: Any) -> Board: + target: Board = kwargs["board"] + target.name = "Platform X" + return target + + async def _fake_lead_notify(**kwargs: Any) -> None: + calls["count"] = int(calls["count"]) + 1 + calls["changes"] = kwargs["changed_fields"] + + monkeypatch.setattr(boards, "_apply_board_update", _fake_apply_board_update) + monkeypatch.setattr(boards, "_notify_lead_on_board_update", _fake_lead_notify) + + updated = await boards.update_board( + payload=payload, + session=session, # type: ignore[arg-type] + board=board, + ) + + assert updated.name == "Platform X" + assert calls["count"] == 1 + assert calls["changes"] == {"name": ("Platform", "Platform X")} + + +@pytest.mark.asyncio +async def test_update_board_skips_lead_notify_when_no_effective_change( + monkeypatch: pytest.MonkeyPatch, +) -> None: + board = _board(board_group_id=None) + session = _FakeSession() + payload = BoardUpdate(name="Platform") + calls = {"lead_notify": 0} + + async def _fake_apply_board_update(**kwargs: Any) -> Board: + return kwargs["board"] + + async def _fake_lead_notify(**_kwargs: Any) -> None: + calls["lead_notify"] += 1 + + monkeypatch.setattr(boards, "_apply_board_update", _fake_apply_board_update) + monkeypatch.setattr(boards, "_notify_lead_on_board_update", _fake_lead_notify) + + updated = await boards.update_board( + payload=payload, + session=session, # type: ignore[arg-type] + board=board, + ) + + assert updated.name == "Platform" + assert calls["lead_notify"] == 0 + + @pytest.mark.asyncio async def test_notify_agents_on_board_group_addition_fanout_and_records_results( monkeypatch: pytest.MonkeyPatch, diff --git a/backend/tests/test_board_schema.py b/backend/tests/test_board_schema.py index f0aa4516..a66a07f5 100644 --- a/backend/tests/test_board_schema.py +++ b/backend/tests/test_board_schema.py @@ -86,6 +86,7 @@ def test_board_rule_toggles_have_expected_defaults() -> None: ) assert created.require_approval_for_done is True assert created.require_review_before_done is False + assert created.comment_required_for_review is False assert created.block_status_changes_with_pending_approval is False assert created.only_lead_can_change_status is False assert created.max_agents == 1 @@ -93,12 +94,14 @@ def test_board_rule_toggles_have_expected_defaults() -> None: updated = BoardUpdate( require_approval_for_done=False, require_review_before_done=True, + comment_required_for_review=True, block_status_changes_with_pending_approval=True, only_lead_can_change_status=True, max_agents=3, ) assert updated.require_approval_for_done is False assert updated.require_review_before_done is True + assert updated.comment_required_for_review is True assert updated.block_status_changes_with_pending_approval is True assert updated.only_lead_can_change_status is True assert updated.max_agents == 3 diff --git a/backend/tests/test_boards_delete.py b/backend/tests/test_boards_delete.py index 228cdb69..926abfd8 100644 --- a/backend/tests/test_boards_delete.py +++ b/backend/tests/test_boards_delete.py @@ -62,6 +62,7 @@ async def test_delete_board_cleans_org_board_access_rows() -> None: ) deleted_table_names = [statement.table.name for statement in session.executed] + assert "activity_events" in deleted_table_names assert "organization_board_access" in deleted_table_names assert "organization_invite_board_access" in deleted_table_names assert "board_task_custom_fields" in deleted_table_names diff --git a/backend/tests/test_config_auth_mode.py b/backend/tests/test_config_auth_mode.py index 1f913302..6c6ac1fc 100644 --- a/backend/tests/test_config_auth_mode.py +++ b/backend/tests/test_config_auth_mode.py @@ -9,6 +9,8 @@ from pydantic import ValidationError from app.core.auth_mode import AuthMode from app.core.config import Settings +BASE_URL = "http://localhost:8000" + def test_local_mode_requires_non_empty_token() -> None: with pytest.raises( @@ -19,6 +21,7 @@ def test_local_mode_requires_non_empty_token() -> None: _env_file=None, auth_mode=AuthMode.LOCAL, local_auth_token="", + base_url=BASE_URL, ) @@ -31,6 +34,7 @@ def test_local_mode_requires_minimum_length() -> None: _env_file=None, auth_mode=AuthMode.LOCAL, local_auth_token="x" * 49, + base_url=BASE_URL, ) @@ -43,6 +47,7 @@ def test_local_mode_rejects_placeholder_token() -> None: _env_file=None, auth_mode=AuthMode.LOCAL, local_auth_token="change-me", + base_url=BASE_URL, ) @@ -52,6 +57,7 @@ def test_local_mode_accepts_real_token() -> None: _env_file=None, auth_mode=AuthMode.LOCAL, local_auth_token=token, + base_url=BASE_URL, ) assert settings.auth_mode == AuthMode.LOCAL @@ -67,4 +73,65 @@ def test_clerk_mode_requires_secret_key() -> None: _env_file=None, auth_mode=AuthMode.CLERK, clerk_secret_key="", + base_url=BASE_URL, ) + + +def test_base_url_required() -> None: + with pytest.raises( + ValidationError, + match="BASE_URL must be set and non-empty", + ): + Settings( + _env_file=None, + auth_mode=AuthMode.CLERK, + clerk_secret_key="sk_test", + base_url=" ", + ) + + +def test_base_url_field_is_required(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("BASE_URL", raising=False) + + with pytest.raises(ValidationError) as exc_info: + Settings( + _env_file=None, + auth_mode=AuthMode.CLERK, + clerk_secret_key="sk_test", + ) + + text = str(exc_info.value) + assert "base_url" in text + assert "Field required" in text + + +@pytest.mark.parametrize( + "base_url", + [ + "localhost:8000", + "ws://localhost:8000", + ], +) +def test_base_url_requires_absolute_http_url(base_url: str) -> None: + with pytest.raises( + ValidationError, + match="BASE_URL must be an absolute http\\(s\\) URL", + ): + Settings( + _env_file=None, + auth_mode=AuthMode.CLERK, + clerk_secret_key="sk_test", + base_url=base_url, + ) + + +def test_base_url_is_normalized_without_trailing_slash() -> None: + token = "a" * 50 + settings = Settings( + _env_file=None, + auth_mode=AuthMode.LOCAL, + local_auth_token=token, + base_url="http://localhost:8000/ ", + ) + + assert settings.base_url == BASE_URL diff --git a/backend/tests/test_gateway_device_identity.py b/backend/tests/test_gateway_device_identity.py new file mode 100644 index 00000000..39db8cf8 --- /dev/null +++ b/backend/tests/test_gateway_device_identity.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import base64 + +import pytest +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +from app.services.openclaw.device_identity import ( + build_device_auth_payload, + load_or_create_device_identity, + sign_device_payload, +) + + +def _base64url_decode(value: str) -> bytes: + padding = "=" * ((4 - len(value) % 4) % 4) + return base64.urlsafe_b64decode(f"{value}{padding}") + + +def test_load_or_create_device_identity_persists_same_identity( + monkeypatch: pytest.MonkeyPatch, + tmp_path, +) -> None: + identity_path = tmp_path / "identity" / "device.json" + monkeypatch.setenv("OPENCLAW_GATEWAY_DEVICE_IDENTITY_PATH", str(identity_path)) + + first = load_or_create_device_identity() + second = load_or_create_device_identity() + + assert identity_path.exists() + assert first.device_id == second.device_id + assert first.public_key_pem.strip() == second.public_key_pem.strip() + assert first.private_key_pem.strip() == second.private_key_pem.strip() + + +def test_build_device_auth_payload_uses_nonce_for_v2() -> None: + payload = build_device_auth_payload( + device_id="dev", + client_id="gateway-client", + client_mode="backend", + role="operator", + scopes=["operator.read", "operator.admin"], + signed_at_ms=123, + token="token", + nonce="nonce-xyz", + ) + + assert payload == ( + "v2|dev|gateway-client|backend|operator|operator.read,operator.admin|123|token|nonce-xyz" + ) + + +def test_sign_device_payload_produces_valid_ed25519_signature( + monkeypatch: pytest.MonkeyPatch, + tmp_path, +) -> None: + identity_path = tmp_path / "identity" / "device.json" + monkeypatch.setenv("OPENCLAW_GATEWAY_DEVICE_IDENTITY_PATH", str(identity_path)) + identity = load_or_create_device_identity() + + payload = "v1|device|client|backend|operator|operator.read|1|token" + signature = sign_device_payload(identity.private_key_pem, payload) + + loaded = serialization.load_pem_public_key(identity.public_key_pem.encode("utf-8")) + assert isinstance(loaded, Ed25519PublicKey) + loaded.verify(_base64url_decode(signature), payload.encode("utf-8")) diff --git a/backend/tests/test_gateway_resolver.py b/backend/tests/test_gateway_resolver.py new file mode 100644 index 00000000..84d988bd --- /dev/null +++ b/backend/tests/test_gateway_resolver.py @@ -0,0 +1,182 @@ +# ruff: noqa: S101 +from __future__ import annotations + +from uuid import uuid4 + +import pytest + +import app.services.openclaw.session_service as session_service +from app.models.gateways import Gateway +from app.schemas.gateway_api import GatewayResolveQuery +from app.services.openclaw.gateway_resolver import ( + gateway_client_config, + optional_gateway_client_config, +) +from app.services.openclaw.session_service import GatewaySessionService + + +def _gateway( + *, + disable_device_pairing: bool, + allow_insecure_tls: bool = False, + url: str = "ws://gateway.example:18789/ws", + token: str | None = " secret-token ", +) -> Gateway: + return Gateway( + id=uuid4(), + organization_id=uuid4(), + name="Primary gateway", + url=url, + token=token, + workspace_root="~/.openclaw", + disable_device_pairing=disable_device_pairing, + allow_insecure_tls=allow_insecure_tls, + ) + + +def test_gateway_client_config_maps_disable_device_pairing() -> None: + config = gateway_client_config(_gateway(disable_device_pairing=True)) + + assert config.url == "ws://gateway.example:18789/ws" + assert config.token == "secret-token" + assert config.disable_device_pairing is True + + +def test_optional_gateway_client_config_maps_disable_device_pairing() -> None: + config = optional_gateway_client_config(_gateway(disable_device_pairing=False)) + + assert config is not None + assert config.disable_device_pairing is False + + +def test_gateway_client_config_maps_allow_insecure_tls() -> None: + config = gateway_client_config( + _gateway(disable_device_pairing=False, allow_insecure_tls=True), + ) + + assert config.allow_insecure_tls is True + + +def test_optional_gateway_client_config_returns_none_for_missing_or_blank_url() -> None: + assert optional_gateway_client_config(None) is None + assert ( + optional_gateway_client_config( + _gateway(disable_device_pairing=False, url=" "), + ) + is None + ) + + +def test_to_resolve_query_keeps_gateway_disable_device_pairing_value() -> None: + resolved = GatewaySessionService.to_resolve_query( + board_id=None, + gateway_url="ws://gateway.example:18789/ws", + gateway_token="secret-token", + gateway_disable_device_pairing=True, + ) + + assert resolved.gateway_disable_device_pairing is True + + +def test_to_resolve_query_keeps_gateway_allow_insecure_tls_value() -> None: + resolved = GatewaySessionService.to_resolve_query( + board_id=None, + gateway_url="wss://gateway.example:18789/ws", + gateway_token="secret-token", + gateway_allow_insecure_tls=True, + ) + + assert resolved.gateway_allow_insecure_tls is True + + +@pytest.mark.asyncio +async def test_resolve_gateway_keeps_gateway_allow_insecure_tls_for_direct_url() -> None: + service = GatewaySessionService(session=object()) # type: ignore[arg-type] + _, config, _ = await service.resolve_gateway( + GatewayResolveQuery( + gateway_url="wss://gateway.example:18789/ws", + gateway_allow_insecure_tls=True, + ), + user=None, + ) + + assert config.allow_insecure_tls is True + + +class _FakeGatewayQuery: + def __init__(self, gateway: Gateway | None) -> None: + self._gateway = gateway + self.filters: list[dict[str, object]] = [] + + def filter_by(self, **kwargs: object) -> _FakeGatewayQuery: + self.filters.append(kwargs) + return self + + async def first(self, _session: object) -> Gateway | None: + return self._gateway + + +class _FakeAsyncSession: + async def exec( + self, *_args: object, **_kwargs: object + ) -> None: # pragma: no cover - guard only + return None + + +@pytest.mark.asyncio +async def test_resolve_gateway_uses_saved_gateway_settings_when_direct_flags_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + gateway = _gateway( + disable_device_pairing=True, + allow_insecure_tls=True, + url="wss://gateway.example:18789/ws", + token=" db-token ", + ) + fake_query = _FakeGatewayQuery(gateway) + monkeypatch.setattr(session_service.Gateway, "objects", fake_query) + + service = GatewaySessionService(session=_FakeAsyncSession()) # type: ignore[arg-type] + _, config, _ = await service.resolve_gateway( + GatewayResolveQuery(gateway_url=gateway.url), + user=None, + organization_id=gateway.organization_id, + ) + + assert config.token is None + assert config.allow_insecure_tls is True + assert config.disable_device_pairing is True + assert fake_query.filters == [ + {"url": gateway.url}, + {"organization_id": gateway.organization_id}, + ] + + +@pytest.mark.asyncio +async def test_resolve_gateway_prefers_explicit_direct_flags_over_saved_settings( + monkeypatch: pytest.MonkeyPatch, +) -> None: + gateway = _gateway( + disable_device_pairing=True, + allow_insecure_tls=True, + url="wss://gateway.example:18789/ws", + token="db-token", + ) + fake_query = _FakeGatewayQuery(gateway) + monkeypatch.setattr(session_service.Gateway, "objects", fake_query) + + service = GatewaySessionService(session=object()) # type: ignore[arg-type] + _, config, _ = await service.resolve_gateway( + GatewayResolveQuery( + gateway_url=gateway.url, + gateway_token="explicit-token", + gateway_allow_insecure_tls=False, + gateway_disable_device_pairing=False, + ), + user=None, + organization_id=gateway.organization_id, + ) + + assert config.token == "explicit-token" + assert config.allow_insecure_tls is False + assert config.disable_device_pairing is False diff --git a/backend/tests/test_gateway_rpc_connect_scopes.py b/backend/tests/test_gateway_rpc_connect_scopes.py index 962ee90d..00a1db86 100644 --- a/backend/tests/test_gateway_rpc_connect_scopes.py +++ b/backend/tests/test_gateway_rpc_connect_scopes.py @@ -1,24 +1,284 @@ from __future__ import annotations +import pytest + +import app.services.openclaw.gateway_rpc as gateway_rpc from app.services.openclaw.gateway_rpc import ( + CONTROL_UI_CLIENT_ID, + CONTROL_UI_CLIENT_MODE, + DEFAULT_GATEWAY_CLIENT_ID, + DEFAULT_GATEWAY_CLIENT_MODE, GATEWAY_OPERATOR_SCOPES, GatewayConfig, + OpenClawGatewayError, _build_connect_params, + _build_control_ui_origin, + openclaw_call, ) -def test_build_connect_params_sets_explicit_operator_role_and_scopes() -> None: +def test_build_connect_params_defaults_to_device_pairing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + expected_device_payload = { + "id": "device-id", + "publicKey": "public-key", + "signature": "signature", + "signedAt": 1, + } + + def _fake_build_device_connect_payload( + *, + client_id: str, + client_mode: str, + role: str, + scopes: list[str], + auth_token: str | None, + connect_nonce: str | None, + ) -> dict[str, object]: + captured["client_id"] = client_id + captured["client_mode"] = client_mode + captured["role"] = role + captured["scopes"] = scopes + captured["auth_token"] = auth_token + captured["connect_nonce"] = connect_nonce + return expected_device_payload + + monkeypatch.setattr( + gateway_rpc, + "_build_device_connect_payload", + _fake_build_device_connect_payload, + ) + params = _build_connect_params(GatewayConfig(url="ws://gateway.example/ws")) assert params["role"] == "operator" assert params["scopes"] == list(GATEWAY_OPERATOR_SCOPES) + assert params["client"]["id"] == DEFAULT_GATEWAY_CLIENT_ID + assert params["client"]["mode"] == DEFAULT_GATEWAY_CLIENT_MODE + assert params["device"] == expected_device_payload assert "auth" not in params + assert captured["client_id"] == DEFAULT_GATEWAY_CLIENT_ID + assert captured["client_mode"] == DEFAULT_GATEWAY_CLIENT_MODE + assert captured["role"] == "operator" + assert captured["scopes"] == list(GATEWAY_OPERATOR_SCOPES) + assert captured["auth_token"] is None + assert captured["connect_nonce"] is None -def test_build_connect_params_includes_auth_token_when_provided() -> None: +def test_build_connect_params_uses_control_ui_when_pairing_disabled() -> None: params = _build_connect_params( - GatewayConfig(url="ws://gateway.example/ws", token="secret-token"), + GatewayConfig( + url="ws://gateway.example/ws", + token="secret-token", + disable_device_pairing=True, + ), ) assert params["auth"] == {"token": "secret-token"} assert params["scopes"] == list(GATEWAY_OPERATOR_SCOPES) + assert params["client"]["id"] == CONTROL_UI_CLIENT_ID + assert params["client"]["mode"] == CONTROL_UI_CLIENT_MODE + assert "device" not in params + + +def test_build_connect_params_passes_nonce_to_device_payload( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_build_device_connect_payload( + *, + client_id: str, + client_mode: str, + role: str, + scopes: list[str], + auth_token: str | None, + connect_nonce: str | None, + ) -> dict[str, object]: + captured["client_id"] = client_id + captured["client_mode"] = client_mode + captured["role"] = role + captured["scopes"] = scopes + captured["auth_token"] = auth_token + captured["connect_nonce"] = connect_nonce + return {"id": "device-id", "nonce": connect_nonce} + + monkeypatch.setattr( + gateway_rpc, + "_build_device_connect_payload", + _fake_build_device_connect_payload, + ) + + params = _build_connect_params( + GatewayConfig(url="ws://gateway.example/ws", token="secret-token"), + connect_nonce="nonce-xyz", + ) + + assert params["auth"] == {"token": "secret-token"} + assert params["client"]["id"] == DEFAULT_GATEWAY_CLIENT_ID + assert params["client"]["mode"] == DEFAULT_GATEWAY_CLIENT_MODE + assert params["device"] == {"id": "device-id", "nonce": "nonce-xyz"} + assert captured["client_id"] == DEFAULT_GATEWAY_CLIENT_ID + assert captured["client_mode"] == DEFAULT_GATEWAY_CLIENT_MODE + assert captured["role"] == "operator" + assert captured["scopes"] == list(GATEWAY_OPERATOR_SCOPES) + assert captured["auth_token"] == "secret-token" + assert captured["connect_nonce"] == "nonce-xyz" + + +@pytest.mark.parametrize( + ("gateway_url", "expected_origin"), + [ + ("ws://gateway.example/ws", "http://gateway.example"), + ("wss://gateway.example/ws", "https://gateway.example"), + ("ws://gateway.example:8080/ws", "http://gateway.example:8080"), + ("wss://gateway.example:8443/ws", "https://gateway.example:8443"), + ("ws://[::1]:8000/ws", "http://[::1]:8000"), + ], +) +def test_build_control_ui_origin(gateway_url: str, expected_origin: str) -> None: + assert _build_control_ui_origin(gateway_url) == expected_origin + + +@pytest.mark.asyncio +async def test_openclaw_call_uses_single_connect_attempt( + monkeypatch: pytest.MonkeyPatch, +) -> None: + call_count = 0 + + async def _fake_call_once( + method: str, + params: dict[str, object] | None, + *, + config: GatewayConfig, + gateway_url: str, + ) -> object: + nonlocal call_count + del method, params, config, gateway_url + call_count += 1 + return {"ok": True} + + monkeypatch.setattr(gateway_rpc, "_openclaw_call_once", _fake_call_once) + + payload = await openclaw_call( + "status", + config=GatewayConfig(url="ws://gateway.example/ws"), + ) + + assert payload == {"ok": True} + assert call_count == 1 + + +@pytest.mark.asyncio +async def test_openclaw_call_surfaces_scope_error_without_device_fallback( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _fake_call_once( + method: str, + params: dict[str, object] | None, + *, + config: GatewayConfig, + gateway_url: str, + ) -> object: + del method, params, config, gateway_url + raise OpenClawGatewayError("missing scope: operator.read") + + monkeypatch.setattr(gateway_rpc, "_openclaw_call_once", _fake_call_once) + + with pytest.raises(OpenClawGatewayError, match="missing scope: operator.read"): + await openclaw_call( + "status", + config=GatewayConfig(url="ws://gateway.example/ws", token="secret-token"), + ) + + +class _FakeConnectContext: + async def __aenter__(self) -> object: + return object() + + async def __aexit__(self, _exc_type: object, _exc: object, _tb: object) -> bool: + return False + + +@pytest.mark.asyncio +async def test_openclaw_call_once_does_not_pass_ssl_none_for_wss( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_connect(url: str, **kwargs: object) -> _FakeConnectContext: + captured["url"] = url + captured["kwargs"] = kwargs + return _FakeConnectContext() + + async def _fake_recv_first(_ws: object) -> None: + return None + + async def _fake_ensure_connected( + _ws: object, _first_message: object, _config: GatewayConfig + ) -> None: + return None + + async def _fake_send_request(_ws: object, _method: str, _params: object) -> object: + return {"ok": True} + + monkeypatch.setattr(gateway_rpc.websockets, "connect", _fake_connect) + monkeypatch.setattr(gateway_rpc, "_recv_first_message_or_none", _fake_recv_first) + monkeypatch.setattr(gateway_rpc, "_ensure_connected", _fake_ensure_connected) + monkeypatch.setattr(gateway_rpc, "_send_request", _fake_send_request) + + payload = await gateway_rpc._openclaw_call_once( + "status", + None, + config=GatewayConfig(url="wss://gateway.example/ws", allow_insecure_tls=False), + gateway_url="wss://gateway.example/ws", + ) + + assert payload == {"ok": True} + assert captured["url"] == "wss://gateway.example/ws" + kwargs = captured["kwargs"] + assert isinstance(kwargs, dict) + assert "ssl" not in kwargs + + +@pytest.mark.asyncio +async def test_openclaw_call_once_passes_ssl_context_for_insecure_wss( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_connect(url: str, **kwargs: object) -> _FakeConnectContext: + captured["url"] = url + captured["kwargs"] = kwargs + return _FakeConnectContext() + + async def _fake_recv_first(_ws: object) -> None: + return None + + async def _fake_ensure_connected( + _ws: object, _first_message: object, _config: GatewayConfig + ) -> None: + return None + + async def _fake_send_request(_ws: object, _method: str, _params: object) -> object: + return {"ok": True} + + monkeypatch.setattr(gateway_rpc.websockets, "connect", _fake_connect) + monkeypatch.setattr(gateway_rpc, "_recv_first_message_or_none", _fake_recv_first) + monkeypatch.setattr(gateway_rpc, "_ensure_connected", _fake_ensure_connected) + monkeypatch.setattr(gateway_rpc, "_send_request", _fake_send_request) + + payload = await gateway_rpc._openclaw_call_once( + "status", + None, + config=GatewayConfig(url="wss://gateway.example/ws", allow_insecure_tls=True), + gateway_url="wss://gateway.example/ws", + ) + + assert payload == {"ok": True} + assert captured["url"] == "wss://gateway.example/ws" + kwargs = captured["kwargs"] + assert isinstance(kwargs, dict) + assert kwargs.get("ssl") is not None diff --git a/backend/tests/test_gateway_ssl_context.py b/backend/tests/test_gateway_ssl_context.py new file mode 100644 index 00000000..b5c4b667 --- /dev/null +++ b/backend/tests/test_gateway_ssl_context.py @@ -0,0 +1,54 @@ +"""Tests for SSL/TLS configuration in gateway RPC connections.""" + +from __future__ import annotations + +import ssl + +from app.services.openclaw.gateway_rpc import GatewayConfig, _create_ssl_context + + +def test_create_ssl_context_returns_none_for_ws_protocol() -> None: + """SSL context should be None for non-secure websocket connections.""" + config = GatewayConfig(url="ws://gateway.example:18789/ws") + ssl_context = _create_ssl_context(config) + + assert ssl_context is None + + +def test_create_ssl_context_returns_none_for_wss_with_secure_mode() -> None: + """SSL context should be None for wss:// with default verification (secure mode).""" + config = GatewayConfig(url="wss://gateway.example:18789/ws", allow_insecure_tls=False) + ssl_context = _create_ssl_context(config) + + assert ssl_context is None + + +def test_create_ssl_context_disables_verification_when_allow_insecure_tls_true() -> None: + """SSL context should disable certificate verification when allow_insecure_tls is True.""" + config = GatewayConfig(url="wss://gateway.example:18789/ws", allow_insecure_tls=True) + ssl_context = _create_ssl_context(config) + + assert ssl_context is not None + assert isinstance(ssl_context, ssl.SSLContext) + assert ssl_context.check_hostname is False + assert ssl_context.verify_mode == ssl.CERT_NONE + + +def test_create_ssl_context_respects_localhost_with_insecure_flag() -> None: + """SSL context for localhost should respect allow_insecure_tls flag.""" + config = GatewayConfig(url="wss://localhost:18789/ws", allow_insecure_tls=True) + ssl_context = _create_ssl_context(config) + + assert ssl_context is not None + assert ssl_context.check_hostname is False + assert ssl_context.verify_mode == ssl.CERT_NONE + + +def test_create_ssl_context_respects_ip_address_with_insecure_flag() -> None: + """SSL context for IP addresses should respect allow_insecure_tls flag.""" + config = GatewayConfig(url="wss://192.168.1.100:18789/ws", allow_insecure_tls=True) + ssl_context = _create_ssl_context(config) + + assert ssl_context is not None + assert ssl_context.check_hostname is False + assert ssl_context.verify_mode == ssl.CERT_NONE diff --git a/backend/tests/test_gateway_version_compat.py b/backend/tests/test_gateway_version_compat.py index 37f177cb..5e6039d1 100644 --- a/backend/tests/test_gateway_version_compat.py +++ b/backend/tests/test_gateway_version_compat.py @@ -16,104 +16,216 @@ from app.services.openclaw.gateway_rpc import GatewayConfig, OpenClawGatewayErro from app.services.openclaw.session_service import GatewaySessionService -def test_extract_gateway_version_prefers_primary_path() -> None: +def test_extract_connect_server_version_uses_server_version_as_source_of_truth() -> None: payload = { - "gateway": {"version": "2026.2.1"}, - "protocolVersion": 3, - "meta": {"version": "2026.1.30"}, + "version": "dev", + "runtime": {"version": "2026.1.0"}, + "server": {"version": "2026.2.21-2"}, } - assert gateway_compat.extract_gateway_version(payload) == "2026.2.1" + assert gateway_compat.extract_connect_server_version(payload) == "2026.2.21-2" -def test_evaluate_gateway_version_detects_old_runtime() -> None: +def test_extract_connect_server_version_returns_none_when_server_version_missing() -> None: + payload = { + "version": "2026.2.21-2", + "runtime": {"version": "2026.2.21-2"}, + } + + assert gateway_compat.extract_connect_server_version(payload) is None + + +def test_extract_config_last_touched_version_reads_config_meta_last_touched_version() -> None: + payload = { + "config": { + "meta": {"lastTouchedVersion": "2026.2.9"}, + "wizard": {"lastRunVersion": "2026.2.8"}, + }, + "parsed": {"meta": {"lastTouchedVersion": "2026.2.7"}}, + } + + assert gateway_compat.extract_config_last_touched_version(payload) == "2026.2.9" + + +def test_extract_config_last_touched_version_returns_none_without_config_meta_last_touched_version() -> ( + None +): + payload = { + "config": {"wizard": {"lastRunVersion": "2026.2.9"}}, + } + + assert gateway_compat.extract_config_last_touched_version(payload) is None + + +@pytest.mark.parametrize( + ("current_version", "minimum_version", "expected_compatible"), + [ + ("2026.2.21", "2026.2.21", True), + ("2026.02.20", "2026.2.20", True), + ("2026.2.22", "2026.2.21", True), + ("2026.2.21-2", "2026.2.21-1", True), + ("2026.2.21-1", "2026.2.21-2", False), + ("2026.2.20", "2026.2.21", False), + ], +) +def test_evaluate_gateway_version_compares_calver( + *, + current_version: str, + minimum_version: str, + expected_compatible: bool, +) -> None: result = gateway_compat.evaluate_gateway_version( - current_version="2025.12.1", + current_version=current_version, + minimum_version=minimum_version, + ) + + assert result.compatible is expected_compatible + assert result.current_version == current_version + assert result.minimum_version == minimum_version + + +@pytest.mark.parametrize("invalid_current", ["dev", "latest", "2026.13.1", "2026.2.0-beta"]) +def test_evaluate_gateway_version_rejects_non_calver_current(invalid_current: str) -> None: + result = gateway_compat.evaluate_gateway_version( + current_version=invalid_current, minimum_version="2026.1.30", ) assert result.compatible is False - assert result.minimum_version == "2026.1.30" - assert "Minimum supported version is 2026.1.30" in (result.message or "") + assert result.current_version == invalid_current + assert "unsupported version format" in (result.message or "").lower() + + +def test_evaluate_gateway_version_rejects_non_calver_minimum_version() -> None: + result = gateway_compat.evaluate_gateway_version( + current_version="2026.2.21", + minimum_version="1.2.3", + ) + + assert result.compatible is False + assert result.minimum_version == "1.2.3" + assert "expected calver" in (result.message or "").lower() @pytest.mark.asyncio -async def test_check_gateway_runtime_compatibility_prefers_schema_version( +async def test_check_gateway_version_compatibility_uses_connect_server_version_only( monkeypatch: pytest.MonkeyPatch, ) -> None: - calls: list[str] = [] + async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None: + _ = config + return { + "version": "dev", + "runtime": {"version": "2026.1.0"}, + "server": {"version": "2026.2.13"}, + } async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object: - _ = (params, config) - calls.append(method) - if method == "config.schema": - return {"version": "2026.2.13"} - raise AssertionError(f"unexpected method: {method}") + _ = (method, params, config) + raise AssertionError("config.get fallback should not run for valid connect version") + monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata) monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call) - result = await gateway_compat.check_gateway_runtime_compatibility( + result = await gateway_compat.check_gateway_version_compatibility( GatewayConfig(url="ws://gateway.example/ws"), minimum_version="2026.1.30", ) - assert calls == ["config.schema"] assert result.compatible is True assert result.current_version == "2026.2.13" @pytest.mark.asyncio -async def test_check_gateway_runtime_compatibility_falls_back_to_health( +async def test_check_gateway_version_compatibility_fails_without_server_version( monkeypatch: pytest.MonkeyPatch, ) -> None: - calls: list[str] = [] + async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None: + _ = config + return {"runtime": {"version": "2026.2.13"}} async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object: _ = (params, config) - calls.append(method) - if method == "config.schema": - raise OpenClawGatewayError("unknown method") - if method == "status": - raise OpenClawGatewayError("unknown method") - return {"version": "2026.2.0"} + assert method == "config.get" + return {"config": {}} + monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata) monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call) - result = await gateway_compat.check_gateway_runtime_compatibility( + result = await gateway_compat.check_gateway_version_compatibility( GatewayConfig(url="ws://gateway.example/ws"), minimum_version="2026.1.30", ) - assert calls == ["config.schema", "status", "health"] - assert result.compatible is True - assert result.current_version == "2026.2.0" + assert result.compatible is False + assert result.current_version is None + assert "unable to determine gateway version" in (result.message or "").lower() @pytest.mark.asyncio -async def test_check_gateway_runtime_compatibility_uses_health_when_status_has_no_version( +async def test_check_gateway_version_compatibility_uses_config_get_fallback_when_connect_is_dev( monkeypatch: pytest.MonkeyPatch, ) -> None: - calls: list[str] = [] + async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None: + _ = config + return {"server": {"version": "dev"}} async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object: _ = (params, config) - calls.append(method) - if method == "config.schema": - return {"schema": {"title": "Gateway schema"}} - if method == "status": - return {"uptime": 1234} - return {"version": "2026.2.0"} + assert method == "config.get" + return {"config": {"meta": {"lastTouchedVersion": "2026.2.9"}}} + monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata) monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call) - result = await gateway_compat.check_gateway_runtime_compatibility( + result = await gateway_compat.check_gateway_version_compatibility( GatewayConfig(url="ws://gateway.example/ws"), minimum_version="2026.1.30", ) - assert calls == ["config.schema", "status", "health"] assert result.compatible is True - assert result.current_version == "2026.2.0" + assert result.current_version == "2026.2.9" + + +@pytest.mark.asyncio +async def test_check_gateway_version_compatibility_rejects_non_calver_server_version_when_fallback_unavailable( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None: + _ = config + return {"server": {"version": "dev"}} + + async def _fake_openclaw_call(method: str, params: object = None, *, config: object) -> object: + _ = (method, params, config) + raise OpenClawGatewayError("method unavailable") + + monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata) + monkeypatch.setattr(gateway_compat, "openclaw_call", _fake_openclaw_call) + + result = await gateway_compat.check_gateway_version_compatibility( + GatewayConfig(url="ws://gateway.example/ws"), + minimum_version="2026.1.30", + ) + + assert result.compatible is False + assert result.current_version == "dev" + assert "unsupported version format" in (result.message or "").lower() + + +@pytest.mark.asyncio +async def test_check_gateway_version_compatibility_propagates_connect_errors( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _fake_connect_metadata(*, config: GatewayConfig) -> object | None: + _ = config + raise OpenClawGatewayError("connection refused") + + monkeypatch.setattr(gateway_compat, "openclaw_connect_metadata", _fake_connect_metadata) + + with pytest.raises(OpenClawGatewayError, match="connection refused"): + await gateway_compat.check_gateway_version_compatibility( + GatewayConfig(url="ws://gateway.example/ws"), + minimum_version="2026.1.30", + ) @pytest.mark.asyncio @@ -129,7 +241,7 @@ async def test_admin_service_rejects_incompatible_gateway( message="Gateway version 2026.1.0 is not supported.", ) - monkeypatch.setattr(admin_service, "check_gateway_runtime_compatibility", _fake_check) + monkeypatch.setattr(admin_service, "check_gateway_version_compatibility", _fake_check) service = GatewayAdminLifecycleService(session=object()) # type: ignore[arg-type] with pytest.raises(HTTPException) as exc_info: @@ -147,7 +259,7 @@ async def test_admin_service_maps_gateway_transport_errors( _ = (config, minimum_version) raise OpenClawGatewayError("connection refused") - monkeypatch.setattr(admin_service, "check_gateway_runtime_compatibility", _fake_check) + monkeypatch.setattr(admin_service, "check_gateway_version_compatibility", _fake_check) service = GatewayAdminLifecycleService(session=object()) # type: ignore[arg-type] with pytest.raises(HTTPException) as exc_info: @@ -157,6 +269,24 @@ async def test_admin_service_maps_gateway_transport_errors( assert "compatibility check failed" in str(exc_info.value.detail).lower() +@pytest.mark.asyncio +async def test_admin_service_maps_gateway_scope_errors_with_guidance( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _fake_check(config: GatewayConfig, *, minimum_version: str | None = None) -> object: + _ = (config, minimum_version) + raise OpenClawGatewayError("missing scope: operator.read") + + monkeypatch.setattr(admin_service, "check_gateway_version_compatibility", _fake_check) + + service = GatewayAdminLifecycleService(session=object()) # type: ignore[arg-type] + with pytest.raises(HTTPException) as exc_info: + await service.assert_gateway_runtime_compatible(url="ws://gateway.example/ws", token=None) + + assert exc_info.value.status_code == 502 + assert "missing required scope `operator.read`" in str(exc_info.value.detail) + + @pytest.mark.asyncio async def test_gateway_status_reports_incompatible_version( monkeypatch: pytest.MonkeyPatch, @@ -170,7 +300,7 @@ async def test_gateway_status_reports_incompatible_version( message="Gateway version 2026.1.0 is not supported.", ) - monkeypatch.setattr(session_service, "check_gateway_runtime_compatibility", _fake_check) + monkeypatch.setattr(session_service, "check_gateway_version_compatibility", _fake_check) service = GatewaySessionService(session=object()) # type: ignore[arg-type] response = await service.get_status( @@ -183,6 +313,28 @@ async def test_gateway_status_reports_incompatible_version( assert response.error == "Gateway version 2026.1.0 is not supported." +@pytest.mark.asyncio +async def test_gateway_status_surfaces_scope_error_guidance( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _fake_check(config: GatewayConfig, *, minimum_version: str | None = None) -> object: + _ = (config, minimum_version) + raise OpenClawGatewayError("missing scope: operator.read") + + monkeypatch.setattr(session_service, "check_gateway_version_compatibility", _fake_check) + + service = GatewaySessionService(session=object()) # type: ignore[arg-type] + response = await service.get_status( + params=GatewayResolveQuery(gateway_url="ws://gateway.example/ws"), + organization_id=uuid4(), + user=None, + ) + + assert response.connected is False + assert response.error is not None + assert "missing required scope `operator.read`" in response.error + + @pytest.mark.asyncio async def test_gateway_status_returns_sessions_when_version_compatible( monkeypatch: pytest.MonkeyPatch, @@ -201,7 +353,7 @@ async def test_gateway_status_returns_sessions_when_version_compatible( assert method == "sessions.list" return {"sessions": [{"key": "agent:main"}]} - monkeypatch.setattr(session_service, "check_gateway_runtime_compatibility", _fake_check) + monkeypatch.setattr(session_service, "check_gateway_version_compatibility", _fake_check) monkeypatch.setattr(session_service, "openclaw_call", _fake_openclaw_call) service = GatewaySessionService(session=object()) # type: ignore[arg-type] diff --git a/backend/tests/test_lifecycle_reconcile_queue.py b/backend/tests/test_lifecycle_reconcile_queue.py new file mode 100644 index 00000000..ab181cbc --- /dev/null +++ b/backend/tests/test_lifecycle_reconcile_queue.py @@ -0,0 +1,126 @@ +# ruff: noqa: INP001 +"""Queue payload helpers for lifecycle reconcile tasks.""" + +from __future__ import annotations + +from datetime import timedelta +from uuid import uuid4 + +import pytest + +from app.core.time import utcnow +from app.services.openclaw.lifecycle_queue import ( + QueuedAgentLifecycleReconcile, + decode_lifecycle_task, + defer_lifecycle_reconcile, + enqueue_lifecycle_reconcile, +) +from app.services.queue import QueuedTask + + +def test_enqueue_lifecycle_reconcile_uses_delayed_enqueue( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_enqueue_with_delay( + task: QueuedTask, + queue_name: str, + *, + delay_seconds: float, + redis_url: str | None = None, + ) -> bool: + captured["task"] = task + captured["queue_name"] = queue_name + captured["delay_seconds"] = delay_seconds + captured["redis_url"] = redis_url + return True + + monkeypatch.setattr( + "app.services.openclaw.lifecycle_queue.enqueue_task_with_delay", + _fake_enqueue_with_delay, + ) + + payload = QueuedAgentLifecycleReconcile( + agent_id=uuid4(), + gateway_id=uuid4(), + board_id=uuid4(), + generation=7, + checkin_deadline_at=utcnow() + timedelta(seconds=30), + attempts=0, + ) + + assert enqueue_lifecycle_reconcile(payload) is True + task = captured["task"] + assert isinstance(task, QueuedTask) + assert task.task_type == "agent_lifecycle_reconcile" + assert float(captured["delay_seconds"]) > 0 + + +def test_defer_lifecycle_reconcile_keeps_attempt_count( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_enqueue_with_delay( + task: QueuedTask, + queue_name: str, + *, + delay_seconds: float, + redis_url: str | None = None, + ) -> bool: + captured["task"] = task + captured["queue_name"] = queue_name + captured["delay_seconds"] = delay_seconds + captured["redis_url"] = redis_url + return True + + monkeypatch.setattr( + "app.services.openclaw.lifecycle_queue.enqueue_task_with_delay", + _fake_enqueue_with_delay, + ) + deadline = utcnow() + timedelta(minutes=1) + task = QueuedTask( + task_type="agent_lifecycle_reconcile", + payload={ + "agent_id": str(uuid4()), + "gateway_id": str(uuid4()), + "board_id": None, + "generation": 3, + "checkin_deadline_at": deadline.isoformat(), + }, + created_at=utcnow(), + attempts=2, + ) + assert defer_lifecycle_reconcile(task, delay_seconds=12) is True + deferred_task = captured["task"] + assert isinstance(deferred_task, QueuedTask) + assert deferred_task.attempts == 2 + assert float(captured["delay_seconds"]) == 12 + + +def test_decode_lifecycle_task_roundtrip() -> None: + deadline = utcnow() + timedelta(minutes=3) + agent_id = uuid4() + gateway_id = uuid4() + board_id = uuid4() + task = QueuedTask( + task_type="agent_lifecycle_reconcile", + payload={ + "agent_id": str(agent_id), + "gateway_id": str(gateway_id), + "board_id": str(board_id), + "generation": 5, + "checkin_deadline_at": deadline.isoformat(), + }, + created_at=utcnow(), + attempts=1, + ) + + decoded = decode_lifecycle_task(task) + assert decoded.agent_id == agent_id + assert decoded.gateway_id == gateway_id + assert decoded.board_id == board_id + assert decoded.generation == 5 + assert decoded.checkin_deadline_at == deadline + assert decoded.attempts == 1 diff --git a/backend/tests/test_lifecycle_reconcile_state.py b/backend/tests/test_lifecycle_reconcile_state.py new file mode 100644 index 00000000..b1c36cba --- /dev/null +++ b/backend/tests/test_lifecycle_reconcile_state.py @@ -0,0 +1,53 @@ +# ruff: noqa: INP001 +"""Lifecycle reconcile state helpers.""" + +from __future__ import annotations + +from datetime import timedelta +from uuid import uuid4 + +from app.core.time import utcnow +from app.models.agents import Agent +from app.services.openclaw.constants import ( + CHECKIN_DEADLINE_AFTER_WAKE, + MAX_WAKE_ATTEMPTS_WITHOUT_CHECKIN, +) +from app.services.openclaw.lifecycle_reconcile import _has_checked_in_since_wake + + +def _agent(*, last_seen_offset_s: int | None, last_wake_offset_s: int | None) -> Agent: + now = utcnow() + return Agent( + name="reconcile-test", + gateway_id=uuid4(), + last_seen_at=( + (now + timedelta(seconds=last_seen_offset_s)) + if last_seen_offset_s is not None + else None + ), + last_wake_sent_at=( + (now + timedelta(seconds=last_wake_offset_s)) + if last_wake_offset_s is not None + else None + ), + ) + + +def test_checked_in_since_wake_when_last_seen_after_wake() -> None: + agent = _agent(last_seen_offset_s=5, last_wake_offset_s=0) + assert _has_checked_in_since_wake(agent) is True + + +def test_not_checked_in_since_wake_when_last_seen_before_wake() -> None: + agent = _agent(last_seen_offset_s=-5, last_wake_offset_s=0) + assert _has_checked_in_since_wake(agent) is False + + +def test_not_checked_in_since_wake_when_missing_last_seen() -> None: + agent = _agent(last_seen_offset_s=None, last_wake_offset_s=0) + assert _has_checked_in_since_wake(agent) is False + + +def test_lifecycle_convergence_policy_constants() -> None: + assert CHECKIN_DEADLINE_AFTER_WAKE == timedelta(seconds=30) + assert MAX_WAKE_ATTEMPTS_WITHOUT_CHECKIN == 3 diff --git a/backend/tests/test_metrics_kpis.py b/backend/tests/test_metrics_kpis.py new file mode 100644 index 00000000..f346a140 --- /dev/null +++ b/backend/tests/test_metrics_kpis.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import uuid4 + +import pytest + +from app.api import metrics as metrics_api +from app.models.approvals import Approval +from app.models.boards import Board +from app.models.tasks import Task + + +class _ExecResult: + def __init__(self, rows: list[tuple[str, int]]) -> None: + self._rows = rows + + def all(self) -> list[tuple[str, int]]: + return self._rows + + +class _FakeSession: + def __init__(self, rows: list[tuple[str, int]]) -> None: + self._rows = rows + + async def exec(self, _statement: object) -> _ExecResult: + return _ExecResult(self._rows) + + +class _ExecOneResult: + def __init__(self, value: int) -> None: + self._value = value + + def one(self) -> int: + return self._value + + +class _ExecAllResult: + def __init__(self, rows: list[tuple[object, ...]]) -> None: + self._rows = rows + + def all(self) -> list[tuple[object, ...]]: + return self._rows + + +class _SequentialSession: + def __init__(self, responses: list[object]) -> None: + self._responses = responses + self._index = 0 + + async def exec(self, _statement: object) -> object: + response = self._responses[self._index] + self._index += 1 + return response + + +@pytest.mark.asyncio +async def test_task_status_counts_returns_zeroes_for_empty_board_scope() -> None: + counts = await metrics_api._task_status_counts(_FakeSession([]), []) + + assert counts == { + "inbox": 0, + "in_progress": 0, + "review": 0, + "done": 0, + } + + +@pytest.mark.asyncio +async def test_task_status_counts_maps_known_statuses() -> None: + session = _FakeSession( + [ + ("inbox", 4), + ("in_progress", 3), + ("review", 2), + ("done", 7), + ("blocked", 99), + ], + ) + + counts = await metrics_api._task_status_counts(session, [uuid4()]) + + assert counts == { + "inbox": 4, + "in_progress": 3, + "review": 2, + "done": 7, + } + + +@pytest.mark.asyncio +async def test_pending_approvals_snapshot_returns_empty_for_empty_scope() -> None: + snapshot = await metrics_api._pending_approvals_snapshot(_SequentialSession([]), []) + + assert snapshot.total == 0 + assert snapshot.items == [] + + +@pytest.mark.asyncio +async def test_pending_approvals_snapshot_maps_rows() -> None: + approval_id = uuid4() + board_id = uuid4() + organization_id = uuid4() + task_id = uuid4() + created_at = datetime(2026, 3, 4, 12, 0, 0) + approval = Approval( + id=approval_id, + board_id=board_id, + task_id=task_id, + action_type="approve_task", + confidence=87.0, + created_at=created_at, + status="pending", + ) + board = Board( + id=board_id, + organization_id=organization_id, + name="Operations Board", + slug="operations-board", + ) + task = Task( + id=task_id, + board_id=board_id, + title="Validate rollout checklist", + ) + rows: list[tuple[object, ...]] = [ + ( + approval, + board, + task, + ) + ] + session = _SequentialSession( + [ + _ExecOneResult(3), + _ExecAllResult(rows), + ] + ) + + snapshot = await metrics_api._pending_approvals_snapshot(session, [board_id], limit=10) + + assert snapshot.total == 3 + assert len(snapshot.items) == 1 + item = snapshot.items[0] + assert item.approval_id == approval_id + assert item.board_id == board_id + assert item.board_name == "Operations Board" + assert item.action_type == "approve_task" + assert item.confidence == 87.0 + assert item.created_at == created_at + assert item.task_title == "Validate rollout checklist" diff --git a/backend/tests/test_openapi_agent_webhook_payload_endpoint.py b/backend/tests/test_openapi_agent_webhook_payload_endpoint.py new file mode 100644 index 00000000..f84fc95a --- /dev/null +++ b/backend/tests/test_openapi_agent_webhook_payload_endpoint.py @@ -0,0 +1,16 @@ +# ruff: noqa: INP001, S101 + +from __future__ import annotations + +from app.main import app + + +def test_openapi_includes_agent_webhook_payload_read_endpoint() -> None: + schema = app.openapi() + + path = "/api/v1/agent/boards/{board_id}/webhooks/{webhook_id}/payloads/{payload_id}" + assert path in schema["paths"] + op = schema["paths"][path]["get"] + tags = set(op.get("tags", [])) + assert "agent-worker" in tags + assert op.get("x-llm-intent") == "agent_board_webhook_payload_read" diff --git a/backend/tests/test_openapi_blocked_task_error_schema.py b/backend/tests/test_openapi_blocked_task_error_schema.py new file mode 100644 index 00000000..d9d7cb16 --- /dev/null +++ b/backend/tests/test_openapi_blocked_task_error_schema.py @@ -0,0 +1,34 @@ +# ruff: noqa: INP001, S101 + +from __future__ import annotations + +from app.main import app + + +def test_openapi_blocked_task_error_includes_code_field() -> None: + schema = app.openapi() + + blocked_detail = schema["components"]["schemas"]["BlockedTaskDetail"] + props = blocked_detail.get("properties", {}) + + # `code` is optional but must be documented for clients. + assert "code" in props + required_fields = blocked_detail.get("required", []) + assert "code" not in required_fields + + code_schema = props["code"] + any_of = code_schema.get("anyOf") + if any_of: + assert isinstance(any_of, list) + + has_string_branch = any(branch.get("type") == "string" for branch in any_of) + assert has_string_branch + + has_null_branch = any( + branch.get("type") == "null" or branch.get("nullable") is True for branch in any_of + ) + assert has_null_branch + else: + # Alternative encoding used by some schema versions for Optional[str]. + assert code_schema.get("type") == "string" + assert code_schema.get("nullable") is True diff --git a/backend/tests/test_queue_worker_lifecycle_handler.py b/backend/tests/test_queue_worker_lifecycle_handler.py new file mode 100644 index 00000000..8d4b8740 --- /dev/null +++ b/backend/tests/test_queue_worker_lifecycle_handler.py @@ -0,0 +1,11 @@ +# ruff: noqa: INP001 +"""Queue worker registration tests for lifecycle reconcile tasks.""" + +from __future__ import annotations + +from app.services.openclaw.lifecycle_queue import TASK_TYPE as LIFECYCLE_TASK_TYPE +from app.services.queue_worker import _TASK_HANDLERS + + +def test_worker_registers_lifecycle_reconcile_handler() -> None: + assert LIFECYCLE_TASK_TYPE in _TASK_HANDLERS diff --git a/backend/tests/test_security_headers_middleware.py b/backend/tests/test_security_headers_middleware.py new file mode 100644 index 00000000..000e03c8 --- /dev/null +++ b/backend/tests/test_security_headers_middleware.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import pytest +from fastapi import FastAPI, Response +from fastapi.middleware.cors import CORSMiddleware +from fastapi.testclient import TestClient + +from app.core.security_headers import SecurityHeadersMiddleware + + +@pytest.mark.asyncio +async def test_security_headers_middleware_passes_through_non_http_scope() -> None: + called = False + + async def app(scope, receive, send): # type: ignore[no-untyped-def] + _ = receive + _ = send + nonlocal called + called = scope["type"] == "websocket" + + middleware = SecurityHeadersMiddleware(app, x_frame_options="SAMEORIGIN") + await middleware({"type": "websocket", "headers": []}, lambda: None, lambda _: None) + + assert called is True + + +@pytest.mark.asyncio +async def test_security_headers_middleware_appends_lowercase_raw_header_names() -> None: + sent_messages: list[dict[str, object]] = [] + + async def app(scope, receive, send): # type: ignore[no-untyped-def] + _ = scope + _ = receive + await send({"type": "http.response.start", "status": 200, "headers": []}) + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + async def capture(message): # type: ignore[no-untyped-def] + sent_messages.append(message) + + middleware = SecurityHeadersMiddleware(app, x_frame_options="SAMEORIGIN") + await middleware( + {"type": "http", "method": "GET", "path": "/", "headers": []}, lambda: None, capture + ) + + response_start = next( + message for message in sent_messages if message.get("type") == "http.response.start" + ) + headers = response_start.get("headers") + assert isinstance(headers, list) + header_names = {name for name, _value in headers} + assert b"x-frame-options" in header_names + assert b"X-Frame-Options" not in header_names + + +def test_security_headers_middleware_injects_configured_headers() -> None: + app = FastAPI() + app.add_middleware( + SecurityHeadersMiddleware, + x_content_type_options="nosniff", + x_frame_options="SAMEORIGIN", + referrer_policy="strict-origin-when-cross-origin", + permissions_policy="camera=(), microphone=(), geolocation=()", + ) + + @app.get("/ok") + def ok() -> dict[str, bool]: + return {"ok": True} + + response = TestClient(app).get("/ok") + + assert response.status_code == 200 + assert response.headers["x-content-type-options"] == "nosniff" + assert response.headers["x-frame-options"] == "SAMEORIGIN" + assert response.headers["referrer-policy"] == "strict-origin-when-cross-origin" + assert response.headers["permissions-policy"] == "camera=(), microphone=(), geolocation=()" + + +def test_security_headers_middleware_does_not_override_existing_values() -> None: + app = FastAPI() + app.add_middleware( + SecurityHeadersMiddleware, + x_content_type_options="nosniff", + x_frame_options="SAMEORIGIN", + referrer_policy="strict-origin-when-cross-origin", + permissions_policy="camera=(), microphone=(), geolocation=()", + ) + + @app.get("/already-set") + def already_set(response: Response) -> dict[str, bool]: + response.headers["X-Frame-Options"] = "ALLOWALL" + response.headers["Referrer-Policy"] = "unsafe-url" + return {"ok": True} + + response = TestClient(app).get("/already-set") + + assert response.status_code == 200 + assert response.headers["x-content-type-options"] == "nosniff" + assert response.headers["x-frame-options"] == "ALLOWALL" + assert response.headers["referrer-policy"] == "unsafe-url" + assert response.headers["permissions-policy"] == "camera=(), microphone=(), geolocation=()" + + +def test_security_headers_middleware_includes_headers_on_cors_preflight() -> None: + app = FastAPI() + app.add_middleware( + CORSMiddleware, + allow_origins=["https://example.com"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + app.add_middleware( + SecurityHeadersMiddleware, + x_content_type_options="nosniff", + ) + + @app.get("/ok") + def ok() -> dict[str, bool]: + return {"ok": True} + + response = TestClient(app).options( + "/ok", + headers={ + "Origin": "https://example.com", + "Access-Control-Request-Method": "GET", + }, + ) + + assert response.status_code == 200 + assert response.headers["x-content-type-options"] == "nosniff" + + +def test_security_headers_middleware_skips_blank_config_values() -> None: + app = FastAPI() + app.add_middleware(SecurityHeadersMiddleware) + + @app.get("/ok") + def ok() -> dict[str, bool]: + return {"ok": True} + + response = TestClient(app).get("/ok") + + assert response.status_code == 200 + assert response.headers.get("x-content-type-options") is None + assert response.headers.get("x-frame-options") is None + assert response.headers.get("referrer-policy") is None + assert response.headers.get("permissions-policy") is None diff --git a/backend/tests/test_task_agent_permissions.py b/backend/tests/test_task_agent_permissions.py index 44b3b6e2..70a1172d 100644 --- a/backend/tests/test_task_agent_permissions.py +++ b/backend/tests/test_task_agent_permissions.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import Any from uuid import uuid4 import pytest @@ -11,6 +12,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession from app.api import tasks as tasks_api from app.api.deps import ActorContext from app.core.time import utcnow +from app.models.activity_events import ActivityEvent from app.models.agents import Agent from app.models.boards import Board from app.models.gateways import Gateway @@ -326,7 +328,7 @@ async def test_non_lead_agent_forbidden_for_lead_only_patch_fields() -> None: @pytest.mark.asyncio -async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None: +async def test_non_lead_agent_moves_task_to_review_and_reassigns_to_lead() -> None: engine = await _make_engine() try: async with await _make_session(engine) as session: @@ -334,6 +336,7 @@ async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None: board_id = uuid4() gateway_id = uuid4() worker_id = uuid4() + lead_id = uuid4() task_id = uuid4() in_progress_at = utcnow() @@ -365,6 +368,16 @@ async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None: status="online", ), ) + session.add( + Agent( + id=lead_id, + name="Lead Agent", + board_id=board_id, + gateway_id=gateway_id, + status="online", + is_board_lead=True, + ), + ) session.add( Task( id=task_id, @@ -391,7 +404,7 @@ async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None: ) assert updated.status == "review" - assert updated.assigned_agent_id is None + assert updated.assigned_agent_id == lead_id assert updated.in_progress_at is None refreshed_task = ( @@ -399,6 +412,268 @@ async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None: ).first() assert refreshed_task is not None assert refreshed_task.previous_in_progress_at == in_progress_at + assert refreshed_task.assigned_agent_id == lead_id + finally: + await engine.dispose() + + +@pytest.mark.asyncio +async def test_non_lead_agent_move_to_review_reassigns_to_lead_and_sends_review_message( + monkeypatch: pytest.MonkeyPatch, +) -> None: + engine = await _make_engine() + try: + async with await _make_session(engine) as session: + org_id = uuid4() + board_id = uuid4() + gateway_id = uuid4() + worker_id = uuid4() + lead_id = uuid4() + task_id = uuid4() + + session.add(Organization(id=org_id, name="org")) + session.add( + Gateway( + id=gateway_id, + organization_id=org_id, + name="gateway", + url="https://gateway.local", + workspace_root="/tmp/workspace", + ), + ) + session.add( + Board( + id=board_id, + organization_id=org_id, + name="board", + slug="board", + gateway_id=gateway_id, + ), + ) + session.add( + Agent( + id=worker_id, + name="worker", + board_id=board_id, + gateway_id=gateway_id, + status="online", + ), + ) + session.add( + Agent( + id=lead_id, + name="Lead Agent", + board_id=board_id, + gateway_id=gateway_id, + status="online", + is_board_lead=True, + openclaw_session_id="lead-session", + ), + ) + session.add( + Task( + id=task_id, + board_id=board_id, + title="assigned task", + description="done and ready", + status="in_progress", + assigned_agent_id=worker_id, + in_progress_at=utcnow(), + ), + ) + await session.commit() + + sent: dict[str, str] = {} + + class _FakeDispatch: + def __init__(self, _session: AsyncSession) -> None: + pass + + async def optional_gateway_config_for_board(self, _board: Board) -> object: + return object() + + async def _fake_send_agent_task_message( + *, + dispatch: Any, + session_key: str, + config: Any, + agent_name: str, + message: str, + ) -> None: + _ = dispatch, config + sent["session_key"] = session_key + sent["agent_name"] = agent_name + sent["message"] = message + return None + + monkeypatch.setattr(tasks_api, "GatewayDispatchService", _FakeDispatch) + monkeypatch.setattr( + tasks_api, "_send_agent_task_message", _fake_send_agent_task_message + ) + + task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() + assert task is not None + actor = (await session.exec(select(Agent).where(col(Agent.id) == worker_id))).first() + assert actor is not None + + updated = await tasks_api.update_task( + payload=TaskUpdate(status="review", comment="Moving to review."), + task=task, + session=session, + actor=ActorContext(actor_type="agent", agent=actor), + ) + + assert updated.status == "review" + assert updated.assigned_agent_id == lead_id + assert sent["session_key"] == "lead-session" + assert sent["agent_name"] == "Lead Agent" + assert "TASK READY FOR LEAD REVIEW" in sent["message"] + assert "review the deliverables" in sent["message"] + finally: + await engine.dispose() + + +@pytest.mark.asyncio +async def test_lead_moves_review_task_to_inbox_and_reassigns_last_worker_with_rework_message( + monkeypatch: pytest.MonkeyPatch, +) -> None: + engine = await _make_engine() + try: + async with await _make_session(engine) as session: + org_id = uuid4() + board_id = uuid4() + gateway_id = uuid4() + worker_id = uuid4() + lead_id = uuid4() + task_id = uuid4() + + session.add(Organization(id=org_id, name="org")) + session.add( + Gateway( + id=gateway_id, + organization_id=org_id, + name="gateway", + url="https://gateway.local", + workspace_root="/tmp/workspace", + ), + ) + session.add( + Board( + id=board_id, + organization_id=org_id, + name="board", + slug="board", + gateway_id=gateway_id, + ), + ) + session.add( + Agent( + id=worker_id, + name="worker", + board_id=board_id, + gateway_id=gateway_id, + status="online", + openclaw_session_id="worker-session", + ), + ) + session.add( + Agent( + id=lead_id, + name="Lead Agent", + board_id=board_id, + gateway_id=gateway_id, + status="online", + is_board_lead=True, + openclaw_session_id="lead-session", + ), + ) + session.add( + Task( + id=task_id, + board_id=board_id, + title="assigned task", + description="ready", + status="in_progress", + assigned_agent_id=worker_id, + in_progress_at=utcnow(), + ), + ) + await session.commit() + + sent: list[dict[str, str]] = [] + + class _FakeDispatch: + def __init__(self, _session: AsyncSession) -> None: + pass + + async def optional_gateway_config_for_board(self, _board: Board) -> object: + return object() + + async def _fake_send_agent_task_message( + *, + dispatch: Any, + session_key: str, + config: Any, + agent_name: str, + message: str, + ) -> None: + _ = dispatch, config + sent.append( + { + "session_key": session_key, + "agent_name": agent_name, + "message": message, + }, + ) + return None + + monkeypatch.setattr(tasks_api, "GatewayDispatchService", _FakeDispatch) + monkeypatch.setattr( + tasks_api, "_send_agent_task_message", _fake_send_agent_task_message + ) + + task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() + assert task is not None + worker = (await session.exec(select(Agent).where(col(Agent.id) == worker_id))).first() + assert worker is not None + lead = (await session.exec(select(Agent).where(col(Agent.id) == lead_id))).first() + assert lead is not None + + moved_to_review = await tasks_api.update_task( + payload=TaskUpdate(status="review", comment="Ready for review."), + task=task, + session=session, + actor=ActorContext(actor_type="agent", agent=worker), + ) + assert moved_to_review.status == "review" + assert moved_to_review.assigned_agent_id == lead_id + + session.add( + ActivityEvent( + event_type="task.comment", + task_id=task_id, + agent_id=lead_id, + message="Please update error handling and add tests for edge cases.", + ), + ) + await session.commit() + + review_task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() + assert review_task is not None + reverted = await tasks_api.update_task( + payload=TaskUpdate(status="inbox"), + task=review_task, + session=session, + actor=ActorContext(actor_type="agent", agent=lead), + ) + + assert reverted.status == "inbox" + assert reverted.assigned_agent_id == worker_id + worker_messages = [item for item in sent if item["session_key"] == "worker-session"] + assert worker_messages + final_message = worker_messages[-1]["message"] + assert "CHANGES REQUESTED" in final_message + assert "Please update error handling and add tests for edge cases." in final_message finally: await engine.dispose() @@ -485,7 +760,91 @@ async def test_non_lead_agent_comment_in_review_without_status_does_not_reassign @pytest.mark.asyncio -async def test_non_lead_agent_moves_to_review_without_comment_or_recent_comment_fails() -> None: +async def test_non_lead_agent_moves_to_review_without_comment_when_rule_disabled() -> None: + engine = await _make_engine() + try: + async with await _make_session(engine) as session: + org_id = uuid4() + board_id = uuid4() + gateway_id = uuid4() + worker_id = uuid4() + lead_id = uuid4() + task_id = uuid4() + + session.add(Organization(id=org_id, name="org")) + session.add( + Gateway( + id=gateway_id, + organization_id=org_id, + name="gateway", + url="https://gateway.local", + workspace_root="/tmp/workspace", + ), + ) + session.add( + Board( + id=board_id, + organization_id=org_id, + name="board", + slug="board", + gateway_id=gateway_id, + comment_required_for_review=False, + ), + ) + session.add( + Agent( + id=worker_id, + name="worker", + board_id=board_id, + gateway_id=gateway_id, + status="online", + ), + ) + session.add( + Agent( + id=lead_id, + name="Lead Agent", + board_id=board_id, + gateway_id=gateway_id, + status="online", + is_board_lead=True, + ), + ) + session.add( + Task( + id=task_id, + board_id=board_id, + title="assigned task", + description="", + status="in_progress", + assigned_agent_id=worker_id, + in_progress_at=utcnow(), + ), + ) + await session.commit() + + task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() + assert task is not None + actor = (await session.exec(select(Agent).where(col(Agent.id) == worker_id))).first() + assert actor is not None + + updated = await tasks_api.update_task( + payload=TaskUpdate(status="review"), + task=task, + session=session, + actor=ActorContext(actor_type="agent", agent=actor), + ) + + assert updated.status == "review" + assert updated.assigned_agent_id == lead_id + finally: + await engine.dispose() + + +@pytest.mark.asyncio +async def test_non_lead_agent_moves_to_review_without_comment_or_recent_comment_fails_when_rule_enabled() -> ( + None +): engine = await _make_engine() try: async with await _make_session(engine) as session: @@ -512,6 +871,7 @@ async def test_non_lead_agent_moves_to_review_without_comment_or_recent_comment_ name="board", slug="board", gateway_id=gateway_id, + comment_required_for_review=True, ), ) session.add( diff --git a/backend/tests/test_tasks_blocked_lead_dependency_only.py b/backend/tests/test_tasks_blocked_lead_dependency_only.py new file mode 100644 index 00000000..82da4bac --- /dev/null +++ b/backend/tests/test_tasks_blocked_lead_dependency_only.py @@ -0,0 +1,138 @@ +# ruff: noqa: INP001 + +from __future__ import annotations + +from uuid import uuid4 + +import pytest +from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine +from sqlmodel import SQLModel, col, select +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.api.deps import ActorContext +from app.api.tasks import _apply_lead_task_update, _TaskUpdateInput +from app.models.agents import Agent +from app.models.boards import Board +from app.models.organizations import Organization +from app.models.task_dependencies import TaskDependency +from app.models.tasks import Task +from app.services.task_dependencies import blocked_by_for_task + + +async def _make_engine() -> AsyncEngine: + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + async with engine.connect() as conn, conn.begin(): + await conn.run_sync(SQLModel.metadata.create_all) + return engine + + +async def _make_session(engine: AsyncEngine) -> AsyncSession: + return AsyncSession(engine, expire_on_commit=False) + + +@pytest.mark.asyncio +async def test_lead_dependency_only_update_allowed_when_task_blocked() -> None: + """Leads may update dependencies even if the task is currently blocked. + + This supports unblocking work by adjusting dependency graphs, while still + rejecting status/assignee transitions. + """ + + engine = await _make_engine() + try: + async with await _make_session(engine) as session: + org_id = uuid4() + board_id = uuid4() + lead_id = uuid4() + dep_id = uuid4() + task_id = uuid4() + + session.add(Organization(id=org_id, name="org")) + session.add(Board(id=board_id, organization_id=org_id, name="b", slug="b")) + session.add( + Agent( + id=lead_id, + name="Lead", + board_id=board_id, + gateway_id=uuid4(), + is_board_lead=True, + openclaw_session_id="agent:lead:session", + ), + ) + session.add( + Task( + id=dep_id, + board_id=board_id, + title="dep", + description=None, + status="inbox", + ), + ) + session.add( + Task( + id=task_id, + board_id=board_id, + title="t", + description=None, + status="review", + assigned_agent_id=None, + ), + ) + session.add( + TaskDependency( + board_id=board_id, + task_id=task_id, + depends_on_task_id=dep_id, + ), + ) + await session.commit() + + lead = (await session.exec(select(Agent).where(col(Agent.id) == lead_id))).first() + task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() + assert lead is not None + assert task is not None + blocked_by_before = await blocked_by_for_task( + session, + board_id=board_id, + task_id=task_id, + ) + assert blocked_by_before == [dep_id] + + # Re-assert the same deps list; this should be a no-op and should not + # be rejected solely because the task is blocked. + update = _TaskUpdateInput( + task=task, + actor=ActorContext(actor_type="agent", agent=lead), + board_id=board_id, + previous_status=task.status, + previous_assigned=task.assigned_agent_id, + status_requested=False, + updates={}, + comment=None, + depends_on_task_ids=[dep_id], + tag_ids=None, + custom_field_values={}, + custom_field_values_set=False, + ) + + result = await _apply_lead_task_update(session, update=update) + assert result.id == task_id + assert result.is_blocked is True + assert result.blocked_by_task_ids == [dep_id] + + reloaded = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() + assert reloaded is not None + assert reloaded.status == "review" + assert reloaded.assigned_agent_id is None + dependency_rows = ( + await session.exec( + select(TaskDependency).where( + col(TaskDependency.task_id) == task_id, + col(TaskDependency.depends_on_task_id) == dep_id, + ), + ) + ).all() + assert len(dependency_rows) == 1 + + finally: + await engine.dispose() diff --git a/backend/uv.lock b/backend/uv.lock index c01cab38..8136c6ad 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -292,12 +292,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, ] -[[package]] -name = "crontab" -version = "1.0.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/36/a255b6f5a2e22df03fd2b2f3088974b44b8c9e9407e26b44742cb7cfbf5b/crontab-1.0.5.tar.gz", hash = "sha256:f80e01b4f07219763a9869f926dd17147278e7965a928089bca6d3dc80ae46d5", size = 21963, upload-time = "2025-07-09T17:09:38.264Z" } - [[package]] name = "cryptography" version = "45.0.7" @@ -335,7 +329,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.128.6" +version = "0.131.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -344,9 +338,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d1/195005b5e45b443e305136df47ee7df4493d782e0c039dd0d97065580324/fastapi-0.128.6.tar.gz", hash = "sha256:0cb3946557e792d731b26a42b04912f16367e3c3135ea8290f620e234f2b604f", size = 374757, upload-time = "2026-02-09T17:27:03.541Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/32/158cbf685b7d5a26f87131069da286bf10fc9fbf7fc968d169d48a45d689/fastapi-0.131.0.tar.gz", hash = "sha256:6531155e52bee2899a932c746c9a8250f210e3c3303a5f7b9f8a808bfe0548ff", size = 369612, upload-time = "2026-02-22T16:38:11.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/58/a2c4f6b240eeb148fb88cdac48f50a194aba760c1ca4988c6031c66a20ee/fastapi-0.128.6-py3-none-any.whl", hash = "sha256:bb1c1ef87d6086a7132d0ab60869d6f1ee67283b20fbf84ec0003bd335099509", size = 103674, upload-time = "2026-02-09T17:27:02.355Z" }, + { url = "https://files.pythonhosted.org/packages/ff/94/b58ec24c321acc2ad1327f69b033cadc005e0f26df9a73828c9e9c7db7ce/fastapi-0.131.0-py3-none-any.whl", hash = "sha256:ed0e53decccf4459de78837ce1b867cd04fa9ce4579497b842579755d20b405a", size = 103854, upload-time = "2026-02-22T16:38:09.814Z" }, ] [[package]] @@ -377,18 +371,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] -[[package]] -name = "freezegun" -version = "1.5.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" }, -] - [[package]] name = "greenlet" version = "3.3.1" @@ -722,6 +704,7 @@ source = { virtual = "." } dependencies = [ { name = "alembic" }, { name = "clerk-backend-api" }, + { name = "cryptography" }, { name = "fastapi" }, { name = "fastapi-pagination" }, { name = "jinja2" }, @@ -759,7 +742,8 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = "==26.1.0" }, { name = "clerk-backend-api", specifier = "==4.2.0" }, { name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = "==7.13.4" }, - { name = "fastapi", specifier = "==0.128.6" }, + { name = "cryptography", specifier = "==45.0.7" }, + { name = "fastapi", specifier = "==0.131.0" }, { name = "fastapi-pagination", specifier = "==0.15.10" }, { name = "flake8", marker = "extra == 'dev'", specifier = "==7.3.0" }, { name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" }, diff --git a/compose.yml b/compose.yml index 9f3bbd62..16c9a505 100644 --- a/compose.yml +++ b/compose.yml @@ -21,6 +21,11 @@ services: image: redis:7-alpine ports: - "127.0.0.1:${REDIS_PORT:-6379}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 backend: build: @@ -29,7 +34,7 @@ services: context: . dockerfile: backend/Dockerfile env_file: - - ./backend/.env.example + - ./backend/.env environment: # Override localhost defaults for container networking DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-mission_control} @@ -42,7 +47,7 @@ services: db: condition: service_healthy redis: - condition: service_started + condition: service_healthy ports: - "${BACKEND_PORT:-8000}:8000" @@ -50,7 +55,7 @@ services: build: context: ./frontend args: - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-auto} NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE} # Optional, user-managed env file. # IMPORTANT: do NOT load `.env.example` here because it contains non-empty @@ -59,23 +64,40 @@ services: - path: ./frontend/.env required: false environment: - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-auto} NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE} depends_on: - backend ports: - "${FRONTEND_PORT:-3000}:3000" + develop: + watch: + # Rebuild frontend image when UI source or build config changes. + - action: rebuild + path: ./frontend/src + - action: rebuild + path: ./frontend/package.json + - action: rebuild + path: ./frontend/package-lock.json + - action: rebuild + path: ./frontend/next.config.ts + - action: rebuild + path: ./frontend/postcss.config.js + - action: rebuild + path: ./frontend/tailwind.config.cjs + - action: rebuild + path: ./frontend/tsconfig.json webhook-worker: build: context: . dockerfile: backend/Dockerfile - command: ["rq", "worker", "-u", "redis://redis:6379/0"] + command: ["python", "scripts/rq-docker", "worker"] env_file: - - ./backend/.env.example + - ./backend/.env depends_on: redis: - condition: service_started + condition: service_healthy db: condition: service_healthy environment: diff --git a/docs/README.md b/docs/README.md index 378e12b2..39fa9bff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,19 +1,26 @@ # Mission Control docs -This folder is the starting point for Mission Control documentation. +This folder is the documentation home for **OpenClaw Mission Control**. -## Sections +## Start here -- [Development workflow](./03-development.md) -- [Testing guide](./testing/README.md) -- [Coverage policy](./coverage-policy.md) +- [Getting started](./getting-started/README.md) +- [Development](./development/README.md) +- [Testing](./testing/README.md) - [Deployment](./deployment/README.md) -- [Production notes](./production/README.md) +- [Release checklist](./release/README.md) +- [Operations](./operations/README.md) - [Troubleshooting](./troubleshooting/README.md) +- [Gateway agent provisioning and check-in troubleshooting](./troubleshooting/gateway-agent-provisioning.md) - [Gateway WebSocket protocol](./openclaw_gateway_ws.md) - [OpenClaw baseline configuration](./openclaw_baseline_config.md) -## Status +## Reference -These pages are minimal placeholders so repo-relative links stay healthy. The actual docs -information architecture will be defined in the Docs overhaul tasks. +- [Configuration reference](./reference/configuration.md) +- [Authentication](./reference/authentication.md) +- [API notes](./reference/api.md) + +## Contributing to docs + +- [Docs style guide](./style-guide.md) diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 00000000..656db21c --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,10 @@ +# Architecture + +## High level + +- Frontend: Next.js +- Backend: FastAPI +- Database: Postgres + +> **Note** +> Add component diagrams and key data flows (auth, task lifecycle, gateway integration) as they solidify. diff --git a/docs/deployment/README.md b/docs/deployment/README.md index cb39e267..a65acb0d 100644 --- a/docs/deployment/README.md +++ b/docs/deployment/README.md @@ -1,3 +1,100 @@ -# Deployment guide +# Deployment -Placeholder. +This section covers deploying Mission Control in self-hosted environments. + +> **Goal** +> A simple, reproducible deploy that preserves the Postgres volume and supports safe upgrades. + +## Deployment mode: single host (Docker Compose) + +### Prerequisites + +- Docker + Docker Compose v2 (`docker compose`) +- A host where the **browser** can reach the backend URL you configure (see `NEXT_PUBLIC_API_URL` below) + +### 1) Configure environment + +From repo root: + +```bash +cp .env.example .env +``` + +Edit `.env`: + +- `AUTH_MODE=local` (default) +- **Set** `LOCAL_AUTH_TOKEN` to a non-placeholder value (≥ 50 chars) +- Ensure `NEXT_PUBLIC_API_URL` is reachable from the browser (not a Docker-internal hostname) + +Key variables (from `.env.example` / `compose.yml`): + +- Frontend: `FRONTEND_PORT` (default `3000`) +- Backend: `BACKEND_PORT` (default `8000`) +- Postgres: `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_PORT` +- Backend: + - `DB_AUTO_MIGRATE` (default `true` in compose) + - `CORS_ORIGINS` (default `http://localhost:3000`) + +### 2) Start the stack + +```bash +docker compose -f compose.yml --env-file .env up -d --build +``` + +Open: + +- Frontend: `http://localhost:${FRONTEND_PORT:-3000}` +- Backend health: `http://localhost:${BACKEND_PORT:-8000}/healthz` + +### 3) Verify + +```bash +curl -f "http://localhost:${BACKEND_PORT:-8000}/healthz" +``` + +If the frontend loads but API calls fail, double-check: + +- `NEXT_PUBLIC_API_URL` is set and reachable from the **browser** +- backend CORS includes the frontend origin (`CORS_ORIGINS`) + +## Database persistence + +The Compose stack uses a named volume: + +- `postgres_data` → `/var/lib/postgresql/data` + +This means: + +- `docker compose ... down` preserves data +- `docker compose ... down -v` is **destructive** (deletes the DB volume) + +## Migrations / upgrades + +### Default behavior in Compose + +In `compose.yml`, the backend container defaults: + +- `DB_AUTO_MIGRATE=true` + +So on startup the backend will attempt to run Alembic migrations automatically. + +> **Warning** +> For zero/near-zero downtime, migrations must be **backward compatible** with the currently running app if you do rolling deploys. + +### Safer operator pattern (manual migrations) + +If you want more control, set `DB_AUTO_MIGRATE=false` and run migrations explicitly during deploy: + +```bash +cd backend +uv run alembic upgrade head +``` + +## Reverse proxy / TLS + +Typical setup (outline): + +- Put the frontend behind HTTPS (reverse proxy) +- Ensure the frontend can reach the backend over the configured `NEXT_PUBLIC_API_URL` + +This section is intentionally minimal until we standardize a recommended proxy (Caddy/Nginx/Traefik). diff --git a/docs/development/README.md b/docs/development/README.md new file mode 100644 index 00000000..045cfc77 --- /dev/null +++ b/docs/development/README.md @@ -0,0 +1,59 @@ +# Development + +This section is for contributors developing Mission Control locally. + +## Recommended workflow (fast loop) + +Run Postgres in Docker, run backend + frontend on your host. + +### 1) Start Postgres + +From repo root: + +```bash +cp .env.example .env +docker compose -f compose.yml --env-file .env up -d db +``` + +### 2) Run the backend (dev) + +```bash +cd backend +cp .env.example .env + +uv sync --extra dev +uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +Verify: + +```bash +curl -f http://localhost:8000/healthz +``` + +### 3) Run the frontend (dev) + +```bash +cd frontend +cp .env.example .env.local +npm install +npm run dev +``` + +Open http://localhost:3000. + +## Useful repo-root commands + +```bash +make help +make setup +make check +``` + +- `make setup`: sync backend + frontend deps +- `make check`: lint + typecheck + tests + build (closest CI parity) + +## Related docs + +- [Testing](../testing/README.md) +- [Release checklist](../release/README.md) diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md new file mode 100644 index 00000000..ae54f033 --- /dev/null +++ b/docs/getting-started/README.md @@ -0,0 +1,30 @@ +# Getting started + +## What is Mission Control? + +Mission Control is the web UI and HTTP API for operating OpenClaw. + +It provides a control plane for boards, tasks, agents, approvals, and (optionally) gateway connections. + +## Quickstart (Docker Compose) + +From repo root: + +```bash +cp .env.example .env + +# REQUIRED when AUTH_MODE=local +# Set LOCAL_AUTH_TOKEN to a non-placeholder value with at least 50 characters. + +docker compose -f compose.yml --env-file .env up -d --build +``` + +Open: +- Frontend: http://localhost:3000 +- Backend health: http://localhost:8000/healthz + +## Next steps + +- [Authentication](../reference/authentication.md) +- [Deployment](../deployment/README.md) +- [Development](../development/README.md) diff --git a/docs/installer-support.md b/docs/installer-support.md index e870914d..6f06600a 100644 --- a/docs/installer-support.md +++ b/docs/installer-support.md @@ -17,6 +17,7 @@ This document defines current support status for `./install.sh`. | openSUSE | `zypper` | **Scaffolded** | Detection + actionable commands present; auto-install path is TODO. | | Arch Linux | `pacman` | **Scaffolded** | Detection + actionable commands present; auto-install path is TODO. | | Other Linux distros | unknown | **Unsupported** | Installer exits with package-manager guidance requirement. | +| macOS (Darwin) | Homebrew | **Stable** | Docker mode requires Docker Desktop. Local mode uses Homebrew for curl, git, make, openssl, Node.js. | ## Guard rails diff --git a/docs/openclaw_baseline_config.md b/docs/openclaw_baseline_config.md index daa0c746..4c28a3d1 100644 --- a/docs/openclaw_baseline_config.md +++ b/docs/openclaw_baseline_config.md @@ -479,6 +479,9 @@ When adding a gateway in Mission Control: - URL: `ws://127.0.0.1:18789` (or your host/IP with explicit port) - Token: provide only if your gateway requires token auth +- Device pairing: enabled by default and recommended + - Keep pairing enabled for normal operation. + - Optional bypass: enable `Disable device pairing` per gateway only when the gateway is explicitly configured for control UI auth bypass (for example `gateway.controlUi.dangerouslyDisableDeviceAuth: true` plus appropriate `gateway.controlUi.allowedOrigins`). - Workspace root (in Mission Control gateway config): align with `agents.defaults.workspace` when possible ## Security Notes diff --git a/docs/openclaw_gateway_ws.md b/docs/openclaw_gateway_ws.md index 5fc7b84e..db617a55 100644 --- a/docs/openclaw_gateway_ws.md +++ b/docs/openclaw_gateway_ws.md @@ -1,3 +1,30 @@ # Gateway WebSocket protocol -Placeholder. +## Connection Types + +OpenClaw Mission Control supports both secure (`wss://`) and non-secure (`ws://`) WebSocket connections to gateways. + +### Secure Connections (wss://) + +For production environments, always use `wss://` (WebSocket Secure) connections with valid TLS certificates. + +### Self-Signed Certificates + +You can enable support for self-signed TLS certificates with a toggle: + +1. Navigate to the gateway configuration page (Settings → Gateways) +2. When creating or editing a gateway, enable: **"Allow self-signed TLS certificates"** +3. This applies to any `wss://` gateway URL for that gateway configuration. + +When enabled, Mission Control skips TLS certificate verification for that gateway connection. + +**Security Warning**: Enabling this weakens transport security and should only be used when you explicitly trust the endpoint and network path. Prefer valid CA-signed certificates for production gateways. + +## Configuration Options + +When configuring a gateway, you can specify: + +- **Gateway URL**: The WebSocket endpoint (e.g., `wss://localhost:18789` or `ws://gateway:18789`) +- **Gateway Token**: Optional authentication token +- **Workspace Root**: The root directory for gateway files (e.g., `~/.openclaw`) +- **Allow self-signed TLS certificates**: Toggle TLS certificate verification off for this gateway's `wss://` connections (default: disabled) diff --git a/docs/operations/README.md b/docs/operations/README.md new file mode 100644 index 00000000..42546271 --- /dev/null +++ b/docs/operations/README.md @@ -0,0 +1,86 @@ +# Operations + +Runbooks and operational notes for running Mission Control. + +## Health checks + +Backend exposes: + +- `/healthz` — liveness +- `/readyz` — readiness + +Example: + +```bash +curl -f http://localhost:8000/healthz +curl -f http://localhost:8000/readyz +``` + +## Logs + +### Docker Compose + +```bash +# tail everything +docker compose -f compose.yml --env-file .env logs -f --tail=200 + +# tail just backend +docker compose -f compose.yml --env-file .env logs -f --tail=200 backend +``` + +The backend supports slow-request logging via `REQUEST_LOG_SLOW_MS`. + +## Backups + +The DB runs in Postgres (Compose `db` service) and persists to the `postgres_data` named volume. + +### Minimal backup (logical) + +Example with `pg_dump` (run on the host): + +```bash +# load variables from .env (trusted file only) +set -a +. ./.env +set +a + +: "${POSTGRES_DB:?set POSTGRES_DB in .env}" +: "${POSTGRES_USER:?set POSTGRES_USER in .env}" +: "${POSTGRES_PORT:?set POSTGRES_PORT in .env}" +: "${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env (strong, unique value; not \"postgres\")}" + +PGPASSWORD="$POSTGRES_PASSWORD" pg_dump \ + -h 127.0.0.1 -p "$POSTGRES_PORT" -U "$POSTGRES_USER" \ + -d "$POSTGRES_DB" \ + --format=custom > mission_control.backup +``` + +> **Note** +> For real production, prefer automated backups + retention + periodic restore drills. + +## Upgrades / rollbacks + +### Upgrade (Compose) + +```bash +docker compose -f compose.yml --env-file .env up -d --build +``` + +### Rollback + +Rollback typically means deploying a previous image/commit. + +> **Warning** +> If you applied non-backward-compatible DB migrations, rolling back the app may require restoring the database. + +## Common issues + +### Frontend loads but API calls fail + +- Confirm `NEXT_PUBLIC_API_URL` is set and reachable from the browser. +- Confirm backend CORS includes the frontend origin (`CORS_ORIGINS`). + +### Auth mismatch + +- Backend: `AUTH_MODE` (`local` or `clerk`) +- Frontend: `NEXT_PUBLIC_AUTH_MODE` should match diff --git a/docs/reference/api.md b/docs/reference/api.md new file mode 100644 index 00000000..77e83c13 --- /dev/null +++ b/docs/reference/api.md @@ -0,0 +1,141 @@ +# API reference (notes + conventions) + +Mission Control exposes a JSON HTTP API (FastAPI) under `/api/v1/*`. + +- Default backend base URL (local): `http://localhost:8000` +- Health endpoints: + - `GET /health` (liveness) + - `GET /healthz` (liveness alias) + - `GET /readyz` (readiness) + +## OpenAPI / Swagger + +- OpenAPI schema: `GET /openapi.json` +- Swagger UI (FastAPI default): `GET /docs` + +> If you are building clients, prefer generating from `openapi.json`. + +## API versioning + +- Current prefix: `/api/v1` +- Backwards compatibility is **best-effort** while the project is under active development. + +## Authentication + +All protected endpoints expect a bearer token: + +```http +Authorization: Bearer +``` + +Auth mode is controlled by `AUTH_MODE`: + +- `local`: shared bearer token auth (token is `LOCAL_AUTH_TOKEN`) +- `clerk`: Clerk JWT auth + +Notes: +- The frontend uses the same bearer token scheme in local mode (users paste the token into the UI). +- Many “agent” endpoints use an agent token header instead (see below). + +### Agent auth (Mission Control agents) + +Some endpoints are designed for autonomous agents and use an agent token header: + +```http +X-Agent-Token: +``` + +In the backend, these are enforced via the “agent auth” context. When in doubt, consult the route’s dependencies (e.g., `require_admin_or_agent`). + +## Authorization / permissions model (high level) + +The backend distinguishes between: + +- **users** (humans) authenticated via `AUTH_MODE` +- **agents** authenticated via agent tokens + +Common patterns: + +- **Admin-only** user endpoints: require an authenticated user with admin privileges. +- **Admin or agent** endpoints: allow either an admin user or an authenticated agent. +- **Board-scoped access**: user/agent access may be restricted to a specific board. + +> SOC2 note: the API produces an audit-friendly request id (see below), but role/permission policy should be documented per endpoint as we stabilize. + +## Request IDs + +Every response includes an `X-Request-Id` header. + +- Clients may supply their own `X-Request-Id`; otherwise the server generates one. +- Use this id to correlate client reports with server logs. + +## Errors + +Errors are returned as JSON with a stable top-level shape: + +```json +{ + "detail": "...", + "request_id": "..." +} +``` + +Common status codes: + +- `401 Unauthorized`: missing/invalid credentials +- `403 Forbidden`: authenticated but not allowed +- `404 Not Found`: resource missing (or not visible) +- `422 Unprocessable Entity`: request validation error +- `500 Internal Server Error`: unhandled server errors + +Validation errors (`422`) typically return `detail` as a list of structured field errors (FastAPI/Pydantic style). + +## Pagination + +List endpoints commonly return an `items` array with paging fields (varies by endpoint). If you’re implementing new list endpoints, prefer consistent parameters: + +- `limit` +- `offset` + +…and return: + +- `items: []` +- `total` +- `limit` +- `offset` + +## Examples (curl) + +### Health + +```bash +curl -f http://localhost:8000/healthz +``` + +### Agent heartbeat check-in + +```bash +curl -s -X POST http://localhost:8000/api/v1/agent/heartbeat \ + -H "X-Agent-Token: $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"Tessa","board_id":"","status":"online"}' +``` + +### List tasks for a board + +```bash +curl -s "http://localhost:8000/api/v1/agent/boards//tasks?status=inbox&limit=10" \ + -H "X-Agent-Token: $AUTH_TOKEN" +``` + +## Gaps / follow-ups + +- Per-endpoint documentation of: + - required auth header (`Authorization` vs `X-Agent-Token`) + - required role (admin vs member vs agent) + - common error responses per endpoint +- Rate limits are not currently specified in the docs; if enforced, document them here and in OpenAPI. +- Add canonical examples for: + - creating/updating tasks + comments + - board memory streaming + - approvals workflow diff --git a/docs/reference/authentication.md b/docs/reference/authentication.md new file mode 100644 index 00000000..b791ad73 --- /dev/null +++ b/docs/reference/authentication.md @@ -0,0 +1,30 @@ +# Authentication + +Mission Control supports two auth modes via `AUTH_MODE`: + +- `local`: shared bearer token auth for self-hosted deployments +- `clerk`: Clerk JWT auth + +## Local mode + +Backend: + +- `AUTH_MODE=local` +- `LOCAL_AUTH_TOKEN=` + +Frontend: + +- `NEXT_PUBLIC_AUTH_MODE=local` +- Provide the token via the login UI. + +## Clerk mode + +Backend: + +- `AUTH_MODE=clerk` +- `CLERK_SECRET_KEY=` + +Frontend: + +- `NEXT_PUBLIC_AUTH_MODE=clerk` +- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md new file mode 100644 index 00000000..ae7037dc --- /dev/null +++ b/docs/reference/configuration.md @@ -0,0 +1,19 @@ +# Configuration reference + +This page collects the most important config values. + +## Root `.env` (Compose) + +See `.env.example` for defaults and required values. + +### `NEXT_PUBLIC_API_URL` + +- **Where set:** `.env` (frontend container environment) +- **Purpose:** Public URL the browser uses to call the backend. +- **Gotcha:** Must be reachable from the *browser* (host), not a Docker network alias. + +### `LOCAL_AUTH_TOKEN` + +- **Where set:** `.env` (backend) +- **When required:** `AUTH_MODE=local` +- **Policy:** Must be non-placeholder and at least 50 characters. diff --git a/docs/release/README.md b/docs/release/README.md new file mode 100644 index 00000000..f7287ae6 --- /dev/null +++ b/docs/release/README.md @@ -0,0 +1,62 @@ +# Release checklist + +This is a lightweight, operator-friendly checklist for releasing Mission Control. + +> Goal: **no data loss** and **near-zero (ideally zero) user-visible downtime**. + +## Before you release + +- [ ] Confirm the target version/commit SHA. +- [ ] Review merged PRs since last release (especially DB schema/auth changes). +- [ ] Ensure CI is green on the target SHA. +- [ ] Confirm you have: + - [ ] access to the host(s) + - [ ] access to Postgres backups (or snapshots) + - [ ] a rollback plan + +## Database safety + +- [ ] Verify migrations are **backward compatible** with the current running app (if doing rolling deploys). +- [ ] Take a backup / snapshot. +- [ ] If migrations are risky or not backward compatible, schedule a maintenance window. + +## Deploy (Docker Compose) + +- [ ] Pull / build the new images (or update the repo checkout). +- [ ] Apply migrations (if you run them manually): + +```bash +# example: if running backend locally on the host +cd backend +uv run alembic upgrade head +``` + +- [ ] Restart services with minimal disruption: + +```bash +docker compose -f compose.yml --env-file .env up -d --build +``` + +## Post-deploy verification + +- [ ] Backend health: `GET /healthz` returns 200 +- [ ] Backend readiness: `GET /readyz` returns 200 +- [ ] Frontend loads (no console spam) +- [ ] Login works (local/clerk mode) +- [ ] Core flows work end-to-end: + - [ ] View board + - [ ] Create/update a task + - [ ] Post a comment + - [ ] Heartbeat check-in succeeds + +## Rollback (if needed) + +- [ ] Roll back the app version (compose / images). +- [ ] If migrations were applied and are not reversible, rollbacks may require a DB restore. + +## Notes to keep this honest + +- If you add a new operational dependency (e.g., redis), update: + - `README.md` (overview + quickstart) + - `docs/deployment/README.md` + - this checklist diff --git a/docs/style-guide.md b/docs/style-guide.md new file mode 100644 index 00000000..bd75495a --- /dev/null +++ b/docs/style-guide.md @@ -0,0 +1,39 @@ +# Docs style guide + +## Principles + +- **Be concrete.** Prefer commands, examples, and “expected output” over prose. +- **Don’t invent behavior.** If unsure, link to the source file and mark it as “verify”. +- **Optimize for scanning.** Short sections, bullets, and tables. +- **Call out risk.** Anything destructive or security-sensitive should be labeled clearly. + +## Markdown conventions + +- Use sentence-case headings. +- Prefer fenced code blocks with a language (`bash`, `yaml`, `json`). +- For warnings/notes, use simple callouts: + +```md +> **Note** +> ... + +> **Warning** +> ... +``` + +## Common templates + +### Procedure + +1. Prereqs +2. Steps +3. Verify +4. Troubleshooting + +### Config reference entry + +- **Name** +- **Where set** (`.env`, env var, compose) +- **Default** +- **Example** +- **Notes / pitfalls** diff --git a/docs/testing/README.md b/docs/testing/README.md index 80dcee45..3d56f4bd 100644 --- a/docs/testing/README.md +++ b/docs/testing/README.md @@ -1,3 +1,82 @@ -# Testing guide +# Testing -Placeholder: see root `README.md` and `CONTRIBUTING.md` for current commands. +This guide describes how to run Mission Control tests locally. + +## Quick start (repo root) + +```bash +make setup +make check +``` + +`make check` is the closest thing to “CI parity”: + +- backend: lint + typecheck + unit tests (with scoped coverage gate) +- frontend: lint + typecheck + unit tests (Vitest) + production build + +## Backend tests + +From repo root: + +```bash +make backend-test +make backend-coverage +``` + +Or from `backend/`: + +```bash +cd backend +uv run pytest +``` + +Notes: + +- Some tests may require a running Postgres (see root `compose.yml`). +- `make backend-coverage` enforces a strict coverage gate on a scoped set of modules. + +## Frontend tests + +From repo root: + +```bash +make frontend-test +``` + +Or from `frontend/`: + +```bash +cd frontend +npm run test +npm run test:watch +``` + +## End-to-end (Cypress) + +The frontend has Cypress configured in `frontend/cypress/`. + +Typical flow: + +1) Start the stack (or start backend + frontend separately) +2) Run Cypress + +Example (two terminals): + +```bash +# terminal 1 +cp .env.example .env +docker compose -f compose.yml --env-file .env up -d --build +``` + +```bash +# terminal 2 +cd frontend +npm run e2e +``` + +Or run interactively: + +```bash +cd frontend +npm run e2e:open +``` diff --git a/docs/troubleshooting/README.md b/docs/troubleshooting/README.md index 1b897489..cb2efac4 100644 --- a/docs/troubleshooting/README.md +++ b/docs/troubleshooting/README.md @@ -1,3 +1,12 @@ # Troubleshooting -Placeholder. +- [Gateway agent provisioning and check-in](./gateway-agent-provisioning.md) + +## Common issues + +- Frontend can’t reach backend (check `NEXT_PUBLIC_API_URL`) +- Auth errors (check `AUTH_MODE`, tokens) +- DB connection/migrations + +> **Note** +> Expand with concrete symptoms + fixes as issues are discovered. diff --git a/docs/troubleshooting/gateway-agent-provisioning.md b/docs/troubleshooting/gateway-agent-provisioning.md new file mode 100644 index 00000000..695b35ab --- /dev/null +++ b/docs/troubleshooting/gateway-agent-provisioning.md @@ -0,0 +1,106 @@ +# Gateway Agent Provisioning and Check-In Troubleshooting + +This guide explains how agent provisioning converges to a healthy state, and how to debug when an agent appears stuck. + +## Fast Convergence Policy + +Mission Control now uses a fast convergence policy for wake/check-in: + +- Check-in deadline after each wake: **30 seconds** +- Maximum wake attempts without check-in: **3** +- If no check-in after the third attempt: agent is marked **offline** and provisioning escalation stops + +This applies to both gateway-main and board agents. + +## Expected Lifecycle + +1. Mission Control provisions/updates the agent and sends wake. +2. A delayed reconcile task is queued for the check-in deadline. +3. Agent should call heartbeat quickly after startup/bootstrap. +4. If heartbeat arrives: + - `last_seen_at` is updated + - wake escalation state is reset (`wake_attempts=0`, check-in deadline cleared) +5. If heartbeat does not arrive by deadline: + - reconcile re-runs lifecycle (wake again) + - up to 3 total wake attempts +6. If still no heartbeat after 3 attempts: + - agent status becomes `offline` + - `last_provision_error` is set + +## Startup Check-In Behavior + +Templates now explicitly require immediate first-cycle check-in: + +- Main agent heartbeat instructions require immediate check-in after wake/bootstrap. +- Board lead bootstrap requires heartbeat check-in before orchestration. +- Board worker bootstrap already included immediate check-in. + +If a gateway still has older templates, run template sync and reprovision/wake. + +## What You Should See in Logs + +Healthy flow usually includes: + +- `lifecycle.queue.enqueued` +- `queue.worker.success` (for lifecycle tasks) +- `lifecycle.reconcile.skip_not_stuck` (after heartbeat lands) + +If agent is not checking in: + +- `lifecycle.reconcile.deferred` (before deadline) +- `lifecycle.reconcile.retriggered` (retry wake) +- `lifecycle.reconcile.max_attempts_reached` (final fail-safe at attempt 3) + +If you do not see lifecycle events at all, verify queue worker health first. + +## Common Failure Modes + +### Wake was sent, but no check-in arrived + +Possible causes: + +- Agent process never started or crashed during bootstrap +- Agent ignored startup instructions due to stale templates +- Heartbeat call failed (network/auth/base URL mismatch) + +Actions: + +1. Confirm current templates were synced to gateway. +2. Re-run provisioning/update to trigger a fresh wake. +3. Verify agent can reach Mission Control API and send heartbeat with `X-Agent-Token`. + +### Agent stays provisioning/updating with no retries + +Possible causes: + +- Queue worker not running +- Queue/Redis mismatch between API process and worker process + +Actions: + +1. Verify worker process is running continuously. +2. Verify `rq_redis_url` and `rq_queue_name` are identical for API and worker. +3. Check worker logs for dequeue/handler errors. + +### Agent ended offline quickly + +This is expected when no check-in is received after 3 wake attempts. The system fails fast by design. + +Actions: + +1. Fix check-in path first (startup, network, token, API reachability). +2. Re-run provisioning/update to start a new attempt cycle. + +## Operator Recovery Checklist + +1. Ensure queue worker is running. +2. Sync templates for the gateway. +3. Trigger agent update/provision from Mission Control. +4. Watch logs for: + - `lifecycle.queue.enqueued` + - `lifecycle.reconcile.retriggered` (if needed) + - heartbeat activity / `skip_not_stuck` +5. If still failing, capture: + - gateway logs around bootstrap + - worker logs around lifecycle events + - agent `last_provision_error`, `wake_attempts`, `last_seen_at` diff --git a/frontend/.env.example b/frontend/.env.example index b1db4249..c5f1ad1d 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,6 +1,7 @@ -# REQUIRED: base URL for frontend -> backend calls (must be set for Activity feed and other API calls). -# Must be reachable from the browser (host). -NEXT_PUBLIC_API_URL=http://localhost:8000 +# Base URL for frontend -> backend calls. +# Use `auto` to target the same host currently serving Mission Control on port 8000. +# Example explicit override: https://mc.example.com +NEXT_PUBLIC_API_URL=auto # Auth mode: clerk or local. # - clerk: Clerk sign-in flow diff --git a/frontend/Dockerfile b/frontend/Dockerfile index a834856f..64c7b179 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -13,7 +13,7 @@ COPY --from=deps /app/node_modules ./node_modules COPY . ./ # Allows configuring the API URL at build time. -ARG NEXT_PUBLIC_API_URL=http://localhost:8000 +ARG NEXT_PUBLIC_API_URL=auto ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ARG NEXT_PUBLIC_AUTH_MODE ENV NEXT_PUBLIC_AUTH_MODE=${NEXT_PUBLIC_AUTH_MODE} @@ -28,7 +28,7 @@ ARG NEXT_PUBLIC_AUTH_MODE # If provided at runtime, Next will expose NEXT_PUBLIC_* to the browser as well # (but note some values may be baked at build time). -ENV NEXT_PUBLIC_API_URL=http://localhost:8000 +ENV NEXT_PUBLIC_API_URL=auto ENV NEXT_PUBLIC_AUTH_MODE=${NEXT_PUBLIC_AUTH_MODE} COPY --from=builder /app/.next ./.next diff --git a/frontend/README.md b/frontend/README.md index 07d4daf2..a80e0695 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -44,15 +44,15 @@ The frontend reads configuration from standard Next.js env files (`.env.local`, #### `NEXT_PUBLIC_API_URL` -Base URL of the backend API. +Base URL of the backend API (or `auto`). -- Default for local dev: `http://localhost:8000` +- Default: `auto` (resolved in browser as `http(s)://:8000`) - Used by the generated API client and helpers (see `src/lib/api-base.ts` and `src/api/mutator.ts`). Example: ```env -NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_API_URL=auto ``` ### Authentication mode @@ -141,9 +141,10 @@ If you’re working on self-hosting, prefer running compose from the repo root s ## Troubleshooting -### `NEXT_PUBLIC_API_URL is not set` +### `NEXT_PUBLIC_API_URL` and remote hosts -The API client throws if `NEXT_PUBLIC_API_URL` is missing. +If unset or set to `auto`, the client uses `http(s)://:8000`. +If your backend is on a different host/port, set `NEXT_PUBLIC_API_URL` explicitly. Fix: diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index 18baa6bb..46b1c779 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -1,28 +1,14 @@ import { defineConfig } from "cypress"; -import { clerkSetup } from "@clerk/testing/cypress"; export default defineConfig({ - env: { - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: - process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, - // Optional overrides. - CLERK_ORIGIN: process.env.CYPRESS_CLERK_ORIGIN, - CLERK_TEST_EMAIL: process.env.CYPRESS_CLERK_TEST_EMAIL, - CLERK_TEST_OTP: process.env.CYPRESS_CLERK_TEST_OTP, - }, e2e: { baseUrl: "http://localhost:3000", specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", supportFile: "cypress/support/e2e.ts", - // Clerk helpers perform async work inside `cy.then()`. CI can be slow enough - // that Cypress' 4s default command timeout flakes. defaultCommandTimeout: 20_000, retries: { runMode: 2, openMode: 0, }, - setupNodeEvents(on, config) { - return clerkSetup({ config }); - }, }, }); diff --git a/frontend/cypress/e2e/activity_feed.cy.ts b/frontend/cypress/e2e/activity_feed.cy.ts index 77a06bf1..4245b4a1 100644 --- a/frontend/cypress/e2e/activity_feed.cy.ts +++ b/frontend/cypress/e2e/activity_feed.cy.ts @@ -1,22 +1,11 @@ /// -// Clerk/Next.js occasionally triggers a hydration mismatch on the SignIn route in CI. -// This is non-deterministic UI noise for these tests; ignore it so assertions can proceed. -Cypress.on("uncaught:exception", (err) => { - if (err.message?.includes("Hydration failed")) { - return false; - } - return true; -}); - describe("/activity feed", () => { const apiBase = "**/api/v1"; - const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); beforeEach(() => { - // Clerk's Cypress helpers perform async work inside `cy.then()`. // CI can be slow enough that the default 4s command timeout flakes. Cypress.config("defaultCommandTimeout", 20_000); }); @@ -49,6 +38,30 @@ describe("/activity feed", () => { function stubBoardBootstrap() { // Some app bootstraps happen before we get to the /activity call. // Keep these stable so the page always reaches the activity request. + cy.intercept("GET", `${apiBase}/users/me*`, { + statusCode: 200, + body: { + id: "u1", + clerk_user_id: "local-auth-user", + email: "local@example.com", + name: "Local User", + preferred_name: "Local User", + timezone: "UTC", + }, + }).as("usersMe"); + + cy.intercept("GET", `${apiBase}/organizations/me/list*`, { + statusCode: 200, + body: [ + { + id: "org1", + name: "Testing Org", + is_active: true, + role: "owner", + }, + ], + }).as("orgsList"); + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { statusCode: 200, body: { organization_id: "org1", role: "owner" }, @@ -77,10 +90,11 @@ describe("/activity feed", () => { cy.contains(/live feed/i).should("be.visible"); } - it("auth negative: signed-out user is redirected to sign-in", () => { - // SignedOutPanel runs in redirect mode on this page. + it("auth negative: signed-out user sees auth prompt", () => { cy.visit("/activity"); - cy.location("pathname", { timeout: 20_000 }).should("match", /\/sign-in/); + cy.contains(/sign in to view the feed|local authentication/i, { + timeout: 20_000, + }).should("be.visible"); }); it("happy path: renders task comment cards", () => { @@ -107,10 +121,7 @@ describe("/activity feed", () => { stubStreamsEmpty(); - cy.visit("/sign-in"); - cy.clerkLoaded(); - cy.clerkSignIn({ strategy: "email_code", identifier: email }); - + cy.loginWithLocalAuth(); cy.visit("/activity"); assertSignedInAndLanded(); cy.wait("@activityList", { timeout: 20_000 }); @@ -131,10 +142,7 @@ describe("/activity feed", () => { stubStreamsEmpty(); - cy.visit("/sign-in"); - cy.clerkLoaded(); - cy.clerkSignIn({ strategy: "email_code", identifier: email }); - + cy.loginWithLocalAuth(); cy.visit("/activity"); assertSignedInAndLanded(); cy.wait("@activityList", { timeout: 20_000 }); @@ -152,10 +160,7 @@ describe("/activity feed", () => { stubStreamsEmpty(); - cy.visit("/sign-in"); - cy.clerkLoaded(); - cy.clerkSignIn({ strategy: "email_code", identifier: email }); - + cy.loginWithLocalAuth(); cy.visit("/activity"); assertSignedInAndLanded(); cy.wait("@activityList", { timeout: 20_000 }); diff --git a/frontend/cypress/e2e/activity_smoke.cy.ts b/frontend/cypress/e2e/activity_smoke.cy.ts index ae60a51a..4da0469e 100644 --- a/frontend/cypress/e2e/activity_smoke.cy.ts +++ b/frontend/cypress/e2e/activity_smoke.cy.ts @@ -1,6 +1,8 @@ describe("/activity page", () => { - it("signed-out user is redirected to sign-in", () => { + it("signed-out user sees an auth prompt", () => { cy.visit("/activity"); - cy.location("pathname", { timeout: 20_000 }).should("match", /\/sign-in/); + cy.contains(/local authentication|sign in to mission control/i, { + timeout: 20_000, + }).should("be.visible"); }); }); diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts new file mode 100644 index 00000000..c347c077 --- /dev/null +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -0,0 +1,304 @@ +/// + +describe("/boards/:id task board", () => { + const apiBase = "**/api/v1"; + const email = "local-auth-user@example.com"; + + const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); + + beforeEach(() => { + Cypress.config("defaultCommandTimeout", 20_000); + }); + + afterEach(() => { + Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); + }); + + function stubEmptySse() { + // Keep known board-related SSE endpoints quiet in tests. + const emptySse = { + statusCode: 200, + headers: { "content-type": "text/event-stream" }, + body: "", + }; + + cy.intercept("GET", `${apiBase}/boards/*/tasks/stream*`, emptySse).as( + "tasksStream", + ); + cy.intercept("GET", `${apiBase}/boards/*/approvals/stream*`, emptySse).as( + "approvalsStream", + ); + cy.intercept("GET", `${apiBase}/boards/*/memory/stream*`, emptySse).as( + "memoryStream", + ); + cy.intercept("GET", `${apiBase}/agents/stream*`, emptySse).as("agentsStream"); + } + + function openEditTaskDialog() { + cy.get('button[title="Edit task"]', { timeout: 20_000 }) + .should("be.visible") + .and("not.be.disabled") + .click(); + cy.get('[aria-label="Edit task"]', { timeout: 20_000 }).should("be.visible"); + } + + it("auth negative: signed-out user is shown local auth login", () => { + cy.visit("/boards/b1"); + cy.contains("h1", /local authentication/i, { timeout: 30_000 }).should( + "be.visible", + ); + }); + + it("happy path: renders tasks from snapshot and supports create + status update + delete (stubbed)", () => { + stubEmptySse(); + + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { + statusCode: 200, + body: { + id: "m1", + organization_id: "o1", + user_id: "u1", + role: "owner", + all_boards_read: true, + all_boards_write: true, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + board_access: [{ board_id: "b1", can_read: true, can_write: true }], + }, + }).as("membership"); + + cy.intercept("GET", `${apiBase}/users/me*`, { + statusCode: 200, + body: { + id: "u1", + clerk_user_id: "clerk_u1", + email, + name: "Jane Test", + preferred_name: "Jane", + timezone: "America/New_York", + is_super_admin: false, + }, + }).as("me"); + + cy.intercept("GET", `${apiBase}/organizations/me/list*`, { + statusCode: 200, + body: [ + { id: "o1", name: "Personal", role: "owner", is_active: true }, + ], + }).as("organizations"); + + cy.intercept("GET", `${apiBase}/tags*`, { + statusCode: 200, + body: { items: [], total: 0, limit: 200, offset: 0 }, + }).as("tags"); + + cy.intercept("GET", `${apiBase}/organizations/me/custom-fields*`, { + statusCode: 200, + body: [], + }).as("customFields"); + + cy.intercept("GET", `${apiBase}/boards/b1/snapshot*`, { + statusCode: 200, + body: { + board: { + id: "b1", + name: "Demo Board", + slug: "demo-board", + description: "Demo", + gateway_id: "g1", + board_group_id: null, + board_type: "general", + objective: null, + success_metrics: null, + target_date: null, + goal_confirmed: true, + goal_source: "test", + organization_id: "o1", + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + }, + tasks: [ + { + id: "t1", + board_id: "b1", + title: "Inbox task", + description: "", + status: "inbox", + priority: "medium", + due_at: null, + assigned_agent_id: null, + depends_on_task_ids: [], + created_by_user_id: null, + in_progress_at: null, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + blocked_by_task_ids: [], + is_blocked: false, + assignee: null, + approvals_count: 0, + approvals_pending_count: 0, + }, + ], + agents: [], + approvals: [], + chat_messages: [], + pending_approvals_count: 0, + }, + }).as("snapshot"); + + cy.intercept("GET", `${apiBase}/boards/b1/group-snapshot*`, { + statusCode: 200, + body: { group: null, boards: [] }, + }).as("groupSnapshot"); + + cy.intercept("POST", `${apiBase}/boards/b1/tasks`, (req) => { + // Minimal assertion the UI sends expected fields. + expect(req.body).to.have.property("title"); + req.reply({ + statusCode: 200, + body: { + id: "t2", + board_id: "b1", + title: req.body.title, + description: req.body.description ?? "", + status: "inbox", + priority: req.body.priority ?? "medium", + due_at: null, + assigned_agent_id: null, + depends_on_task_ids: [], + created_by_user_id: null, + in_progress_at: null, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + blocked_by_task_ids: [], + is_blocked: false, + assignee: null, + approvals_count: 0, + approvals_pending_count: 0, + }, + }); + }).as("createTask"); + + cy.intercept("PATCH", `${apiBase}/boards/b1/tasks/t1`, (req) => { + expect(req.body).to.have.property("status"); + req.reply({ + statusCode: 200, + body: { + id: "t1", + board_id: "b1", + title: "Inbox task", + description: "", + status: req.body.status, + priority: "medium", + due_at: null, + assigned_agent_id: null, + depends_on_task_ids: [], + created_by_user_id: null, + in_progress_at: null, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:01Z", + blocked_by_task_ids: [], + is_blocked: false, + assignee: null, + approvals_count: 0, + approvals_pending_count: 0, + }, + }); + }).as("updateTask"); + + cy.intercept("DELETE", `${apiBase}/boards/b1/tasks/t1`, { + statusCode: 200, + body: { ok: true }, + }).as("deleteTask"); + + cy.intercept("GET", `${apiBase}/boards/b1/tasks/t1/comments*`, { + statusCode: 200, + body: { items: [], total: 0, limit: 200, offset: 0 }, + }).as("taskComments"); + + cy.loginWithLocalAuth(); + cy.visit("/boards/b1"); + cy.waitForAppLoaded(); + + cy.wait([ + "@snapshot", + "@groupSnapshot", + "@membership", + "@me", + "@organizations", + "@tags", + "@customFields", + ]); + + // Existing task visible. + cy.contains("Inbox task").should("be.visible"); + + // Open create task flow. + // Board page uses an icon-only button with aria-label="New task". + cy.get('button[aria-label="New task"]') + .should("be.visible") + .and("not.be.disabled") + .click(); + + cy.contains('[role="dialog"]', "New task") + .should("be.visible") + .within(() => { + cy.contains("label", "Title").parent().find("input").type("New task"); + cy.contains("button", /^Create task$/) + .should("be.visible") + .and("not.be.disabled") + .click(); + }); + cy.wait(["@createTask"]); + + cy.contains("New task").should("be.visible"); + + // Open edit task dialog. + cy.contains("Inbox task").scrollIntoView().should("be.visible").click(); + cy.wait(["@taskComments"]); + cy.contains(/task detail/i).should("be.visible"); + openEditTaskDialog(); + + // Change status via Status select. + cy.get('[aria-label="Edit task"]').within(() => { + cy.contains("label", "Status") + .parent() + .within(() => { + cy.get('[role="combobox"]').first().should("be.visible").click(); + }); + }); + + cy.contains("In progress").should("be.visible").click(); + + cy.contains("button", /save changes/i) + .should("be.visible") + .and("not.be.disabled") + .click(); + cy.wait(["@updateTask"]); + cy.get('[aria-label="Edit task"]').should("not.exist"); + + // Save closes the edit dialog; reopen it from task detail. + cy.contains(/task detail/i).should("be.visible"); + openEditTaskDialog(); + + // Delete task via delete dialog. + cy.get('[aria-label="Edit task"]').within(() => { + cy.contains("button", /^Delete task$/) + .scrollIntoView() + .should("be.visible") + .and("not.be.disabled") + .click(); + }); + cy.get('[aria-label="Delete task"]').should("be.visible"); + cy.get('[aria-label="Delete task"]').within(() => { + cy.contains("button", /^Delete task$/) + .scrollIntoView() + .should("be.visible") + .and("not.be.disabled") + .click(); + }); + cy.wait(["@deleteTask"]); + + cy.contains("Inbox task").should("not.exist"); + }); +}); diff --git a/frontend/cypress/e2e/boards_list.cy.ts b/frontend/cypress/e2e/boards_list.cy.ts new file mode 100644 index 00000000..ee075937 --- /dev/null +++ b/frontend/cypress/e2e/boards_list.cy.ts @@ -0,0 +1,95 @@ +/// + +import { setupCommonPageTestHooks } from "../support/testHooks"; + +describe("/boards", () => { + const apiBase = "**/api/v1"; + const email = "local-auth-user@example.com"; + + setupCommonPageTestHooks(apiBase); + + it("auth negative: signed-out user is shown local auth login", () => { + cy.visit("/boards"); + cy.contains("h1", /local authentication/i, { timeout: 30_000 }).should( + "be.visible", + ); + }); + + it("happy path: signed-in user sees boards list and create button", () => { + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { + statusCode: 200, + body: { + id: "m1", + organization_id: "o1", + user_id: "u1", + role: "owner", + all_boards_read: true, + all_boards_write: true, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + board_access: [], + }, + }).as("membership"); + + cy.intercept("GET", `${apiBase}/users/me*`, { + statusCode: 200, + body: { + id: "u1", + clerk_user_id: "clerk_u1", + email, + name: "Jane Test", + preferred_name: "Jane", + timezone: "America/New_York", + is_super_admin: false, + }, + }).as("me"); + + cy.intercept("GET", `${apiBase}/organizations/me/list*`, { + statusCode: 200, + body: [{ id: "o1", name: "Personal", role: "owner", is_active: true }], + }).as("organizations"); + + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { + items: [ + { + id: "b1", + name: "Demo Board", + slug: "demo-board", + description: "Demo", + gateway_id: "g1", + board_group_id: null, + board_type: "general", + objective: null, + success_metrics: null, + target_date: null, + goal_confirmed: true, + goal_source: "test", + organization_id: "o1", + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + }, + ], + total: 1, + limit: 200, + offset: 0, + }, + }).as("boards"); + + cy.intercept("GET", `${apiBase}/board-groups*`, { + statusCode: 200, + body: { items: [], total: 0, limit: 200, offset: 0 }, + }).as("boardGroups"); + + cy.loginWithLocalAuth(); + cy.visit("/boards"); + cy.waitForAppLoaded(); + + cy.wait(["@membership", "@me", "@organizations", "@boards", "@boardGroups"]); + + cy.contains(/boards/i).should("be.visible"); + cy.contains("Demo Board").should("be.visible"); + cy.contains("a", /create board/i).should("be.visible"); + }); +}); diff --git a/frontend/cypress/e2e/clerk_login.cy.ts b/frontend/cypress/e2e/clerk_login.cy.ts deleted file mode 100644 index 2f928ac0..00000000 --- a/frontend/cypress/e2e/clerk_login.cy.ts +++ /dev/null @@ -1,16 +0,0 @@ -describe("Clerk login", () => { - it("user can sign in via Clerk testing commands", () => { - const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; - - // Prereq per Clerk docs: visit a non-protected page that loads Clerk. - cy.visit("/sign-in"); - cy.clerkLoaded(); - - cy.clerkSignIn({ strategy: "email_code", identifier: email }); - - // After login, user should be able to access protected route. - cy.visit("/activity"); - cy.waitForAppLoaded(); - cy.contains(/live feed/i).should("be.visible"); - }); -}); diff --git a/frontend/cypress/e2e/global_approvals.cy.ts b/frontend/cypress/e2e/global_approvals.cy.ts new file mode 100644 index 00000000..7e0d7fb4 --- /dev/null +++ b/frontend/cypress/e2e/global_approvals.cy.ts @@ -0,0 +1,81 @@ +/// + +import { setupCommonPageTestHooks } from "../support/testHooks"; + +describe("Global approvals", () => { + const apiBase = "**/api/v1"; + + setupCommonPageTestHooks(apiBase); + + it("can render a pending approval and approve it", () => { + const approval = { + id: "a1", + board_id: "b1", + action_type: "task.closeout", + status: "pending", + confidence: 92, + created_at: "2026-02-14T00:00:00Z", + task_id: "t1", + task_ids: ["t1"], + payload: { + task_id: "t1", + title: "Close task", + reason: "Merged and ready to close", + }, + }; + + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { + items: [ + { + id: "b1", + name: "Testing", + group_id: null, + objective: null, + success_metrics: null, + target_date: null, + updated_at: "2026-02-14T00:00:00Z", + created_at: "2026-02-10T00:00:00Z", + }, + ], + }, + }).as("boardsList"); + + cy.intercept("GET", `${apiBase}/boards/b1/approvals*`, { + statusCode: 200, + body: { items: [approval] }, + }).as("approvalsList"); + + cy.intercept("PATCH", `${apiBase}/boards/b1/approvals/a1`, { + statusCode: 200, + body: { ...approval, status: "approved" }, + }).as("approvalUpdate"); + + cy.loginWithLocalAuth(); + cy.visit("/approvals"); + cy.waitForAppLoaded(); + + cy.wait( + [ + "@usersMe", + "@organizationsList", + "@orgMeMember", + "@boardsList", + "@approvalsList", + ], + { timeout: 20_000 }, + ); + + // Pending approval should be visible in the list. + cy.contains(/unapproved tasks/i).should("be.visible"); + // Action type is humanized as "Task · Closeout" in the UI. + cy.contains(/task\s*(?:·|\u00b7|\u2022)?\s*closeout/i).should("be.visible"); + + cy.contains("button", /^approve$/i).click(); + cy.wait("@approvalUpdate", { timeout: 20_000 }); + + // Status badge should flip to approved. + cy.contains(/approved/i).should("be.visible"); + }); +}); diff --git a/frontend/cypress/e2e/local_auth_login.cy.ts b/frontend/cypress/e2e/local_auth_login.cy.ts new file mode 100644 index 00000000..7df170ed --- /dev/null +++ b/frontend/cypress/e2e/local_auth_login.cy.ts @@ -0,0 +1,49 @@ +describe("Local auth login", () => { + it("user with local auth token can access protected route", () => { + cy.intercept("GET", "**/api/v1/users/me*", { + statusCode: 200, + body: { + id: "u1", + clerk_user_id: "local-auth-user", + email: "local@example.com", + name: "Local User", + preferred_name: "Local User", + timezone: "UTC", + }, + }).as("usersMe"); + + cy.intercept("GET", "**/api/v1/organizations/me/list*", { + statusCode: 200, + body: [ + { + id: "org1", + name: "Testing Org", + is_active: true, + role: "owner", + }, + ], + }).as("orgsList"); + + cy.intercept("GET", "**/api/v1/organizations/me/member*", { + statusCode: 200, + body: { organization_id: "org1", role: "owner" }, + }).as("orgMeMember"); + + cy.intercept("GET", "**/api/v1/boards*", { + statusCode: 200, + body: { + items: [{ id: "b1", name: "Testing", updated_at: "2026-02-07T00:00:00Z" }], + }, + }).as("boardsList"); + + cy.intercept("GET", "**/api/v1/boards/b1/snapshot*", { + statusCode: 200, + body: { tasks: [], agents: [], approvals: [], chat_messages: [] }, + }).as("boardSnapshot"); + + cy.loginWithLocalAuth(); + cy.visit("/activity"); + cy.waitForAppLoaded(); + cy.contains(/live feed/i).should("be.visible"); + }); +}); diff --git a/frontend/cypress/e2e/organizations.cy.ts b/frontend/cypress/e2e/organizations.cy.ts index c2ab5303..f302d252 100644 --- a/frontend/cypress/e2e/organizations.cy.ts +++ b/frontend/cypress/e2e/organizations.cy.ts @@ -1,36 +1,88 @@ describe("Organizations (PR #61)", () => { - const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; + const apiBase = "**/api/v1"; - it("negative: signed-out user is redirected to sign-in when opening /organization", () => { + function stubOrganizationApis() { + cy.intercept("GET", `${apiBase}/users/me*`, { + statusCode: 200, + body: { + id: "u1", + clerk_user_id: "local-auth-user", + email: "local@example.com", + name: "Local User", + preferred_name: "Local User", + timezone: "UTC", + }, + }).as("usersMe"); + + cy.intercept("GET", `${apiBase}/organizations/me/list*`, { + statusCode: 200, + body: [ + { + id: "org1", + name: "Testing Org", + is_active: true, + role: "member", + }, + ], + }).as("orgsList"); + + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { + statusCode: 200, + body: { + id: "membership-1", + user_id: "u1", + organization_id: "org1", + role: "member", + }, + }).as("orgMembership"); + + cy.intercept("GET", `${apiBase}/organizations/me`, { + statusCode: 200, + body: { id: "org1", name: "Testing Org" }, + }).as("orgMe"); + + cy.intercept("GET", `${apiBase}/organizations/me/members*`, { + statusCode: 200, + body: { + items: [ + { + id: "membership-1", + user_id: "u1", + role: "member", + user: { + id: "u1", + email: "local@example.com", + name: "Local User", + preferred_name: "Local User", + }, + }, + ], + }, + }).as("orgMembers"); + + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { items: [] }, + }).as("boardsList"); + } + + it("negative: signed-out user sees auth prompt when opening /organization", () => { cy.visit("/organization"); - cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + cy.contains(/sign in to manage your organization|local authentication/i, { + timeout: 30_000, + }).should("be.visible"); }); it("positive: signed-in user can view /organization and sees correct invite permissions", () => { - // Story (positive): a signed-in user can reach the organization page. - // Story (negative within flow): non-admin users cannot invite members. - cy.visit("/sign-in"); - cy.clerkLoaded(); - cy.clerkSignIn({ strategy: "email_code", identifier: email }); - + stubOrganizationApis(); + cy.loginWithLocalAuth(); cy.visit("/organization"); cy.waitForAppLoaded(); cy.contains(/members\s*&\s*invites/i).should("be.visible"); - - // Deterministic assertion across roles: - // - if user is admin: invite button enabled - // - else: invite button disabled with the correct tooltip cy.contains("button", /invite member/i) .should("be.visible") - .then(($btn) => { - const isDisabled = $btn.is(":disabled"); - if (isDisabled) { - cy.wrap($btn) - .should("have.attr", "title") - .and("match", /only organization admins can invite/i); - } else { - cy.wrap($btn).should("not.be.disabled"); - } - }); + .should("be.disabled") + .and("have.attr", "title") + .and("match", /only organization admins can invite/i); }); }); diff --git a/frontend/cypress/e2e/skill_packs_sync.cy.ts b/frontend/cypress/e2e/skill_packs_sync.cy.ts new file mode 100644 index 00000000..d0cb05fb --- /dev/null +++ b/frontend/cypress/e2e/skill_packs_sync.cy.ts @@ -0,0 +1,48 @@ +/// + +import { setupCommonPageTestHooks } from "../support/testHooks"; + +describe("Skill packs", () => { + const apiBase = "**/api/v1"; + + setupCommonPageTestHooks(apiBase); + + it("can sync a pack and surface warnings", () => { + cy.intercept("GET", `${apiBase}/skills/packs*`, { + statusCode: 200, + body: [ + { + id: "p1", + name: "OpenClaw Skills", + description: "Test pack", + source_url: "https://github.com/openclaw/skills", + branch: "main", + skill_count: 12, + updated_at: "2026-02-14T00:00:00Z", + created_at: "2026-02-10T00:00:00Z", + }, + ], + }).as("packsList"); + + cy.intercept("POST", `${apiBase}/skills/packs/p1/sync*`, { + statusCode: 200, + body: { + warnings: ["1 skill skipped (missing SKILL.md)"], + }, + }).as("packSync"); + + cy.loginWithLocalAuth(); + cy.visit("/skills/packs"); + cy.waitForAppLoaded(); + + cy.wait(["@usersMe", "@organizationsList", "@orgMeMember", "@packsList"], { + timeout: 20_000, + }); + cy.contains(/openclaw skills/i).should("be.visible"); + + cy.contains("button", /^sync$/i).click(); + cy.wait("@packSync", { timeout: 20_000 }); + + cy.contains(/skill skipped/i).should("be.visible"); + }); +}); diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts index 0848d34a..83eae8e4 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.ts @@ -1,46 +1,9 @@ /// -type ClerkOtpLoginOptions = { - clerkOrigin: string; - email: string; - otp: string; -}; - const APP_LOAD_TIMEOUT_MS = 30_000; - -function getEnv(name: string, fallback?: string): string { - const value = Cypress.env(name) as string | undefined; - if (value) return value; - if (fallback !== undefined) return fallback; - throw new Error( - `Missing Cypress env var ${name}. ` + - `Set it via CYPRESS_${name}=... in CI/local before running Clerk login tests.`, - ); -} - -function clerkOriginFromPublishableKey(): string { - const key = getEnv("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"); - - // pk_test_ OR pk_live_<...> - const m = /^pk_(?:test|live)_(.+)$/.exec(key); - if (!m) throw new Error(`Unexpected Clerk publishable key format: ${key}`); - - const decoded = atob(m[1]); // e.g. beloved-ghost-73.clerk.accounts.dev$ - const domain = decoded.replace(/\$$/, ""); - - // Some flows redirect to *.accounts.dev (no clerk. subdomain) - const normalized = domain.replace(".clerk.accounts.dev", ".accounts.dev"); - return `https://${normalized}`; -} - -function normalizeOrigin(value: string): string { - try { - const url = new URL(value); - return url.origin; - } catch { - return value.replace(/\/$/, ""); - } -} +const LOCAL_AUTH_STORAGE_KEY = "mc_local_auth_token"; +const DEFAULT_LOCAL_AUTH_TOKEN = + "cypress-local-auth-token-0123456789-0123456789-0123456789x"; Cypress.Commands.add("waitForAppLoaded", () => { cy.get("[data-cy='route-loader']", { @@ -52,153 +15,19 @@ Cypress.Commands.add("waitForAppLoaded", () => { }).should("have.attr", "aria-hidden", "true"); }); -Cypress.Commands.add("loginWithClerkOtp", () => { - const clerkOrigin = normalizeOrigin( - getEnv("CLERK_ORIGIN", clerkOriginFromPublishableKey()), - ); - const email = getEnv("CLERK_TEST_EMAIL", "jane+clerk_test@example.com"); - const otp = getEnv("CLERK_TEST_OTP", "424242"); +Cypress.Commands.add("loginWithLocalAuth", (token = DEFAULT_LOCAL_AUTH_TOKEN) => { + cy.visit("/", { + onBeforeLoad(win) { + win.sessionStorage.setItem(LOCAL_AUTH_STORAGE_KEY, token); + }, + }); +}); - const opts: ClerkOtpLoginOptions = { clerkOrigin, email, otp }; - - // Navigate to a dedicated sign-in route that renders Clerk SignIn top-level. - // Cypress cannot reliably drive Clerk modal/iframe flows. - cy.visit("/sign-in"); - - const emailSelector = - 'input[type="email"], input[name="identifier"], input[autocomplete="email"]'; - const otpSelector = - 'input[autocomplete="one-time-code"], input[name*="code"], input[name^="code"], input[name^="code."], input[inputmode="numeric"]'; - const continueSelector = 'button[type="submit"], button'; - const methodSelector = /email|code|otp|send code|verification|verify|use email/i; - - const fillEmailStep = (email: string) => { - cy.get(emailSelector, { timeout: 20_000 }) - .first() - .clear() - .type(email, { delay: 10 }); - - cy.contains(continueSelector, /continue|sign in|send|next/i, { timeout: 20_000 }) - .should("be.visible") - .click({ force: true }); - }; - - const maybeSelectEmailCodeMethod = () => { - cy.get("body").then(($body) => { - const hasOtp = $body.find(otpSelector).length > 0; - if (hasOtp) return; - - const candidates = $body - .find("button,a") - .toArray() - .filter((el) => methodSelector.test((el.textContent || "").trim())); - - if (candidates.length > 0) { - cy.wrap(candidates[0]).click({ force: true }); - } - }); - }; - - const waitForOtpOrMethod = () => { - cy.get("body", { timeout: 60_000 }).should(($body) => { - const hasOtp = $body.find(otpSelector).length > 0; - const hasMethod = $body - .find("button,a") - .toArray() - .some((el) => methodSelector.test((el.textContent || "").trim())); - expect( - hasOtp || hasMethod, - "waiting for OTP input or verification method UI", - ).to.equal(true); - }); - }; - - const fillOtpAndSubmit = (otp: string) => { - waitForOtpOrMethod(); - maybeSelectEmailCodeMethod(); - - cy.get(otpSelector, { timeout: 60_000 }).first().clear().type(otp, { delay: 10 }); - - cy.get("body").then(($body) => { - const hasSubmit = $body - .find(continueSelector) - .toArray() - .some((el) => /verify|continue|sign in|confirm/i.test(el.textContent || "")); - if (hasSubmit) { - cy.contains(continueSelector, /verify|continue|sign in|confirm/i, { timeout: 20_000 }) - .should("be.visible") - .click({ force: true }); - } - }); - }; - - // Clerk SignIn can start on our app origin and then redirect to Clerk-hosted UI. - // Do email step first, then decide where the OTP step lives based on the *current* origin. - fillEmailStep(opts.email); - - cy.location("origin", { timeout: 60_000 }).then((origin) => { - const current = normalizeOrigin(origin); - if (current === opts.clerkOrigin) { - cy.origin( - opts.clerkOrigin, - { args: { otp: opts.otp } }, - ({ otp }) => { - const otpSelector = - 'input[autocomplete="one-time-code"], input[name*="code"], input[name^="code"], input[name^="code."], input[inputmode="numeric"]'; - const continueSelector = 'button[type="submit"], button'; - const methodSelector = /email|code|otp|send code|verification|verify|use email/i; - - const maybeSelectEmailCodeMethod = () => { - cy.get("body").then(($body) => { - const hasOtp = $body.find(otpSelector).length > 0; - if (hasOtp) return; - - const candidates = $body - .find("button,a") - .toArray() - .filter((el) => methodSelector.test((el.textContent || "").trim())); - - if (candidates.length > 0) { - cy.wrap(candidates[0]).click({ force: true }); - } - }); - }; - - const waitForOtpOrMethod = () => { - cy.get("body", { timeout: 60_000 }).should(($body) => { - const hasOtp = $body.find(otpSelector).length > 0; - const hasMethod = $body - .find("button,a") - .toArray() - .some((el) => methodSelector.test((el.textContent || "").trim())); - expect( - hasOtp || hasMethod, - "waiting for OTP input or verification method UI", - ).to.equal(true); - }); - }; - - waitForOtpOrMethod(); - maybeSelectEmailCodeMethod(); - - cy.get(otpSelector, { timeout: 60_000 }).first().clear().type(otp, { delay: 10 }); - - cy.get("body").then(($body) => { - const hasSubmit = $body - .find(continueSelector) - .toArray() - .some((el) => /verify|continue|sign in|confirm/i.test(el.textContent || "")); - if (hasSubmit) { - cy.contains(continueSelector, /verify|continue|sign in|confirm/i, { timeout: 20_000 }) - .should("be.visible") - .click({ force: true }); - } - }); - }, - ); - } else { - fillOtpAndSubmit(opts.otp); - } +Cypress.Commands.add("logoutLocalAuth", () => { + cy.visit("/", { + onBeforeLoad(win) { + win.sessionStorage.removeItem(LOCAL_AUTH_STORAGE_KEY); + }, }); }); @@ -212,15 +41,14 @@ declare global { waitForAppLoaded(): Chainable; /** - * Logs in via the real Clerk SignIn page using deterministic OTP credentials. - * - * Optional env vars (CYPRESS_*): - * - CLERK_ORIGIN (e.g. https://.accounts.dev) - * - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY (used to derive origin when CLERK_ORIGIN not set) - * - CLERK_TEST_EMAIL (default: jane+clerk_test@example.com) - * - CLERK_TEST_OTP (default: 424242) + * Seeds session storage with a local auth token for local-auth mode. */ - loginWithClerkOtp(): Chainable; + loginWithLocalAuth(token?: string): Chainable; + + /** + * Clears local auth token from session storage. + */ + logoutLocalAuth(): Chainable; } } } diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts index 369182a5..a1b252f3 100644 --- a/frontend/cypress/support/e2e.ts +++ b/frontend/cypress/support/e2e.ts @@ -5,6 +5,15 @@ import { addClerkCommands } from "@clerk/testing/cypress"; +// Clerk/Next.js occasionally throws a non-deterministic hydration mismatch +// on /sign-in. Ignore this known UI noise so E2E assertions can proceed. +Cypress.on("uncaught:exception", (err) => { + if (err?.message?.includes("Hydration failed")) { + return false; + } + return true; +}); + addClerkCommands({ Cypress, cy }); import "./commands"; diff --git a/frontend/cypress/support/testHooks.ts b/frontend/cypress/support/testHooks.ts new file mode 100644 index 00000000..4414132f --- /dev/null +++ b/frontend/cypress/support/testHooks.ts @@ -0,0 +1,77 @@ +/// + +type CommonPageTestHooksOptions = { + timeoutMs?: number; + orgMemberRole?: string; + organizationId?: string; + organizationName?: string; + userId?: string; + userEmail?: string; + userName?: string; +}; + +export function setupCommonPageTestHooks( + apiBase: string, + options: CommonPageTestHooksOptions = {}, +): void { + const { + timeoutMs = 20_000, + orgMemberRole = "owner", + organizationId = "org1", + organizationName = "Testing Org", + userId = "u1", + userEmail = "local-auth-user@example.com", + userName = "Local User", + } = options; + const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); + + beforeEach(() => { + Cypress.config("defaultCommandTimeout", timeoutMs); + + cy.intercept("GET", "**/healthz", { + statusCode: 200, + body: { ok: true }, + }).as("healthz"); + + cy.intercept("GET", `${apiBase}/users/me*`, { + statusCode: 200, + body: { + id: userId, + clerk_user_id: "local-auth-user", + email: userEmail, + name: userName, + preferred_name: userName, + timezone: "UTC", + }, + }).as("usersMe"); + + cy.intercept("GET", `${apiBase}/organizations/me/list*`, { + statusCode: 200, + body: [ + { + id: organizationId, + name: organizationName, + is_active: true, + role: orgMemberRole, + }, + ], + }).as("organizationsList"); + + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { + statusCode: 200, + body: { + id: "membership-1", + organization_id: organizationId, + user_id: userId, + role: orgMemberRole, + all_boards_read: true, + all_boards_write: true, + board_access: [], + }, + }).as("orgMeMember"); + }); + + afterEach(() => { + Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); + }); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index eb2e08eb..1b960dde 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3019,9 +3019,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -3033,9 +3033,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -3047,9 +3047,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -3061,9 +3061,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -3075,9 +3075,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -3089,9 +3089,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -3103,9 +3103,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -3117,9 +3117,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -3131,9 +3131,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -3145,9 +3145,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -3159,9 +3159,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -3173,9 +3173,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -3187,9 +3187,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -3201,9 +3201,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -3215,9 +3215,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -3229,9 +3229,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -3243,9 +3243,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -3257,9 +3257,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -3271,9 +3271,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -3285,9 +3285,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -3299,9 +3299,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -3313,9 +3313,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -3327,9 +3327,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -3341,9 +3341,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -3355,9 +3355,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -4187,13 +4187,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10465,9 +10465,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -12257,9 +12257,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -12273,31 +12273,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -13753,13 +13753,13 @@ } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" diff --git a/frontend/package.json b/frontend/package.json index b7305124..eae80f94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "start": "next start", "lint": "eslint", "test": "vitest run --passWithNoTests --coverage", + "test:full-coverage": "vitest run --passWithNoTests --coverage --config ./vitest.full-coverage.config.ts", "test:watch": "vitest", "dev:lan": "next dev --hostname 0.0.0.0 --port 3000", "api:gen": "orval --config ./orval.config.ts", diff --git a/frontend/src/api/generated/agent/agent.ts b/frontend/src/api/generated/agent/agent.ts index dbd5bfba..a419702c 100644 --- a/frontend/src/api/generated/agent/agent.ts +++ b/frontend/src/api/generated/agent/agent.ts @@ -22,7 +22,7 @@ import type { import type { AgentCreate, - AgentHeartbeatCreate, + AgentHealthStatusResponse, AgentNudge, AgentRead, ApprovalCreate, @@ -67,6 +67,352 @@ import { customFetch } from "../../mutator"; type SecondParameter unknown> = Parameters[1]; +/** + * Return agents visible to the caller, optionally filtered by board. + +Use when downstream routing or coordination needs recipient actors. + * @summary List visible agents + */ +export type listAgentsApiV1AgentAgentsGetResponse200 = { + data: LimitOffsetPageTypeVarCustomizedAgentRead; + status: 200; +}; + +export type listAgentsApiV1AgentAgentsGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type listAgentsApiV1AgentAgentsGetResponseSuccess = + listAgentsApiV1AgentAgentsGetResponse200 & { + headers: Headers; + }; +export type listAgentsApiV1AgentAgentsGetResponseError = + listAgentsApiV1AgentAgentsGetResponse422 & { + headers: Headers; + }; + +export type listAgentsApiV1AgentAgentsGetResponse = + | listAgentsApiV1AgentAgentsGetResponseSuccess + | listAgentsApiV1AgentAgentsGetResponseError; + +export const getListAgentsApiV1AgentAgentsGetUrl = ( + params?: ListAgentsApiV1AgentAgentsGetParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/agent/agents?${stringifiedParams}` + : `/api/v1/agent/agents`; +}; + +export const listAgentsApiV1AgentAgentsGet = async ( + params?: ListAgentsApiV1AgentAgentsGetParams, + options?: RequestInit, +): Promise => { + return customFetch( + getListAgentsApiV1AgentAgentsGetUrl(params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getListAgentsApiV1AgentAgentsGetQueryKey = ( + params?: ListAgentsApiV1AgentAgentsGetParams, +) => { + return [`/api/v1/agent/agents`, ...(params ? [params] : [])] as const; +}; + +export const getListAgentsApiV1AgentAgentsGetQueryOptions = < + TData = Awaited>, + TError = HTTPValidationError, +>( + params?: ListAgentsApiV1AgentAgentsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getListAgentsApiV1AgentAgentsGetQueryKey(params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + listAgentsApiV1AgentAgentsGet(params, { signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListAgentsApiV1AgentAgentsGetQueryResult = NonNullable< + Awaited> +>; +export type ListAgentsApiV1AgentAgentsGetQueryError = HTTPValidationError; + +export function useListAgentsApiV1AgentAgentsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + params: undefined | ListAgentsApiV1AgentAgentsGetParams, + options: { + query: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListAgentsApiV1AgentAgentsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + params?: ListAgentsApiV1AgentAgentsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListAgentsApiV1AgentAgentsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + params?: ListAgentsApiV1AgentAgentsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List visible agents + */ + +export function useListAgentsApiV1AgentAgentsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + params?: ListAgentsApiV1AgentAgentsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = getListAgentsApiV1AgentAgentsGetQueryOptions( + params, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Register a new board agent and attach it to the lead's board. + +The target board is derived from the caller identity and cannot be changed in payload. + * @summary Create a board agent as lead + */ +export type agentLeadCreateAgentResponse200 = { + data: AgentRead; + status: 200; +}; + +export type agentLeadCreateAgentResponse403 = { + data: LLMErrorResponse; + status: 403; +}; + +export type agentLeadCreateAgentResponse409 = { + data: LLMErrorResponse; + status: 409; +}; + +export type agentLeadCreateAgentResponse422 = { + data: LLMErrorResponse; + status: 422; +}; + +export type agentLeadCreateAgentResponseSuccess = + agentLeadCreateAgentResponse200 & { + headers: Headers; + }; +export type agentLeadCreateAgentResponseError = ( + | agentLeadCreateAgentResponse403 + | agentLeadCreateAgentResponse409 + | agentLeadCreateAgentResponse422 +) & { + headers: Headers; +}; + +export type agentLeadCreateAgentResponse = + | agentLeadCreateAgentResponseSuccess + | agentLeadCreateAgentResponseError; + +export const getAgentLeadCreateAgentUrl = () => { + return `/api/v1/agent/agents`; +}; + +export const agentLeadCreateAgent = async ( + agentCreate: AgentCreate, + options?: RequestInit, +): Promise => { + return customFetch( + getAgentLeadCreateAgentUrl(), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(agentCreate), + }, + ); +}; + +export const getAgentLeadCreateAgentMutationOptions = < + TError = LLMErrorResponse, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: AgentCreate }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: AgentCreate }, + TContext +> => { + const mutationKey = ["agentLeadCreateAgent"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: AgentCreate } + > = (props) => { + const { data } = props ?? {}; + + return agentLeadCreateAgent(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AgentLeadCreateAgentMutationResult = NonNullable< + Awaited> +>; +export type AgentLeadCreateAgentMutationBody = AgentCreate; +export type AgentLeadCreateAgentMutationError = LLMErrorResponse; + +/** + * @summary Create a board agent as lead + */ +export const useAgentLeadCreateAgent = < + TError = LLMErrorResponse, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: AgentCreate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { data: AgentCreate }, + TContext +> => { + return useMutation( + getAgentLeadCreateAgentMutationOptions(options), + queryClient, + ); +}; /** * Return boards the authenticated agent can access. @@ -486,36 +832,745 @@ export function useGetBoardApiV1AgentBoardsBoardIdGet< } /** - * Return agents visible to the caller, optionally filtered by board. + * Permanently remove a board agent and tear down associated lifecycle state. -Use when downstream routing or coordination needs recipient actors. - * @summary List visible agents +Use sparingly; prefer reassignment for continuity-sensitive teams. + * @summary Delete a board agent as lead */ -export type listAgentsApiV1AgentAgentsGetResponse200 = { - data: LimitOffsetPageTypeVarCustomizedAgentRead; +export type agentLeadDeleteBoardAgentResponse200 = { + data: OkResponse; status: 200; }; -export type listAgentsApiV1AgentAgentsGetResponse422 = { +export type agentLeadDeleteBoardAgentResponse403 = { + data: LLMErrorResponse; + status: 403; +}; + +export type agentLeadDeleteBoardAgentResponse404 = { + data: LLMErrorResponse; + status: 404; +}; + +export type agentLeadDeleteBoardAgentResponse422 = { data: HTTPValidationError; status: 422; }; -export type listAgentsApiV1AgentAgentsGetResponseSuccess = - listAgentsApiV1AgentAgentsGetResponse200 & { +export type agentLeadDeleteBoardAgentResponseSuccess = + agentLeadDeleteBoardAgentResponse200 & { headers: Headers; }; -export type listAgentsApiV1AgentAgentsGetResponseError = - listAgentsApiV1AgentAgentsGetResponse422 & { +export type agentLeadDeleteBoardAgentResponseError = ( + | agentLeadDeleteBoardAgentResponse403 + | agentLeadDeleteBoardAgentResponse404 + | agentLeadDeleteBoardAgentResponse422 +) & { + headers: Headers; +}; + +export type agentLeadDeleteBoardAgentResponse = + | agentLeadDeleteBoardAgentResponseSuccess + | agentLeadDeleteBoardAgentResponseError; + +export const getAgentLeadDeleteBoardAgentUrl = ( + boardId: string, + agentId: string, +) => { + return `/api/v1/agent/boards/${boardId}/agents/${agentId}`; +}; + +export const agentLeadDeleteBoardAgent = async ( + boardId: string, + agentId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getAgentLeadDeleteBoardAgentUrl(boardId, agentId), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getAgentLeadDeleteBoardAgentMutationOptions = < + TError = LLMErrorResponse | HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string }, + TContext +> => { + const mutationKey = ["agentLeadDeleteBoardAgent"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { boardId: string; agentId: string } + > = (props) => { + const { boardId, agentId } = props ?? {}; + + return agentLeadDeleteBoardAgent(boardId, agentId, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AgentLeadDeleteBoardAgentMutationResult = NonNullable< + Awaited> +>; + +export type AgentLeadDeleteBoardAgentMutationError = + | LLMErrorResponse + | HTTPValidationError; + +/** + * @summary Delete a board agent as lead + */ +export const useAgentLeadDeleteBoardAgent = < + TError = LLMErrorResponse | HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { boardId: string; agentId: string }, + TContext +> => { + return useMutation( + getAgentLeadDeleteBoardAgentMutationOptions(options), + queryClient, + ); +}; +/** + * Send a direct coordination message to a specific board agent. + +Use this when a lead sees stalled, idle, or misaligned work. + * @summary Nudge an agent on a board + */ +export type agentLeadNudgeAgentResponse200 = { + data: OkResponse; + status: 200; +}; + +export type agentLeadNudgeAgentResponse403 = { + data: LLMErrorResponse; + status: 403; +}; + +export type agentLeadNudgeAgentResponse404 = { + data: LLMErrorResponse; + status: 404; +}; + +export type agentLeadNudgeAgentResponse422 = { + data: LLMErrorResponse; + status: 422; +}; + +export type agentLeadNudgeAgentResponse502 = { + data: LLMErrorResponse; + status: 502; +}; + +export type agentLeadNudgeAgentResponseSuccess = + agentLeadNudgeAgentResponse200 & { + headers: Headers; + }; +export type agentLeadNudgeAgentResponseError = ( + | agentLeadNudgeAgentResponse403 + | agentLeadNudgeAgentResponse404 + | agentLeadNudgeAgentResponse422 + | agentLeadNudgeAgentResponse502 +) & { + headers: Headers; +}; + +export type agentLeadNudgeAgentResponse = + | agentLeadNudgeAgentResponseSuccess + | agentLeadNudgeAgentResponseError; + +export const getAgentLeadNudgeAgentUrl = (boardId: string, agentId: string) => { + return `/api/v1/agent/boards/${boardId}/agents/${agentId}/nudge`; +}; + +export const agentLeadNudgeAgent = async ( + boardId: string, + agentId: string, + agentNudge: AgentNudge, + options?: RequestInit, +): Promise => { + return customFetch( + getAgentLeadNudgeAgentUrl(boardId, agentId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(agentNudge), + }, + ); +}; + +export const getAgentLeadNudgeAgentMutationOptions = < + TError = LLMErrorResponse, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string; data: AgentNudge }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string; data: AgentNudge }, + TContext +> => { + const mutationKey = ["agentLeadNudgeAgent"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { boardId: string; agentId: string; data: AgentNudge } + > = (props) => { + const { boardId, agentId, data } = props ?? {}; + + return agentLeadNudgeAgent(boardId, agentId, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AgentLeadNudgeAgentMutationResult = NonNullable< + Awaited> +>; +export type AgentLeadNudgeAgentMutationBody = AgentNudge; +export type AgentLeadNudgeAgentMutationError = LLMErrorResponse; + +/** + * @summary Nudge an agent on a board + */ +export const useAgentLeadNudgeAgent = < + TError = LLMErrorResponse, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string; data: AgentNudge }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { boardId: string; agentId: string; data: AgentNudge }, + TContext +> => { + return useMutation( + getAgentLeadNudgeAgentMutationOptions(options), + queryClient, + ); +}; +/** + * Fetch an agent's SOUL.md content. + +Allowed for board lead, or for an agent reading its own SOUL. + * @summary Get Agent Soul + */ +export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse200 = + { + data: string; + status: 200; + }; + +export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponseSuccess = + getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse200 & { + headers: Headers; + }; +export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponseError = + getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse422 & { headers: Headers; }; -export type listAgentsApiV1AgentAgentsGetResponse = - | listAgentsApiV1AgentAgentsGetResponseSuccess - | listAgentsApiV1AgentAgentsGetResponseError; +export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse = + | getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponseSuccess + | getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponseError; -export const getListAgentsApiV1AgentAgentsGetUrl = ( - params?: ListAgentsApiV1AgentAgentsGetParams, +export const getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetUrl = ( + boardId: string, + agentId: string, +) => { + return `/api/v1/agent/boards/${boardId}/agents/${agentId}/soul`; +}; + +export const getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet = async ( + boardId: string, + agentId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetUrl( + boardId, + agentId, + ), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryKey = + (boardId: string, agentId: string) => { + return [`/api/v1/agent/boards/${boardId}/agents/${agentId}/soul`] as const; + }; + +export const getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryOptions = + < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, + >( + boardId: string, + agentId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + ) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryKey( + boardId, + agentId, + ); + + const queryFn: QueryFunction< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + > + > = ({ signal }) => + getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet( + boardId, + agentId, + { signal, ...requestOptions }, + ); + + return { + queryKey, + queryFn, + enabled: !!(boardId && agentId), + ...queryOptions, + } as UseQueryOptions< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + >, + TError, + TData + > & { queryKey: DataTag }; + }; + +export type GetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type GetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryError = + HTTPValidationError; + +export function useGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + agentId: string, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + >, + TError, + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + agentId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + >, + TError, + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + agentId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Get Agent Soul + */ + +export function useGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + agentId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryOptions( + boardId, + agentId, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Write SOUL.md content for a board agent and persist it for reprovisioning. + +Use this when role instructions or behavior guardrails need updates. + * @summary Update an agent's SOUL template + */ +export type agentLeadUpdateAgentSoulResponse200 = { + data: OkResponse; + status: 200; +}; + +export type agentLeadUpdateAgentSoulResponse403 = { + data: LLMErrorResponse; + status: 403; +}; + +export type agentLeadUpdateAgentSoulResponse404 = { + data: LLMErrorResponse; + status: 404; +}; + +export type agentLeadUpdateAgentSoulResponse422 = { + data: LLMErrorResponse; + status: 422; +}; + +export type agentLeadUpdateAgentSoulResponse502 = { + data: LLMErrorResponse; + status: 502; +}; + +export type agentLeadUpdateAgentSoulResponseSuccess = + agentLeadUpdateAgentSoulResponse200 & { + headers: Headers; + }; +export type agentLeadUpdateAgentSoulResponseError = ( + | agentLeadUpdateAgentSoulResponse403 + | agentLeadUpdateAgentSoulResponse404 + | agentLeadUpdateAgentSoulResponse422 + | agentLeadUpdateAgentSoulResponse502 +) & { + headers: Headers; +}; + +export type agentLeadUpdateAgentSoulResponse = + | agentLeadUpdateAgentSoulResponseSuccess + | agentLeadUpdateAgentSoulResponseError; + +export const getAgentLeadUpdateAgentSoulUrl = ( + boardId: string, + agentId: string, +) => { + return `/api/v1/agent/boards/${boardId}/agents/${agentId}/soul`; +}; + +export const agentLeadUpdateAgentSoul = async ( + boardId: string, + agentId: string, + soulUpdateRequest: SoulUpdateRequest, + options?: RequestInit, +): Promise => { + return customFetch( + getAgentLeadUpdateAgentSoulUrl(boardId, agentId), + { + ...options, + method: "PUT", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(soulUpdateRequest), + }, + ); +}; + +export const getAgentLeadUpdateAgentSoulMutationOptions = < + TError = LLMErrorResponse, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string; data: SoulUpdateRequest }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string; data: SoulUpdateRequest }, + TContext +> => { + const mutationKey = ["agentLeadUpdateAgentSoul"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { boardId: string; agentId: string; data: SoulUpdateRequest } + > = (props) => { + const { boardId, agentId, data } = props ?? {}; + + return agentLeadUpdateAgentSoul(boardId, agentId, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AgentLeadUpdateAgentSoulMutationResult = NonNullable< + Awaited> +>; +export type AgentLeadUpdateAgentSoulMutationBody = SoulUpdateRequest; +export type AgentLeadUpdateAgentSoulMutationError = LLMErrorResponse; + +/** + * @summary Update an agent's SOUL template + */ +export const useAgentLeadUpdateAgentSoul = < + TError = LLMErrorResponse, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; agentId: string; data: SoulUpdateRequest }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { boardId: string; agentId: string; data: SoulUpdateRequest }, + TContext +> => { + return useMutation( + getAgentLeadUpdateAgentSoulMutationOptions(options), + queryClient, + ); +}; +/** + * List approvals for a board. + +Use status filtering to process pending approvals efficiently. + * @summary List Approvals + */ +export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse200 = { + data: LimitOffsetPageTypeVarCustomizedApprovalRead; + status: 200; +}; + +export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponseSuccess = + listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse200 & { + headers: Headers; + }; +export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponseError = + listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse422 & { + headers: Headers; + }; + +export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse = + | listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponseSuccess + | listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponseError; + +export const getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetUrl = ( + boardId: string, + params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, ) => { const normalizedParams = new URLSearchParams(); @@ -528,16 +1583,17 @@ export const getListAgentsApiV1AgentAgentsGetUrl = ( const stringifiedParams = normalizedParams.toString(); return stringifiedParams.length > 0 - ? `/api/v1/agent/agents?${stringifiedParams}` - : `/api/v1/agent/agents`; + ? `/api/v1/agent/boards/${boardId}/approvals?${stringifiedParams}` + : `/api/v1/agent/boards/${boardId}/approvals`; }; -export const listAgentsApiV1AgentAgentsGet = async ( - params?: ListAgentsApiV1AgentAgentsGetParams, +export const listApprovalsApiV1AgentBoardsBoardIdApprovalsGet = async ( + boardId: string, + params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, options?: RequestInit, -): Promise => { - return customFetch( - getListAgentsApiV1AgentAgentsGetUrl(params), +): Promise => { + return customFetch( + getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetUrl(boardId, params), { ...options, method: "GET", @@ -545,21 +1601,30 @@ export const listAgentsApiV1AgentAgentsGet = async ( ); }; -export const getListAgentsApiV1AgentAgentsGetQueryKey = ( - params?: ListAgentsApiV1AgentAgentsGetParams, +export const getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryKey = ( + boardId: string, + params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, ) => { - return [`/api/v1/agent/agents`, ...(params ? [params] : [])] as const; + return [ + `/api/v1/agent/boards/${boardId}/approvals`, + ...(params ? [params] : []), + ] as const; }; -export const getListAgentsApiV1AgentAgentsGetQueryOptions = < - TData = Awaited>, +export const getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryOptions = < + TData = Awaited< + ReturnType + >, TError = HTTPValidationError, >( - params?: ListAgentsApiV1AgentAgentsGetParams, + boardId: string, + params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited< + ReturnType + >, TError, TData > @@ -570,43 +1635,68 @@ export const getListAgentsApiV1AgentAgentsGetQueryOptions = < const { query: queryOptions, request: requestOptions } = options ?? {}; const queryKey = - queryOptions?.queryKey ?? getListAgentsApiV1AgentAgentsGetQueryKey(params); + queryOptions?.queryKey ?? + getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryKey( + boardId, + params, + ); const queryFn: QueryFunction< - Awaited> + Awaited> > = ({ signal }) => - listAgentsApiV1AgentAgentsGet(params, { signal, ...requestOptions }); + listApprovalsApiV1AgentBoardsBoardIdApprovalsGet(boardId, params, { + signal, + ...requestOptions, + }); - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, + return { + queryKey, + queryFn, + enabled: !!boardId, + ...queryOptions, + } as UseQueryOptions< + Awaited< + ReturnType + >, TError, TData > & { queryKey: DataTag }; }; -export type ListAgentsApiV1AgentAgentsGetQueryResult = NonNullable< - Awaited> ->; -export type ListAgentsApiV1AgentAgentsGetQueryError = HTTPValidationError; +export type ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryResult = + NonNullable< + Awaited> + >; +export type ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryError = + HTTPValidationError; -export function useListAgentsApiV1AgentAgentsGet< - TData = Awaited>, +export function useListApprovalsApiV1AgentBoardsBoardIdApprovalsGet< + TData = Awaited< + ReturnType + >, TError = HTTPValidationError, >( - params: undefined | ListAgentsApiV1AgentAgentsGetParams, + boardId: string, + params: undefined | ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, options: { query: Partial< UseQueryOptions< - Awaited>, + Awaited< + ReturnType + >, TError, TData > > & Pick< DefinedInitialDataOptions< - Awaited>, + Awaited< + ReturnType + >, TError, - Awaited> + Awaited< + ReturnType + > >, "initialData" >; @@ -616,24 +1706,33 @@ export function useListAgentsApiV1AgentAgentsGet< ): DefinedUseQueryResult & { queryKey: DataTag; }; -export function useListAgentsApiV1AgentAgentsGet< - TData = Awaited>, +export function useListApprovalsApiV1AgentBoardsBoardIdApprovalsGet< + TData = Awaited< + ReturnType + >, TError = HTTPValidationError, >( - params?: ListAgentsApiV1AgentAgentsGetParams, + boardId: string, + params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited< + ReturnType + >, TError, TData > > & Pick< UndefinedInitialDataOptions< - Awaited>, + Awaited< + ReturnType + >, TError, - Awaited> + Awaited< + ReturnType + > >, "initialData" >; @@ -643,15 +1742,20 @@ export function useListAgentsApiV1AgentAgentsGet< ): UseQueryResult & { queryKey: DataTag; }; -export function useListAgentsApiV1AgentAgentsGet< - TData = Awaited>, +export function useListApprovalsApiV1AgentBoardsBoardIdApprovalsGet< + TData = Awaited< + ReturnType + >, TError = HTTPValidationError, >( - params?: ListAgentsApiV1AgentAgentsGetParams, + boardId: string, + params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited< + ReturnType + >, TError, TData > @@ -663,18 +1767,23 @@ export function useListAgentsApiV1AgentAgentsGet< queryKey: DataTag; }; /** - * @summary List visible agents + * @summary List Approvals */ -export function useListAgentsApiV1AgentAgentsGet< - TData = Awaited>, +export function useListApprovalsApiV1AgentBoardsBoardIdApprovalsGet< + TData = Awaited< + ReturnType + >, TError = HTTPValidationError, >( - params?: ListAgentsApiV1AgentAgentsGetParams, + boardId: string, + params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited< + ReturnType + >, TError, TData > @@ -685,8 +1794,1077 @@ export function useListAgentsApiV1AgentAgentsGet< ): UseQueryResult & { queryKey: DataTag; } { - const queryOptions = getListAgentsApiV1AgentAgentsGetQueryOptions( - params, + const queryOptions = + getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryOptions( + boardId, + params, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Create an approval request for risky or low-confidence actions. + +Include `task_id` or `task_ids` to scope the decision precisely. + * @summary Create Approval + */ +export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse200 = { + data: ApprovalRead; + status: 200; +}; + +export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponseSuccess = + createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse200 & { + headers: Headers; + }; +export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponseError = + createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse422 & { + headers: Headers; + }; + +export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse = + | createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponseSuccess + | createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponseError; + +export const getCreateApprovalApiV1AgentBoardsBoardIdApprovalsPostUrl = ( + boardId: string, +) => { + return `/api/v1/agent/boards/${boardId}/approvals`; +}; + +export const createApprovalApiV1AgentBoardsBoardIdApprovalsPost = async ( + boardId: string, + approvalCreate: ApprovalCreate, + options?: RequestInit, +): Promise => { + return customFetch( + getCreateApprovalApiV1AgentBoardsBoardIdApprovalsPostUrl(boardId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(approvalCreate), + }, + ); +}; + +export const getCreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: ApprovalCreate }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: ApprovalCreate }, + TContext + > => { + const mutationKey = ["createApprovalApiV1AgentBoardsBoardIdApprovalsPost"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType + >, + { boardId: string; data: ApprovalCreate } + > = (props) => { + const { boardId, data } = props ?? {}; + + return createApprovalApiV1AgentBoardsBoardIdApprovalsPost( + boardId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type CreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type CreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationBody = + ApprovalCreate; +export type CreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationError = + HTTPValidationError; + +/** + * @summary Create Approval + */ +export const useCreateApprovalApiV1AgentBoardsBoardIdApprovalsPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: ApprovalCreate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: ApprovalCreate }, + TContext +> => { + return useMutation( + getCreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationOptions( + options, + ), + queryClient, + ); +}; +/** + * Escalate a high-impact decision or ambiguity through the gateway-main interaction channel. + +Use when lead-level context needs human confirmation or consent. + * @summary Ask the human via gateway-main + */ +export type agentLeadAskUserViaGatewayMainResponse200 = { + data: GatewayMainAskUserResponse; + status: 200; +}; + +export type agentLeadAskUserViaGatewayMainResponse403 = { + data: LLMErrorResponse; + status: 403; +}; + +export type agentLeadAskUserViaGatewayMainResponse404 = { + data: LLMErrorResponse; + status: 404; +}; + +export type agentLeadAskUserViaGatewayMainResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type agentLeadAskUserViaGatewayMainResponse502 = { + data: LLMErrorResponse; + status: 502; +}; + +export type agentLeadAskUserViaGatewayMainResponseSuccess = + agentLeadAskUserViaGatewayMainResponse200 & { + headers: Headers; + }; +export type agentLeadAskUserViaGatewayMainResponseError = ( + | agentLeadAskUserViaGatewayMainResponse403 + | agentLeadAskUserViaGatewayMainResponse404 + | agentLeadAskUserViaGatewayMainResponse422 + | agentLeadAskUserViaGatewayMainResponse502 +) & { + headers: Headers; +}; + +export type agentLeadAskUserViaGatewayMainResponse = + | agentLeadAskUserViaGatewayMainResponseSuccess + | agentLeadAskUserViaGatewayMainResponseError; + +export const getAgentLeadAskUserViaGatewayMainUrl = (boardId: string) => { + return `/api/v1/agent/boards/${boardId}/gateway/main/ask-user`; +}; + +export const agentLeadAskUserViaGatewayMain = async ( + boardId: string, + gatewayMainAskUserRequest: GatewayMainAskUserRequest, + options?: RequestInit, +): Promise => { + return customFetch( + getAgentLeadAskUserViaGatewayMainUrl(boardId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(gatewayMainAskUserRequest), + }, + ); +}; + +export const getAgentLeadAskUserViaGatewayMainMutationOptions = < + TError = LLMErrorResponse | HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; data: GatewayMainAskUserRequest }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { boardId: string; data: GatewayMainAskUserRequest }, + TContext +> => { + const mutationKey = ["agentLeadAskUserViaGatewayMain"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { boardId: string; data: GatewayMainAskUserRequest } + > = (props) => { + const { boardId, data } = props ?? {}; + + return agentLeadAskUserViaGatewayMain(boardId, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AgentLeadAskUserViaGatewayMainMutationResult = NonNullable< + Awaited> +>; +export type AgentLeadAskUserViaGatewayMainMutationBody = + GatewayMainAskUserRequest; +export type AgentLeadAskUserViaGatewayMainMutationError = + | LLMErrorResponse + | HTTPValidationError; + +/** + * @summary Ask the human via gateway-main + */ +export const useAgentLeadAskUserViaGatewayMain = < + TError = LLMErrorResponse | HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; data: GatewayMainAskUserRequest }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { boardId: string; data: GatewayMainAskUserRequest }, + TContext +> => { + return useMutation( + getAgentLeadAskUserViaGatewayMainMutationOptions(options), + queryClient, + ); +}; +/** + * List board memory with optional chat filtering. + +Use `is_chat=false` for durable context and `is_chat=true` for board chat. + * @summary List Board Memory + */ +export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse200 = { + data: LimitOffsetPageTypeVarCustomizedBoardMemoryRead; + status: 200; +}; + +export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponseSuccess = + listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse200 & { + headers: Headers; + }; +export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponseError = + listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse422 & { + headers: Headers; + }; + +export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse = + | listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponseSuccess + | listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponseError; + +export const getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetUrl = ( + boardId: string, + params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/agent/boards/${boardId}/memory?${stringifiedParams}` + : `/api/v1/agent/boards/${boardId}/memory`; +}; + +export const listBoardMemoryApiV1AgentBoardsBoardIdMemoryGet = async ( + boardId: string, + params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, + options?: RequestInit, +): Promise => { + return customFetch( + getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetUrl(boardId, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryKey = ( + boardId: string, + params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, +) => { + return [ + `/api/v1/agent/boards/${boardId}/memory`, + ...(params ? [params] : []), + ] as const; +}; + +export const getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryKey(boardId, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + listBoardMemoryApiV1AgentBoardsBoardIdMemoryGet(boardId, params, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!boardId, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryResult = + NonNullable< + Awaited> + >; +export type ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryError = + HTTPValidationError; + +export function useListBoardMemoryApiV1AgentBoardsBoardIdMemoryGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params: undefined | ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListBoardMemoryApiV1AgentBoardsBoardIdMemoryGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListBoardMemoryApiV1AgentBoardsBoardIdMemoryGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List Board Memory + */ + +export function useListBoardMemoryApiV1AgentBoardsBoardIdMemoryGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryOptions( + boardId, + params, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Create a board memory entry. + +Use tags to indicate purpose (e.g. `chat`, `decision`, `plan`, `handoff`). + * @summary Create Board Memory + */ +export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse200 = { + data: BoardMemoryRead; + status: 200; +}; + +export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponseSuccess = + createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse200 & { + headers: Headers; + }; +export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponseError = + createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse422 & { + headers: Headers; + }; + +export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse = + | createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponseSuccess + | createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponseError; + +export const getCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostUrl = ( + boardId: string, +) => { + return `/api/v1/agent/boards/${boardId}/memory`; +}; + +export const createBoardMemoryApiV1AgentBoardsBoardIdMemoryPost = async ( + boardId: string, + boardMemoryCreate: BoardMemoryCreate, + options?: RequestInit, +): Promise => { + return customFetch( + getCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostUrl(boardId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(boardMemoryCreate), + }, + ); +}; + +export const getCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardMemoryCreate }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardMemoryCreate }, + TContext + > => { + const mutationKey = ["createBoardMemoryApiV1AgentBoardsBoardIdMemoryPost"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType + >, + { boardId: string; data: BoardMemoryCreate } + > = (props) => { + const { boardId, data } = props ?? {}; + + return createBoardMemoryApiV1AgentBoardsBoardIdMemoryPost( + boardId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type CreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type CreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationBody = + BoardMemoryCreate; +export type CreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationError = + HTTPValidationError; + +/** + * @summary Create Board Memory + */ +export const useCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardMemoryCreate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardMemoryCreate }, + TContext +> => { + return useMutation( + getCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationOptions( + options, + ), + queryClient, + ); +}; +/** + * Apply board onboarding updates from an agent workflow. + +Used during structured objective/success-metric intake loops. + * @summary Update Onboarding + */ +export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse200 = { + data: BoardOnboardingRead; + status: 200; +}; + +export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponseSuccess = + updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse200 & { + headers: Headers; + }; +export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponseError = + updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse422 & { + headers: Headers; + }; + +export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse = + | updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponseSuccess + | updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponseError; + +export const getUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostUrl = ( + boardId: string, +) => { + return `/api/v1/agent/boards/${boardId}/onboarding`; +}; + +export const updateOnboardingApiV1AgentBoardsBoardIdOnboardingPost = async ( + boardId: string, + boardOnboardingAgentCompleteBoardOnboardingAgentQuestion: + | BoardOnboardingAgentComplete + | BoardOnboardingAgentQuestion, + options?: RequestInit, +): Promise => { + return customFetch( + getUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostUrl(boardId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify( + boardOnboardingAgentCompleteBoardOnboardingAgentQuestion, + ), + }, + ); +}; + +export const getUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { + boardId: string; + data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; + }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { + boardId: string; + data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; + }, + TContext + > => { + const mutationKey = [ + "updateOnboardingApiV1AgentBoardsBoardIdOnboardingPost", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType + >, + { + boardId: string; + data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; + } + > = (props) => { + const { boardId, data } = props ?? {}; + + return updateOnboardingApiV1AgentBoardsBoardIdOnboardingPost( + boardId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type UpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type UpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationBody = + | BoardOnboardingAgentComplete + | BoardOnboardingAgentQuestion; +export type UpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationError = + HTTPValidationError; + +/** + * @summary Update Onboarding + */ +export const useUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { + boardId: string; + data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; + }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { + boardId: string; + data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; + }, + TContext +> => { + return useMutation( + getUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationOptions( + options, + ), + queryClient, + ); +}; +/** + * List available tags for the board's organization. + +Use returned ids in task create/update payloads (`tag_ids`). + * @summary List Tags + */ +export type listTagsApiV1AgentBoardsBoardIdTagsGetResponse200 = { + data: TagRef[]; + status: 200; +}; + +export type listTagsApiV1AgentBoardsBoardIdTagsGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type listTagsApiV1AgentBoardsBoardIdTagsGetResponseSuccess = + listTagsApiV1AgentBoardsBoardIdTagsGetResponse200 & { + headers: Headers; + }; +export type listTagsApiV1AgentBoardsBoardIdTagsGetResponseError = + listTagsApiV1AgentBoardsBoardIdTagsGetResponse422 & { + headers: Headers; + }; + +export type listTagsApiV1AgentBoardsBoardIdTagsGetResponse = + | listTagsApiV1AgentBoardsBoardIdTagsGetResponseSuccess + | listTagsApiV1AgentBoardsBoardIdTagsGetResponseError; + +export const getListTagsApiV1AgentBoardsBoardIdTagsGetUrl = ( + boardId: string, +) => { + return `/api/v1/agent/boards/${boardId}/tags`; +}; + +export const listTagsApiV1AgentBoardsBoardIdTagsGet = async ( + boardId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getListTagsApiV1AgentBoardsBoardIdTagsGetUrl(boardId), + { + ...options, + method: "GET", + }, + ); +}; + +export const getListTagsApiV1AgentBoardsBoardIdTagsGetQueryKey = ( + boardId: string, +) => { + return [`/api/v1/agent/boards/${boardId}/tags`] as const; +}; + +export const getListTagsApiV1AgentBoardsBoardIdTagsGetQueryOptions = < + TData = Awaited>, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getListTagsApiV1AgentBoardsBoardIdTagsGetQueryKey(boardId); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + listTagsApiV1AgentBoardsBoardIdTagsGet(boardId, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!boardId, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListTagsApiV1AgentBoardsBoardIdTagsGetQueryResult = NonNullable< + Awaited> +>; +export type ListTagsApiV1AgentBoardsBoardIdTagsGetQueryError = + HTTPValidationError; + +export function useListTagsApiV1AgentBoardsBoardIdTagsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + boardId: string, + options: { + query: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListTagsApiV1AgentBoardsBoardIdTagsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListTagsApiV1AgentBoardsBoardIdTagsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List Tags + */ + +export function useListTagsApiV1AgentBoardsBoardIdTagsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = getListTagsApiV1AgentBoardsBoardIdTagsGetQueryOptions( + boardId, options, ); @@ -698,139 +2876,6 @@ export function useListAgentsApiV1AgentAgentsGet< return { ...query, queryKey: queryOptions.queryKey }; } -/** - * Register a new board agent and attach it to the lead's board. - -The target board is derived from the caller identity and cannot be changed in payload. - * @summary Create a board agent as lead - */ -export type agentLeadCreateAgentResponse200 = { - data: AgentRead; - status: 200; -}; - -export type agentLeadCreateAgentResponse403 = { - data: LLMErrorResponse; - status: 403; -}; - -export type agentLeadCreateAgentResponse409 = { - data: LLMErrorResponse; - status: 409; -}; - -export type agentLeadCreateAgentResponse422 = { - data: LLMErrorResponse; - status: 422; -}; - -export type agentLeadCreateAgentResponseSuccess = - agentLeadCreateAgentResponse200 & { - headers: Headers; - }; -export type agentLeadCreateAgentResponseError = ( - | agentLeadCreateAgentResponse403 - | agentLeadCreateAgentResponse409 - | agentLeadCreateAgentResponse422 -) & { - headers: Headers; -}; - -export type agentLeadCreateAgentResponse = - | agentLeadCreateAgentResponseSuccess - | agentLeadCreateAgentResponseError; - -export const getAgentLeadCreateAgentUrl = () => { - return `/api/v1/agent/agents`; -}; - -export const agentLeadCreateAgent = async ( - agentCreate: AgentCreate, - options?: RequestInit, -): Promise => { - return customFetch( - getAgentLeadCreateAgentUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(agentCreate), - }, - ); -}; - -export const getAgentLeadCreateAgentMutationOptions = < - TError = LLMErrorResponse, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: AgentCreate }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: AgentCreate }, - TContext -> => { - const mutationKey = ["agentLeadCreateAgent"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: AgentCreate } - > = (props) => { - const { data } = props ?? {}; - - return agentLeadCreateAgent(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type AgentLeadCreateAgentMutationResult = NonNullable< - Awaited> ->; -export type AgentLeadCreateAgentMutationBody = AgentCreate; -export type AgentLeadCreateAgentMutationError = LLMErrorResponse; - -/** - * @summary Create a board agent as lead - */ -export const useAgentLeadCreateAgent = < - TError = LLMErrorResponse, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: AgentCreate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: AgentCreate }, - TContext -> => { - return useMutation( - getAgentLeadCreateAgentMutationOptions(options), - queryClient, - ); -}; /** * List tasks on a board with status/assignment filters. @@ -1210,216 +3255,145 @@ export const useAgentLeadCreateTask = < ); }; /** - * List available tags for the board's organization. + * Delete a board task and related records. -Use returned ids in task create/update payloads (`tag_ids`). - * @summary List Tags +This action is restricted to board lead agents. + * @summary Delete a task as board lead */ -export type listTagsApiV1AgentBoardsBoardIdTagsGetResponse200 = { - data: TagRef[]; +export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse200 = { + data: OkResponse; status: 200; }; -export type listTagsApiV1AgentBoardsBoardIdTagsGetResponse422 = { +export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse422 = { data: HTTPValidationError; status: 422; }; -export type listTagsApiV1AgentBoardsBoardIdTagsGetResponseSuccess = - listTagsApiV1AgentBoardsBoardIdTagsGetResponse200 & { +export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponseSuccess = + deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse200 & { headers: Headers; }; -export type listTagsApiV1AgentBoardsBoardIdTagsGetResponseError = - listTagsApiV1AgentBoardsBoardIdTagsGetResponse422 & { +export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponseError = + deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse422 & { headers: Headers; }; -export type listTagsApiV1AgentBoardsBoardIdTagsGetResponse = - | listTagsApiV1AgentBoardsBoardIdTagsGetResponseSuccess - | listTagsApiV1AgentBoardsBoardIdTagsGetResponseError; +export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse = + | deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponseSuccess + | deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponseError; -export const getListTagsApiV1AgentBoardsBoardIdTagsGetUrl = ( +export const getDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteUrl = ( boardId: string, + taskId: string, ) => { - return `/api/v1/agent/boards/${boardId}/tags`; + return `/api/v1/agent/boards/${boardId}/tasks/${taskId}`; }; -export const listTagsApiV1AgentBoardsBoardIdTagsGet = async ( +export const deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDelete = async ( boardId: string, + taskId: string, options?: RequestInit, -): Promise => { - return customFetch( - getListTagsApiV1AgentBoardsBoardIdTagsGetUrl(boardId), +): Promise => { + return customFetch( + getDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteUrl(boardId, taskId), { ...options, - method: "GET", + method: "DELETE", }, ); }; -export const getListTagsApiV1AgentBoardsBoardIdTagsGetQueryKey = ( - boardId: string, -) => { - return [`/api/v1/agent/boards/${boardId}/tags`] as const; -}; - -export const getListTagsApiV1AgentBoardsBoardIdTagsGetQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - boardId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > +export const getDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; taskId: string }, + TContext >; request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getListTagsApiV1AgentBoardsBoardIdTagsGetQueryKey(boardId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - listTagsApiV1AgentBoardsBoardIdTagsGet(boardId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!boardId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, + }): UseMutationOptions< + Awaited< + ReturnType + >, TError, - TData - > & { queryKey: DataTag }; -}; + { boardId: string; taskId: string }, + TContext + > => { + const mutationKey = ["deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDelete"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; -export type ListTagsApiV1AgentBoardsBoardIdTagsGetQueryResult = NonNullable< - Awaited> ->; -export type ListTagsApiV1AgentBoardsBoardIdTagsGetQueryError = + const mutationFn: MutationFunction< + Awaited< + ReturnType + >, + { boardId: string; taskId: string } + > = (props) => { + const { boardId, taskId } = props ?? {}; + + return deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDelete( + boardId, + taskId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type DeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteMutationResult = + NonNullable< + Awaited< + ReturnType + > + >; + +export type DeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteMutationError = HTTPValidationError; -export function useListTagsApiV1AgentBoardsBoardIdTagsGet< - TData = Awaited>, - TError = HTTPValidationError, ->( - boardId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useListTagsApiV1AgentBoardsBoardIdTagsGet< - TData = Awaited>, - TError = HTTPValidationError, ->( - boardId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useListTagsApiV1AgentBoardsBoardIdTagsGet< - TData = Awaited>, - TError = HTTPValidationError, ->( - boardId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; /** - * @summary List Tags + * @summary Delete a task as board lead */ - -export function useListTagsApiV1AgentBoardsBoardIdTagsGet< - TData = Awaited>, +export const useDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDelete = < TError = HTTPValidationError, + TContext = unknown, >( - boardId: string, options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; taskId: string }, + TContext >; request?: SecondParameter; }, queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getListTagsApiV1AgentBoardsBoardIdTagsGetQueryOptions( - boardId, - options, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { boardId: string; taskId: string }, + TContext +> => { + return useMutation( + getDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteMutationOptions( + options, + ), + queryClient, ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - +}; /** * Update a task after board-level authorization checks. @@ -1563,146 +3537,6 @@ export const useUpdateTaskApiV1AgentBoardsBoardIdTasksTaskIdPatch = < queryClient, ); }; -/** - * Delete a board task and related records. - -This action is restricted to board lead agents. - * @summary Delete a task as board lead - */ -export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse200 = { - data: OkResponse; - status: 200; -}; - -export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponseSuccess = - deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponseError = - deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponse = - | deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponseSuccess - | deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteResponseError; - -export const getDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteUrl = ( - boardId: string, - taskId: string, -) => { - return `/api/v1/agent/boards/${boardId}/tasks/${taskId}`; -}; - -export const deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDelete = async ( - boardId: string, - taskId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteUrl(boardId, taskId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; taskId: string }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; taskId: string }, - TContext - > => { - const mutationKey = ["deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType - >, - { boardId: string; taskId: string } - > = (props) => { - const { boardId, taskId } = props ?? {}; - - return deleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDelete( - boardId, - taskId, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type DeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteMutationResult = - NonNullable< - Awaited< - ReturnType - > - >; - -export type DeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Delete a task as board lead - */ -export const useDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; taskId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType - >, - TError, - { boardId: string; taskId: string }, - TContext -> => { - return useMutation( - getDeleteTaskApiV1AgentBoardsBoardIdTasksTaskIdDeleteMutationOptions( - options, - ), - queryClient, - ); -}; /** * List task comments visible to the authenticated agent. @@ -2201,1962 +4035,6 @@ export const useCreateTaskCommentApiV1AgentBoardsBoardIdTasksTaskIdCommentsPost queryClient, ); }; -/** - * List board memory with optional chat filtering. - -Use `is_chat=false` for durable context and `is_chat=true` for board chat. - * @summary List Board Memory - */ -export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse200 = { - data: LimitOffsetPageTypeVarCustomizedBoardMemoryRead; - status: 200; -}; - -export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponseSuccess = - listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse200 & { - headers: Headers; - }; -export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponseError = - listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse422 & { - headers: Headers; - }; - -export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse = - | listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponseSuccess - | listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponseError; - -export const getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetUrl = ( - boardId: string, - params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/v1/agent/boards/${boardId}/memory?${stringifiedParams}` - : `/api/v1/agent/boards/${boardId}/memory`; -}; - -export const listBoardMemoryApiV1AgentBoardsBoardIdMemoryGet = async ( - boardId: string, - params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, - options?: RequestInit, -): Promise => { - return customFetch( - getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetUrl(boardId, params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryKey = ( - boardId: string, - params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, -) => { - return [ - `/api/v1/agent/boards/${boardId}/memory`, - ...(params ? [params] : []), - ] as const; -}; - -export const getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryOptions = < - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryKey(boardId, params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - listBoardMemoryApiV1AgentBoardsBoardIdMemoryGet(boardId, params, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!boardId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryResult = - NonNullable< - Awaited> - >; -export type ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryError = - HTTPValidationError; - -export function useListBoardMemoryApiV1AgentBoardsBoardIdMemoryGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params: undefined | ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, - options: { - query: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useListBoardMemoryApiV1AgentBoardsBoardIdMemoryGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useListBoardMemoryApiV1AgentBoardsBoardIdMemoryGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List Board Memory - */ - -export function useListBoardMemoryApiV1AgentBoardsBoardIdMemoryGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetQueryOptions( - boardId, - params, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - -/** - * Create a board memory entry. - -Use tags to indicate purpose (e.g. `chat`, `decision`, `plan`, `handoff`). - * @summary Create Board Memory - */ -export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse200 = { - data: BoardMemoryRead; - status: 200; -}; - -export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponseSuccess = - createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse200 & { - headers: Headers; - }; -export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponseError = - createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse422 & { - headers: Headers; - }; - -export type createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponse = - | createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponseSuccess - | createBoardMemoryApiV1AgentBoardsBoardIdMemoryPostResponseError; - -export const getCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostUrl = ( - boardId: string, -) => { - return `/api/v1/agent/boards/${boardId}/memory`; -}; - -export const createBoardMemoryApiV1AgentBoardsBoardIdMemoryPost = async ( - boardId: string, - boardMemoryCreate: BoardMemoryCreate, - options?: RequestInit, -): Promise => { - return customFetch( - getCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostUrl(boardId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(boardMemoryCreate), - }, - ); -}; - -export const getCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardMemoryCreate }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardMemoryCreate }, - TContext - > => { - const mutationKey = ["createBoardMemoryApiV1AgentBoardsBoardIdMemoryPost"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType - >, - { boardId: string; data: BoardMemoryCreate } - > = (props) => { - const { boardId, data } = props ?? {}; - - return createBoardMemoryApiV1AgentBoardsBoardIdMemoryPost( - boardId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type CreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationResult = - NonNullable< - Awaited< - ReturnType - > - >; -export type CreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationBody = - BoardMemoryCreate; -export type CreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationError = - HTTPValidationError; - -/** - * @summary Create Board Memory - */ -export const useCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPost = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardMemoryCreate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardMemoryCreate }, - TContext -> => { - return useMutation( - getCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPostMutationOptions( - options, - ), - queryClient, - ); -}; -/** - * List approvals for a board. - -Use status filtering to process pending approvals efficiently. - * @summary List Approvals - */ -export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse200 = { - data: LimitOffsetPageTypeVarCustomizedApprovalRead; - status: 200; -}; - -export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponseSuccess = - listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse200 & { - headers: Headers; - }; -export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponseError = - listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse422 & { - headers: Headers; - }; - -export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse = - | listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponseSuccess - | listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponseError; - -export const getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetUrl = ( - boardId: string, - params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/v1/agent/boards/${boardId}/approvals?${stringifiedParams}` - : `/api/v1/agent/boards/${boardId}/approvals`; -}; - -export const listApprovalsApiV1AgentBoardsBoardIdApprovalsGet = async ( - boardId: string, - params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, - options?: RequestInit, -): Promise => { - return customFetch( - getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetUrl(boardId, params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryKey = ( - boardId: string, - params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, -) => { - return [ - `/api/v1/agent/boards/${boardId}/approvals`, - ...(params ? [params] : []), - ] as const; -}; - -export const getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryOptions = < - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryKey( - boardId, - params, - ); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - listApprovalsApiV1AgentBoardsBoardIdApprovalsGet(boardId, params, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!boardId, - ...queryOptions, - } as UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryResult = - NonNullable< - Awaited> - >; -export type ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryError = - HTTPValidationError; - -export function useListApprovalsApiV1AgentBoardsBoardIdApprovalsGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params: undefined | ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, - options: { - query: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useListApprovalsApiV1AgentBoardsBoardIdApprovalsGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useListApprovalsApiV1AgentBoardsBoardIdApprovalsGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List Approvals - */ - -export function useListApprovalsApiV1AgentBoardsBoardIdApprovalsGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getListApprovalsApiV1AgentBoardsBoardIdApprovalsGetQueryOptions( - boardId, - params, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - -/** - * Create an approval request for risky or low-confidence actions. - -Include `task_id` or `task_ids` to scope the decision precisely. - * @summary Create Approval - */ -export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse200 = { - data: ApprovalRead; - status: 200; -}; - -export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponseSuccess = - createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse200 & { - headers: Headers; - }; -export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponseError = - createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse422 & { - headers: Headers; - }; - -export type createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponse = - | createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponseSuccess - | createApprovalApiV1AgentBoardsBoardIdApprovalsPostResponseError; - -export const getCreateApprovalApiV1AgentBoardsBoardIdApprovalsPostUrl = ( - boardId: string, -) => { - return `/api/v1/agent/boards/${boardId}/approvals`; -}; - -export const createApprovalApiV1AgentBoardsBoardIdApprovalsPost = async ( - boardId: string, - approvalCreate: ApprovalCreate, - options?: RequestInit, -): Promise => { - return customFetch( - getCreateApprovalApiV1AgentBoardsBoardIdApprovalsPostUrl(boardId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(approvalCreate), - }, - ); -}; - -export const getCreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: ApprovalCreate }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: ApprovalCreate }, - TContext - > => { - const mutationKey = ["createApprovalApiV1AgentBoardsBoardIdApprovalsPost"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType - >, - { boardId: string; data: ApprovalCreate } - > = (props) => { - const { boardId, data } = props ?? {}; - - return createApprovalApiV1AgentBoardsBoardIdApprovalsPost( - boardId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type CreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationResult = - NonNullable< - Awaited< - ReturnType - > - >; -export type CreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationBody = - ApprovalCreate; -export type CreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationError = - HTTPValidationError; - -/** - * @summary Create Approval - */ -export const useCreateApprovalApiV1AgentBoardsBoardIdApprovalsPost = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: ApprovalCreate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: ApprovalCreate }, - TContext -> => { - return useMutation( - getCreateApprovalApiV1AgentBoardsBoardIdApprovalsPostMutationOptions( - options, - ), - queryClient, - ); -}; -/** - * Apply board onboarding updates from an agent workflow. - -Used during structured objective/success-metric intake loops. - * @summary Update Onboarding - */ -export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse200 = { - data: BoardOnboardingRead; - status: 200; -}; - -export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponseSuccess = - updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse200 & { - headers: Headers; - }; -export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponseError = - updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse422 & { - headers: Headers; - }; - -export type updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponse = - | updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponseSuccess - | updateOnboardingApiV1AgentBoardsBoardIdOnboardingPostResponseError; - -export const getUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostUrl = ( - boardId: string, -) => { - return `/api/v1/agent/boards/${boardId}/onboarding`; -}; - -export const updateOnboardingApiV1AgentBoardsBoardIdOnboardingPost = async ( - boardId: string, - boardOnboardingAgentCompleteBoardOnboardingAgentQuestion: - | BoardOnboardingAgentComplete - | BoardOnboardingAgentQuestion, - options?: RequestInit, -): Promise => { - return customFetch( - getUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostUrl(boardId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify( - boardOnboardingAgentCompleteBoardOnboardingAgentQuestion, - ), - }, - ); -}; - -export const getUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { - boardId: string; - data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; - }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { - boardId: string; - data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; - }, - TContext - > => { - const mutationKey = [ - "updateOnboardingApiV1AgentBoardsBoardIdOnboardingPost", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType - >, - { - boardId: string; - data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; - } - > = (props) => { - const { boardId, data } = props ?? {}; - - return updateOnboardingApiV1AgentBoardsBoardIdOnboardingPost( - boardId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type UpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationResult = - NonNullable< - Awaited< - ReturnType - > - >; -export type UpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationBody = - | BoardOnboardingAgentComplete - | BoardOnboardingAgentQuestion; -export type UpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationError = - HTTPValidationError; - -/** - * @summary Update Onboarding - */ -export const useUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPost = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { - boardId: string; - data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; - }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType - >, - TError, - { - boardId: string; - data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion; - }, - TContext -> => { - return useMutation( - getUpdateOnboardingApiV1AgentBoardsBoardIdOnboardingPostMutationOptions( - options, - ), - queryClient, - ); -}; -/** - * Send a direct coordination message to a specific board agent. - -Use this when a lead sees stalled, idle, or misaligned work. - * @summary Nudge an agent on a board - */ -export type agentLeadNudgeAgentResponse200 = { - data: OkResponse; - status: 200; -}; - -export type agentLeadNudgeAgentResponse403 = { - data: LLMErrorResponse; - status: 403; -}; - -export type agentLeadNudgeAgentResponse404 = { - data: LLMErrorResponse; - status: 404; -}; - -export type agentLeadNudgeAgentResponse422 = { - data: LLMErrorResponse; - status: 422; -}; - -export type agentLeadNudgeAgentResponse502 = { - data: LLMErrorResponse; - status: 502; -}; - -export type agentLeadNudgeAgentResponseSuccess = - agentLeadNudgeAgentResponse200 & { - headers: Headers; - }; -export type agentLeadNudgeAgentResponseError = ( - | agentLeadNudgeAgentResponse403 - | agentLeadNudgeAgentResponse404 - | agentLeadNudgeAgentResponse422 - | agentLeadNudgeAgentResponse502 -) & { - headers: Headers; -}; - -export type agentLeadNudgeAgentResponse = - | agentLeadNudgeAgentResponseSuccess - | agentLeadNudgeAgentResponseError; - -export const getAgentLeadNudgeAgentUrl = (boardId: string, agentId: string) => { - return `/api/v1/agent/boards/${boardId}/agents/${agentId}/nudge`; -}; - -export const agentLeadNudgeAgent = async ( - boardId: string, - agentId: string, - agentNudge: AgentNudge, - options?: RequestInit, -): Promise => { - return customFetch( - getAgentLeadNudgeAgentUrl(boardId, agentId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(agentNudge), - }, - ); -}; - -export const getAgentLeadNudgeAgentMutationOptions = < - TError = LLMErrorResponse, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string; data: AgentNudge }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string; data: AgentNudge }, - TContext -> => { - const mutationKey = ["agentLeadNudgeAgent"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { boardId: string; agentId: string; data: AgentNudge } - > = (props) => { - const { boardId, agentId, data } = props ?? {}; - - return agentLeadNudgeAgent(boardId, agentId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type AgentLeadNudgeAgentMutationResult = NonNullable< - Awaited> ->; -export type AgentLeadNudgeAgentMutationBody = AgentNudge; -export type AgentLeadNudgeAgentMutationError = LLMErrorResponse; - -/** - * @summary Nudge an agent on a board - */ -export const useAgentLeadNudgeAgent = < - TError = LLMErrorResponse, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string; data: AgentNudge }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { boardId: string; agentId: string; data: AgentNudge }, - TContext -> => { - return useMutation( - getAgentLeadNudgeAgentMutationOptions(options), - queryClient, - ); -}; -/** - * Record liveness for the authenticated agent's current status. - -Use this when the agent heartbeat loop reports status changes. - * @summary Upsert agent heartbeat - */ -export type agentHeartbeatApiV1AgentHeartbeatPostResponse200 = { - data: AgentRead; - status: 200; -}; - -export type agentHeartbeatApiV1AgentHeartbeatPostResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type agentHeartbeatApiV1AgentHeartbeatPostResponseSuccess = - agentHeartbeatApiV1AgentHeartbeatPostResponse200 & { - headers: Headers; - }; -export type agentHeartbeatApiV1AgentHeartbeatPostResponseError = - agentHeartbeatApiV1AgentHeartbeatPostResponse422 & { - headers: Headers; - }; - -export type agentHeartbeatApiV1AgentHeartbeatPostResponse = - | agentHeartbeatApiV1AgentHeartbeatPostResponseSuccess - | agentHeartbeatApiV1AgentHeartbeatPostResponseError; - -export const getAgentHeartbeatApiV1AgentHeartbeatPostUrl = () => { - return `/api/v1/agent/heartbeat`; -}; - -export const agentHeartbeatApiV1AgentHeartbeatPost = async ( - agentHeartbeatCreate: AgentHeartbeatCreate, - options?: RequestInit, -): Promise => { - return customFetch( - getAgentHeartbeatApiV1AgentHeartbeatPostUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(agentHeartbeatCreate), - }, - ); -}; - -export const getAgentHeartbeatApiV1AgentHeartbeatPostMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: AgentHeartbeatCreate }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: AgentHeartbeatCreate }, - TContext -> => { - const mutationKey = ["agentHeartbeatApiV1AgentHeartbeatPost"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: AgentHeartbeatCreate } - > = (props) => { - const { data } = props ?? {}; - - return agentHeartbeatApiV1AgentHeartbeatPost(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type AgentHeartbeatApiV1AgentHeartbeatPostMutationResult = NonNullable< - Awaited> ->; -export type AgentHeartbeatApiV1AgentHeartbeatPostMutationBody = - AgentHeartbeatCreate; -export type AgentHeartbeatApiV1AgentHeartbeatPostMutationError = - HTTPValidationError; - -/** - * @summary Upsert agent heartbeat - */ -export const useAgentHeartbeatApiV1AgentHeartbeatPost = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: AgentHeartbeatCreate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: AgentHeartbeatCreate }, - TContext -> => { - return useMutation( - getAgentHeartbeatApiV1AgentHeartbeatPostMutationOptions(options), - queryClient, - ); -}; -/** - * Fetch an agent's SOUL.md content. - -Allowed for board lead, or for an agent reading its own SOUL. - * @summary Get Agent Soul - */ -export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse200 = - { - data: string; - status: 200; - }; - -export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse422 = - { - data: HTTPValidationError; - status: 422; - }; - -export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponseSuccess = - getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse200 & { - headers: Headers; - }; -export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponseError = - getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse422 & { - headers: Headers; - }; - -export type getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponse = - | getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponseSuccess - | getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetResponseError; - -export const getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetUrl = ( - boardId: string, - agentId: string, -) => { - return `/api/v1/agent/boards/${boardId}/agents/${agentId}/soul`; -}; - -export const getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet = async ( - boardId: string, - agentId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetUrl( - boardId, - agentId, - ), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryKey = - (boardId: string, agentId: string) => { - return [`/api/v1/agent/boards/${boardId}/agents/${agentId}/soul`] as const; - }; - -export const getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryOptions = - < - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, - >( - boardId: string, - agentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - ) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryKey( - boardId, - agentId, - ); - - const queryFn: QueryFunction< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - > - > = ({ signal }) => - getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet( - boardId, - agentId, - { signal, ...requestOptions }, - ); - - return { - queryKey, - queryFn, - enabled: !!(boardId && agentId), - ...queryOptions, - } as UseQueryOptions< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - >, - TError, - TData - > & { queryKey: DataTag }; - }; - -export type GetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryResult = - NonNullable< - Awaited< - ReturnType - > - >; -export type GetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryError = - HTTPValidationError; - -export function useGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - agentId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - >, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - >, - TError, - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - agentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - >, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - >, - TError, - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - agentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Agent Soul - */ - -export function useGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - agentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType< - typeof getAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGet - > - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getGetAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulGetQueryOptions( - boardId, - agentId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - -/** - * Write SOUL.md content for a board agent and persist it for reprovisioning. - -Use this when role instructions or behavior guardrails need updates. - * @summary Update an agent's SOUL template - */ -export type agentLeadUpdateAgentSoulResponse200 = { - data: OkResponse; - status: 200; -}; - -export type agentLeadUpdateAgentSoulResponse403 = { - data: LLMErrorResponse; - status: 403; -}; - -export type agentLeadUpdateAgentSoulResponse404 = { - data: LLMErrorResponse; - status: 404; -}; - -export type agentLeadUpdateAgentSoulResponse422 = { - data: LLMErrorResponse; - status: 422; -}; - -export type agentLeadUpdateAgentSoulResponse502 = { - data: LLMErrorResponse; - status: 502; -}; - -export type agentLeadUpdateAgentSoulResponseSuccess = - agentLeadUpdateAgentSoulResponse200 & { - headers: Headers; - }; -export type agentLeadUpdateAgentSoulResponseError = ( - | agentLeadUpdateAgentSoulResponse403 - | agentLeadUpdateAgentSoulResponse404 - | agentLeadUpdateAgentSoulResponse422 - | agentLeadUpdateAgentSoulResponse502 -) & { - headers: Headers; -}; - -export type agentLeadUpdateAgentSoulResponse = - | agentLeadUpdateAgentSoulResponseSuccess - | agentLeadUpdateAgentSoulResponseError; - -export const getAgentLeadUpdateAgentSoulUrl = ( - boardId: string, - agentId: string, -) => { - return `/api/v1/agent/boards/${boardId}/agents/${agentId}/soul`; -}; - -export const agentLeadUpdateAgentSoul = async ( - boardId: string, - agentId: string, - soulUpdateRequest: SoulUpdateRequest, - options?: RequestInit, -): Promise => { - return customFetch( - getAgentLeadUpdateAgentSoulUrl(boardId, agentId), - { - ...options, - method: "PUT", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(soulUpdateRequest), - }, - ); -}; - -export const getAgentLeadUpdateAgentSoulMutationOptions = < - TError = LLMErrorResponse, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string; data: SoulUpdateRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string; data: SoulUpdateRequest }, - TContext -> => { - const mutationKey = ["agentLeadUpdateAgentSoul"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { boardId: string; agentId: string; data: SoulUpdateRequest } - > = (props) => { - const { boardId, agentId, data } = props ?? {}; - - return agentLeadUpdateAgentSoul(boardId, agentId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type AgentLeadUpdateAgentSoulMutationResult = NonNullable< - Awaited> ->; -export type AgentLeadUpdateAgentSoulMutationBody = SoulUpdateRequest; -export type AgentLeadUpdateAgentSoulMutationError = LLMErrorResponse; - -/** - * @summary Update an agent's SOUL template - */ -export const useAgentLeadUpdateAgentSoul = < - TError = LLMErrorResponse, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string; data: SoulUpdateRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { boardId: string; agentId: string; data: SoulUpdateRequest }, - TContext -> => { - return useMutation( - getAgentLeadUpdateAgentSoulMutationOptions(options), - queryClient, - ); -}; -/** - * Permanently remove a board agent and tear down associated lifecycle state. - -Use sparingly; prefer reassignment for continuity-sensitive teams. - * @summary Delete a board agent as lead - */ -export type agentLeadDeleteBoardAgentResponse200 = { - data: OkResponse; - status: 200; -}; - -export type agentLeadDeleteBoardAgentResponse403 = { - data: LLMErrorResponse; - status: 403; -}; - -export type agentLeadDeleteBoardAgentResponse404 = { - data: LLMErrorResponse; - status: 404; -}; - -export type agentLeadDeleteBoardAgentResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type agentLeadDeleteBoardAgentResponseSuccess = - agentLeadDeleteBoardAgentResponse200 & { - headers: Headers; - }; -export type agentLeadDeleteBoardAgentResponseError = ( - | agentLeadDeleteBoardAgentResponse403 - | agentLeadDeleteBoardAgentResponse404 - | agentLeadDeleteBoardAgentResponse422 -) & { - headers: Headers; -}; - -export type agentLeadDeleteBoardAgentResponse = - | agentLeadDeleteBoardAgentResponseSuccess - | agentLeadDeleteBoardAgentResponseError; - -export const getAgentLeadDeleteBoardAgentUrl = ( - boardId: string, - agentId: string, -) => { - return `/api/v1/agent/boards/${boardId}/agents/${agentId}`; -}; - -export const agentLeadDeleteBoardAgent = async ( - boardId: string, - agentId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getAgentLeadDeleteBoardAgentUrl(boardId, agentId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getAgentLeadDeleteBoardAgentMutationOptions = < - TError = LLMErrorResponse | HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string }, - TContext -> => { - const mutationKey = ["agentLeadDeleteBoardAgent"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { boardId: string; agentId: string } - > = (props) => { - const { boardId, agentId } = props ?? {}; - - return agentLeadDeleteBoardAgent(boardId, agentId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type AgentLeadDeleteBoardAgentMutationResult = NonNullable< - Awaited> ->; - -export type AgentLeadDeleteBoardAgentMutationError = - | LLMErrorResponse - | HTTPValidationError; - -/** - * @summary Delete a board agent as lead - */ -export const useAgentLeadDeleteBoardAgent = < - TError = LLMErrorResponse | HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; agentId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { boardId: string; agentId: string }, - TContext -> => { - return useMutation( - getAgentLeadDeleteBoardAgentMutationOptions(options), - queryClient, - ); -}; -/** - * Escalate a high-impact decision or ambiguity through the gateway-main interaction channel. - -Use when lead-level context needs human confirmation or consent. - * @summary Ask the human via gateway-main - */ -export type agentLeadAskUserViaGatewayMainResponse200 = { - data: GatewayMainAskUserResponse; - status: 200; -}; - -export type agentLeadAskUserViaGatewayMainResponse403 = { - data: LLMErrorResponse; - status: 403; -}; - -export type agentLeadAskUserViaGatewayMainResponse404 = { - data: LLMErrorResponse; - status: 404; -}; - -export type agentLeadAskUserViaGatewayMainResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type agentLeadAskUserViaGatewayMainResponse502 = { - data: LLMErrorResponse; - status: 502; -}; - -export type agentLeadAskUserViaGatewayMainResponseSuccess = - agentLeadAskUserViaGatewayMainResponse200 & { - headers: Headers; - }; -export type agentLeadAskUserViaGatewayMainResponseError = ( - | agentLeadAskUserViaGatewayMainResponse403 - | agentLeadAskUserViaGatewayMainResponse404 - | agentLeadAskUserViaGatewayMainResponse422 - | agentLeadAskUserViaGatewayMainResponse502 -) & { - headers: Headers; -}; - -export type agentLeadAskUserViaGatewayMainResponse = - | agentLeadAskUserViaGatewayMainResponseSuccess - | agentLeadAskUserViaGatewayMainResponseError; - -export const getAgentLeadAskUserViaGatewayMainUrl = (boardId: string) => { - return `/api/v1/agent/boards/${boardId}/gateway/main/ask-user`; -}; - -export const agentLeadAskUserViaGatewayMain = async ( - boardId: string, - gatewayMainAskUserRequest: GatewayMainAskUserRequest, - options?: RequestInit, -): Promise => { - return customFetch( - getAgentLeadAskUserViaGatewayMainUrl(boardId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(gatewayMainAskUserRequest), - }, - ); -}; - -export const getAgentLeadAskUserViaGatewayMainMutationOptions = < - TError = LLMErrorResponse | HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; data: GatewayMainAskUserRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { boardId: string; data: GatewayMainAskUserRequest }, - TContext -> => { - const mutationKey = ["agentLeadAskUserViaGatewayMain"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { boardId: string; data: GatewayMainAskUserRequest } - > = (props) => { - const { boardId, data } = props ?? {}; - - return agentLeadAskUserViaGatewayMain(boardId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type AgentLeadAskUserViaGatewayMainMutationResult = NonNullable< - Awaited> ->; -export type AgentLeadAskUserViaGatewayMainMutationBody = - GatewayMainAskUserRequest; -export type AgentLeadAskUserViaGatewayMainMutationError = - | LLMErrorResponse - | HTTPValidationError; - -/** - * @summary Ask the human via gateway-main - */ -export const useAgentLeadAskUserViaGatewayMain = < - TError = LLMErrorResponse | HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; data: GatewayMainAskUserRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { boardId: string; data: GatewayMainAskUserRequest }, - TContext -> => { - return useMutation( - getAgentLeadAskUserViaGatewayMainMutationOptions(options), - queryClient, - ); -}; /** * Route a direct lead handoff or question from an agent to the board lead. @@ -4437,3 +4315,305 @@ export const useAgentMainBroadcastLeadMessage = < queryClient, ); }; +/** + * Token-authenticated liveness probe for agent API clients. + +Use this endpoint when the caller needs to verify both service availability and agent-token validity in one request. + * @summary Agent Auth Health Check + */ +export type agentHealthzApiV1AgentHealthzGetResponse200 = { + data: AgentHealthStatusResponse; + status: 200; +}; + +export type agentHealthzApiV1AgentHealthzGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type agentHealthzApiV1AgentHealthzGetResponseSuccess = + agentHealthzApiV1AgentHealthzGetResponse200 & { + headers: Headers; + }; +export type agentHealthzApiV1AgentHealthzGetResponseError = + agentHealthzApiV1AgentHealthzGetResponse422 & { + headers: Headers; + }; + +export type agentHealthzApiV1AgentHealthzGetResponse = + | agentHealthzApiV1AgentHealthzGetResponseSuccess + | agentHealthzApiV1AgentHealthzGetResponseError; + +export const getAgentHealthzApiV1AgentHealthzGetUrl = () => { + return `/api/v1/agent/healthz`; +}; + +export const agentHealthzApiV1AgentHealthzGet = async ( + options?: RequestInit, +): Promise => { + return customFetch( + getAgentHealthzApiV1AgentHealthzGetUrl(), + { + ...options, + method: "GET", + }, + ); +}; + +export const getAgentHealthzApiV1AgentHealthzGetQueryKey = () => { + return [`/api/v1/agent/healthz`] as const; +}; + +export const getAgentHealthzApiV1AgentHealthzGetQueryOptions = < + TData = Awaited>, + TError = HTTPValidationError, +>(options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getAgentHealthzApiV1AgentHealthzGetQueryKey(); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + agentHealthzApiV1AgentHealthzGet({ signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type AgentHealthzApiV1AgentHealthzGetQueryResult = NonNullable< + Awaited> +>; +export type AgentHealthzApiV1AgentHealthzGetQueryError = HTTPValidationError; + +export function useAgentHealthzApiV1AgentHealthzGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + options: { + query: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useAgentHealthzApiV1AgentHealthzGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useAgentHealthzApiV1AgentHealthzGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Agent Auth Health Check + */ + +export function useAgentHealthzApiV1AgentHealthzGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = getAgentHealthzApiV1AgentHealthzGetQueryOptions(options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Record liveness for the authenticated agent. + +Use this when the agent heartbeat loop checks in. + * @summary Upsert agent heartbeat + */ +export type agentHeartbeatApiV1AgentHeartbeatPostResponse200 = { + data: AgentRead; + status: 200; +}; + +export type agentHeartbeatApiV1AgentHeartbeatPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type agentHeartbeatApiV1AgentHeartbeatPostResponseSuccess = + agentHeartbeatApiV1AgentHeartbeatPostResponse200 & { + headers: Headers; + }; +export type agentHeartbeatApiV1AgentHeartbeatPostResponseError = + agentHeartbeatApiV1AgentHeartbeatPostResponse422 & { + headers: Headers; + }; + +export type agentHeartbeatApiV1AgentHeartbeatPostResponse = + | agentHeartbeatApiV1AgentHeartbeatPostResponseSuccess + | agentHeartbeatApiV1AgentHeartbeatPostResponseError; + +export const getAgentHeartbeatApiV1AgentHeartbeatPostUrl = () => { + return `/api/v1/agent/heartbeat`; +}; + +export const agentHeartbeatApiV1AgentHeartbeatPost = async ( + options?: RequestInit, +): Promise => { + return customFetch( + getAgentHeartbeatApiV1AgentHeartbeatPostUrl(), + { + ...options, + method: "POST", + }, + ); +}; + +export const getAgentHeartbeatApiV1AgentHeartbeatPostMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + void, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + void, + TContext +> => { + const mutationKey = ["agentHeartbeatApiV1AgentHeartbeatPost"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + void + > = () => { + return agentHeartbeatApiV1AgentHeartbeatPost(requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AgentHeartbeatApiV1AgentHeartbeatPostMutationResult = NonNullable< + Awaited> +>; + +export type AgentHeartbeatApiV1AgentHeartbeatPostMutationError = + HTTPValidationError; + +/** + * @summary Upsert agent heartbeat + */ +export const useAgentHeartbeatApiV1AgentHeartbeatPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + void, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + void, + TContext +> => { + return useMutation( + getAgentHeartbeatApiV1AgentHeartbeatPostMutationOptions(options), + queryClient, + ); +}; diff --git a/frontend/src/api/generated/agents/agents.ts b/frontend/src/api/generated/agents/agents.ts index ede3ec2f..81822c55 100644 --- a/frontend/src/api/generated/agents/agents.ts +++ b/frontend/src/api/generated/agents/agents.ts @@ -364,6 +364,134 @@ export const useCreateAgentApiV1AgentsPost = < queryClient, ); }; +/** + * Heartbeat an existing agent or create/provision one if needed. + * @summary Heartbeat Or Create Agent + */ +export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse200 = { + data: AgentRead; + status: 200; +}; + +export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponseSuccess = + heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse200 & { + headers: Headers; + }; +export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponseError = + heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse422 & { + headers: Headers; + }; + +export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse = + | heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponseSuccess + | heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponseError; + +export const getHeartbeatOrCreateAgentApiV1AgentsHeartbeatPostUrl = () => { + return `/api/v1/agents/heartbeat`; +}; + +export const heartbeatOrCreateAgentApiV1AgentsHeartbeatPost = async ( + agentHeartbeatCreate: AgentHeartbeatCreate, + options?: RequestInit, +): Promise => { + return customFetch( + getHeartbeatOrCreateAgentApiV1AgentsHeartbeatPostUrl(), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(agentHeartbeatCreate), + }, + ); +}; + +export const getHeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { data: AgentHeartbeatCreate }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited>, + TError, + { data: AgentHeartbeatCreate }, + TContext + > => { + const mutationKey = ["heartbeatOrCreateAgentApiV1AgentsHeartbeatPost"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType + >, + { data: AgentHeartbeatCreate } + > = (props) => { + const { data } = props ?? {}; + + return heartbeatOrCreateAgentApiV1AgentsHeartbeatPost( + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type HeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationResult = + NonNullable< + Awaited> + >; +export type HeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationBody = + AgentHeartbeatCreate; +export type HeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationError = + HTTPValidationError; + +/** + * @summary Heartbeat Or Create Agent + */ +export const useHeartbeatOrCreateAgentApiV1AgentsHeartbeatPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { data: AgentHeartbeatCreate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { data: AgentHeartbeatCreate }, + TContext +> => { + return useMutation( + getHeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationOptions(options), + queryClient, + ); +}; /** * Stream agent updates as SSE events. * @summary Stream Agents @@ -576,6 +704,123 @@ export function useStreamAgentsApiV1AgentsStreamGet< return { ...query, queryKey: queryOptions.queryKey }; } +/** + * Delete an agent and clean related task state. + * @summary Delete Agent + */ +export type deleteAgentApiV1AgentsAgentIdDeleteResponse200 = { + data: OkResponse; + status: 200; +}; + +export type deleteAgentApiV1AgentsAgentIdDeleteResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type deleteAgentApiV1AgentsAgentIdDeleteResponseSuccess = + deleteAgentApiV1AgentsAgentIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteAgentApiV1AgentsAgentIdDeleteResponseError = + deleteAgentApiV1AgentsAgentIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteAgentApiV1AgentsAgentIdDeleteResponse = + | deleteAgentApiV1AgentsAgentIdDeleteResponseSuccess + | deleteAgentApiV1AgentsAgentIdDeleteResponseError; + +export const getDeleteAgentApiV1AgentsAgentIdDeleteUrl = (agentId: string) => { + return `/api/v1/agents/${agentId}`; +}; + +export const deleteAgentApiV1AgentsAgentIdDelete = async ( + agentId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getDeleteAgentApiV1AgentsAgentIdDeleteUrl(agentId), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getDeleteAgentApiV1AgentsAgentIdDeleteMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { agentId: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { agentId: string }, + TContext +> => { + const mutationKey = ["deleteAgentApiV1AgentsAgentIdDelete"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { agentId: string } + > = (props) => { + const { agentId } = props ?? {}; + + return deleteAgentApiV1AgentsAgentIdDelete(agentId, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type DeleteAgentApiV1AgentsAgentIdDeleteMutationResult = NonNullable< + Awaited> +>; + +export type DeleteAgentApiV1AgentsAgentIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Delete Agent + */ +export const useDeleteAgentApiV1AgentsAgentIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { agentId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { agentId: string }, + TContext +> => { + return useMutation( + getDeleteAgentApiV1AgentsAgentIdDeleteMutationOptions(options), + queryClient, + ); +}; /** * Get a single agent by id. * @summary Get Agent @@ -937,123 +1182,6 @@ export const useUpdateAgentApiV1AgentsAgentIdPatch = < queryClient, ); }; -/** - * Delete an agent and clean related task state. - * @summary Delete Agent - */ -export type deleteAgentApiV1AgentsAgentIdDeleteResponse200 = { - data: OkResponse; - status: 200; -}; - -export type deleteAgentApiV1AgentsAgentIdDeleteResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteAgentApiV1AgentsAgentIdDeleteResponseSuccess = - deleteAgentApiV1AgentsAgentIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteAgentApiV1AgentsAgentIdDeleteResponseError = - deleteAgentApiV1AgentsAgentIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteAgentApiV1AgentsAgentIdDeleteResponse = - | deleteAgentApiV1AgentsAgentIdDeleteResponseSuccess - | deleteAgentApiV1AgentsAgentIdDeleteResponseError; - -export const getDeleteAgentApiV1AgentsAgentIdDeleteUrl = (agentId: string) => { - return `/api/v1/agents/${agentId}`; -}; - -export const deleteAgentApiV1AgentsAgentIdDelete = async ( - agentId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteAgentApiV1AgentsAgentIdDeleteUrl(agentId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteAgentApiV1AgentsAgentIdDeleteMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { agentId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { agentId: string }, - TContext -> => { - const mutationKey = ["deleteAgentApiV1AgentsAgentIdDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { agentId: string } - > = (props) => { - const { agentId } = props ?? {}; - - return deleteAgentApiV1AgentsAgentIdDelete(agentId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteAgentApiV1AgentsAgentIdDeleteMutationResult = NonNullable< - Awaited> ->; - -export type DeleteAgentApiV1AgentsAgentIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Delete Agent - */ -export const useDeleteAgentApiV1AgentsAgentIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { agentId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { agentId: string }, - TContext -> => { - return useMutation( - getDeleteAgentApiV1AgentsAgentIdDeleteMutationOptions(options), - queryClient, - ); -}; /** * Record a heartbeat for a specific agent. * @summary Heartbeat Agent @@ -1182,131 +1310,3 @@ export const useHeartbeatAgentApiV1AgentsAgentIdHeartbeatPost = < queryClient, ); }; -/** - * Heartbeat an existing agent or create/provision one if needed. - * @summary Heartbeat Or Create Agent - */ -export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse200 = { - data: AgentRead; - status: 200; -}; - -export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponseSuccess = - heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse200 & { - headers: Headers; - }; -export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponseError = - heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse422 & { - headers: Headers; - }; - -export type heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponse = - | heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponseSuccess - | heartbeatOrCreateAgentApiV1AgentsHeartbeatPostResponseError; - -export const getHeartbeatOrCreateAgentApiV1AgentsHeartbeatPostUrl = () => { - return `/api/v1/agents/heartbeat`; -}; - -export const heartbeatOrCreateAgentApiV1AgentsHeartbeatPost = async ( - agentHeartbeatCreate: AgentHeartbeatCreate, - options?: RequestInit, -): Promise => { - return customFetch( - getHeartbeatOrCreateAgentApiV1AgentsHeartbeatPostUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(agentHeartbeatCreate), - }, - ); -}; - -export const getHeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { data: AgentHeartbeatCreate }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited>, - TError, - { data: AgentHeartbeatCreate }, - TContext - > => { - const mutationKey = ["heartbeatOrCreateAgentApiV1AgentsHeartbeatPost"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType - >, - { data: AgentHeartbeatCreate } - > = (props) => { - const { data } = props ?? {}; - - return heartbeatOrCreateAgentApiV1AgentsHeartbeatPost( - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type HeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationResult = - NonNullable< - Awaited> - >; -export type HeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationBody = - AgentHeartbeatCreate; -export type HeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationError = - HTTPValidationError; - -/** - * @summary Heartbeat Or Create Agent - */ -export const useHeartbeatOrCreateAgentApiV1AgentsHeartbeatPost = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { data: AgentHeartbeatCreate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: AgentHeartbeatCreate }, - TContext -> => { - return useMutation( - getHeartbeatOrCreateAgentApiV1AgentsHeartbeatPostMutationOptions(options), - queryClient, - ); -}; diff --git a/frontend/src/api/generated/board-groups/board-groups.ts b/frontend/src/api/generated/board-groups/board-groups.ts index 78fbaf2f..ceb6c86c 100644 --- a/frontend/src/api/generated/board-groups/board-groups.ts +++ b/frontend/src/api/generated/board-groups/board-groups.ts @@ -369,6 +369,129 @@ export const useCreateBoardGroupApiV1BoardGroupsPost = < queryClient, ); }; +/** + * Delete a board group. + * @summary Delete Board Group + */ +export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse200 = { + data: OkResponse; + status: 200; +}; + +export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponseSuccess = + deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponseError = + deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse = + | deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponseSuccess + | deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponseError; + +export const getDeleteBoardGroupApiV1BoardGroupsGroupIdDeleteUrl = ( + groupId: string, +) => { + return `/api/v1/board-groups/${groupId}`; +}; + +export const deleteBoardGroupApiV1BoardGroupsGroupIdDelete = async ( + groupId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getDeleteBoardGroupApiV1BoardGroupsGroupIdDeleteUrl(groupId), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getDeleteBoardGroupApiV1BoardGroupsGroupIdDeleteMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { groupId: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { groupId: string }, + TContext +> => { + const mutationKey = ["deleteBoardGroupApiV1BoardGroupsGroupIdDelete"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { groupId: string } + > = (props) => { + const { groupId } = props ?? {}; + + return deleteBoardGroupApiV1BoardGroupsGroupIdDelete( + groupId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type DeleteBoardGroupApiV1BoardGroupsGroupIdDeleteMutationResult = + NonNullable< + Awaited> + >; + +export type DeleteBoardGroupApiV1BoardGroupsGroupIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Delete Board Group + */ +export const useDeleteBoardGroupApiV1BoardGroupsGroupIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { groupId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { groupId: string }, + TContext +> => { + return useMutation( + getDeleteBoardGroupApiV1BoardGroupsGroupIdDeleteMutationOptions(options), + queryClient, + ); +}; /** * Get a board group by id. * @summary Get Board Group @@ -707,125 +830,161 @@ export const useUpdateBoardGroupApiV1BoardGroupsGroupIdPatch = < ); }; /** - * Delete a board group. - * @summary Delete Board Group + * Apply heartbeat settings to agents in a board group. + * @summary Apply Board Group Heartbeat */ -export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse200 = { - data: OkResponse; - status: 200; -}; +export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse200 = + { + data: BoardGroupHeartbeatApplyResult; + status: 200; + }; -export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse422 = { - data: HTTPValidationError; - status: 422; -}; +export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse422 = + { + data: HTTPValidationError; + status: 422; + }; -export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponseSuccess = - deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse200 & { +export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponseSuccess = + applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse200 & { headers: Headers; }; -export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponseError = - deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse422 & { +export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponseError = + applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse422 & { headers: Headers; }; -export type deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponse = - | deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponseSuccess - | deleteBoardGroupApiV1BoardGroupsGroupIdDeleteResponseError; +export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse = -export const getDeleteBoardGroupApiV1BoardGroupsGroupIdDeleteUrl = ( - groupId: string, -) => { - return `/api/v1/board-groups/${groupId}`; -}; + | applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponseSuccess + | applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponseError; -export const deleteBoardGroupApiV1BoardGroupsGroupIdDelete = async ( - groupId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteBoardGroupApiV1BoardGroupsGroupIdDeleteUrl(groupId), - { - ...options, - method: "DELETE", - }, - ); -}; +export const getApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostUrl = + (groupId: string) => { + return `/api/v1/board-groups/${groupId}/heartbeat`; + }; -export const getDeleteBoardGroupApiV1BoardGroupsGroupIdDeleteMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { groupId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { groupId: string }, - TContext -> => { - const mutationKey = ["deleteBoardGroupApiV1BoardGroupsGroupIdDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { groupId: string } - > = (props) => { - const { groupId } = props ?? {}; - - return deleteBoardGroupApiV1BoardGroupsGroupIdDelete( - groupId, - requestOptions, +export const applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost = + async ( + groupId: string, + boardGroupHeartbeatApply: BoardGroupHeartbeatApply, + options?: RequestInit, + ): Promise => { + return customFetch( + getApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostUrl( + groupId, + ), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(boardGroupHeartbeatApply), + }, ); }; - return { mutationFn, ...mutationOptions }; -}; +export const getApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost + > + >, + TError, + { groupId: string; data: BoardGroupHeartbeatApply }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType< + typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost + > + >, + TError, + { groupId: string; data: BoardGroupHeartbeatApply }, + TContext + > => { + const mutationKey = [ + "applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; -export type DeleteBoardGroupApiV1BoardGroupsGroupIdDeleteMutationResult = + const mutationFn: MutationFunction< + Awaited< + ReturnType< + typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost + > + >, + { groupId: string; data: BoardGroupHeartbeatApply } + > = (props) => { + const { groupId, data } = props ?? {}; + + return applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost( + groupId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type ApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationResult = NonNullable< - Awaited> + Awaited< + ReturnType< + typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost + > + > >; - -export type DeleteBoardGroupApiV1BoardGroupsGroupIdDeleteMutationError = +export type ApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationBody = + BoardGroupHeartbeatApply; +export type ApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationError = HTTPValidationError; /** - * @summary Delete Board Group + * @summary Apply Board Group Heartbeat */ -export const useDeleteBoardGroupApiV1BoardGroupsGroupIdDelete = < +export const useApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost = < TError = HTTPValidationError, TContext = unknown, >( options?: { mutation?: UseMutationOptions< - Awaited>, + Awaited< + ReturnType< + typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost + > + >, TError, - { groupId: string }, + { groupId: string; data: BoardGroupHeartbeatApply }, TContext >; request?: SecondParameter; }, queryClient?: QueryClient, ): UseMutationResult< - Awaited>, + Awaited< + ReturnType< + typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost + > + >, TError, - { groupId: string }, + { groupId: string; data: BoardGroupHeartbeatApply }, TContext > => { return useMutation( - getDeleteBoardGroupApiV1BoardGroupsGroupIdDeleteMutationOptions(options), + getApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationOptions( + options, + ), queryClient, ); }; @@ -1131,163 +1290,3 @@ export function useGetBoardGroupSnapshotApiV1BoardGroupsGroupIdSnapshotGet< return { ...query, queryKey: queryOptions.queryKey }; } - -/** - * Apply heartbeat settings to agents in a board group. - * @summary Apply Board Group Heartbeat - */ -export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse200 = - { - data: BoardGroupHeartbeatApplyResult; - status: 200; - }; - -export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse422 = - { - data: HTTPValidationError; - status: 422; - }; - -export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponseSuccess = - applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse200 & { - headers: Headers; - }; -export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponseError = - applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse422 & { - headers: Headers; - }; - -export type applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponse = - - | applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponseSuccess - | applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostResponseError; - -export const getApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostUrl = - (groupId: string) => { - return `/api/v1/board-groups/${groupId}/heartbeat`; - }; - -export const applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost = - async ( - groupId: string, - boardGroupHeartbeatApply: BoardGroupHeartbeatApply, - options?: RequestInit, - ): Promise => { - return customFetch( - getApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostUrl( - groupId, - ), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(boardGroupHeartbeatApply), - }, - ); - }; - -export const getApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost - > - >, - TError, - { groupId: string; data: BoardGroupHeartbeatApply }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType< - typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost - > - >, - TError, - { groupId: string; data: BoardGroupHeartbeatApply }, - TContext - > => { - const mutationKey = [ - "applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType< - typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost - > - >, - { groupId: string; data: BoardGroupHeartbeatApply } - > = (props) => { - const { groupId, data } = props ?? {}; - - return applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost( - groupId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type ApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationResult = - NonNullable< - Awaited< - ReturnType< - typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost - > - > - >; -export type ApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationBody = - BoardGroupHeartbeatApply; -export type ApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationError = - HTTPValidationError; - -/** - * @summary Apply Board Group Heartbeat - */ -export const useApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost - > - >, - TError, - { groupId: string; data: BoardGroupHeartbeatApply }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType< - typeof applyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPost - > - >, - TError, - { groupId: string; data: BoardGroupHeartbeatApply }, - TContext -> => { - return useMutation( - getApplyBoardGroupHeartbeatApiV1BoardGroupsGroupIdHeartbeatPostMutationOptions( - options, - ), - queryClient, - ); -}; diff --git a/frontend/src/api/generated/board-onboarding/board-onboarding.ts b/frontend/src/api/generated/board-onboarding/board-onboarding.ts index 8efcb6ff..479bfd9a 100644 --- a/frontend/src/api/generated/board-onboarding/board-onboarding.ts +++ b/frontend/src/api/generated/board-onboarding/board-onboarding.ts @@ -274,298 +274,6 @@ export function useGetOnboardingApiV1BoardsBoardIdOnboardingGet< return { ...query, queryKey: queryOptions.queryKey }; } -/** - * Start onboarding and send instructions to the gateway agent. - * @summary Start Onboarding - */ -export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse200 = { - data: BoardOnboardingRead; - status: 200; -}; - -export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseSuccess = - startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse200 & { - headers: Headers; - }; -export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseError = - startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse422 & { - headers: Headers; - }; - -export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse = - | startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseSuccess - | startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseError; - -export const getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostUrl = ( - boardId: string, -) => { - return `/api/v1/boards/${boardId}/onboarding/start`; -}; - -export const startOnboardingApiV1BoardsBoardIdOnboardingStartPost = async ( - boardId: string, - boardOnboardingStart: BoardOnboardingStart, - options?: RequestInit, -): Promise => { - return customFetch( - getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostUrl(boardId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(boardOnboardingStart), - }, - ); -}; - -export const getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardOnboardingStart }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardOnboardingStart }, - TContext - > => { - const mutationKey = [ - "startOnboardingApiV1BoardsBoardIdOnboardingStartPost", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType - >, - { boardId: string; data: BoardOnboardingStart } - > = (props) => { - const { boardId, data } = props ?? {}; - - return startOnboardingApiV1BoardsBoardIdOnboardingStartPost( - boardId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationResult = - NonNullable< - Awaited< - ReturnType - > - >; -export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationBody = - BoardOnboardingStart; -export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationError = - HTTPValidationError; - -/** - * @summary Start Onboarding - */ -export const useStartOnboardingApiV1BoardsBoardIdOnboardingStartPost = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardOnboardingStart }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardOnboardingStart }, - TContext -> => { - return useMutation( - getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationOptions( - options, - ), - queryClient, - ); -}; -/** - * Send a user onboarding answer to the gateway agent. - * @summary Answer Onboarding - */ -export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse200 = - { - data: BoardOnboardingRead; - status: 200; - }; - -export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse422 = - { - data: HTTPValidationError; - status: 422; - }; - -export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseSuccess = - answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse200 & { - headers: Headers; - }; -export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseError = - answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse422 & { - headers: Headers; - }; - -export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse = - | answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseSuccess - | answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseError; - -export const getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostUrl = ( - boardId: string, -) => { - return `/api/v1/boards/${boardId}/onboarding/answer`; -}; - -export const answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost = async ( - boardId: string, - boardOnboardingAnswer: BoardOnboardingAnswer, - options?: RequestInit, -): Promise => { - return customFetch( - getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostUrl(boardId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(boardOnboardingAnswer), - }, - ); -}; - -export const getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost - > - >, - TError, - { boardId: string; data: BoardOnboardingAnswer }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardOnboardingAnswer }, - TContext - > => { - const mutationKey = [ - "answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType< - typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost - > - >, - { boardId: string; data: BoardOnboardingAnswer } - > = (props) => { - const { boardId, data } = props ?? {}; - - return answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost( - boardId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationResult = - NonNullable< - Awaited< - ReturnType - > - >; -export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationBody = - BoardOnboardingAnswer; -export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationError = - HTTPValidationError; - -/** - * @summary Answer Onboarding - */ -export const useAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost - > - >, - TError, - { boardId: string; data: BoardOnboardingAnswer }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType - >, - TError, - { boardId: string; data: BoardOnboardingAnswer }, - TContext -> => { - return useMutation( - getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationOptions( - options, - ), - queryClient, - ); -}; /** * Store onboarding updates submitted by the gateway agent. * @summary Agent Onboarding Update @@ -741,6 +449,156 @@ export const useAgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost = < queryClient, ); }; +/** + * Send a user onboarding answer to the gateway agent. + * @summary Answer Onboarding + */ +export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse200 = + { + data: BoardOnboardingRead; + status: 200; + }; + +export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseSuccess = + answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse200 & { + headers: Headers; + }; +export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseError = + answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse422 & { + headers: Headers; + }; + +export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse = + | answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseSuccess + | answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseError; + +export const getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostUrl = ( + boardId: string, +) => { + return `/api/v1/boards/${boardId}/onboarding/answer`; +}; + +export const answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost = async ( + boardId: string, + boardOnboardingAnswer: BoardOnboardingAnswer, + options?: RequestInit, +): Promise => { + return customFetch( + getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostUrl(boardId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(boardOnboardingAnswer), + }, + ); +}; + +export const getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost + > + >, + TError, + { boardId: string; data: BoardOnboardingAnswer }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardOnboardingAnswer }, + TContext + > => { + const mutationKey = [ + "answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType< + typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost + > + >, + { boardId: string; data: BoardOnboardingAnswer } + > = (props) => { + const { boardId, data } = props ?? {}; + + return answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost( + boardId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationBody = + BoardOnboardingAnswer; +export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationError = + HTTPValidationError; + +/** + * @summary Answer Onboarding + */ +export const useAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost + > + >, + TError, + { boardId: string; data: BoardOnboardingAnswer }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardOnboardingAnswer }, + TContext +> => { + return useMutation( + getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationOptions( + options, + ), + queryClient, + ); +}; /** * Confirm onboarding results and provision the board lead agent. * @summary Confirm Onboarding @@ -895,3 +753,145 @@ export const useConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost = < queryClient, ); }; +/** + * Start onboarding and send instructions to the gateway agent. + * @summary Start Onboarding + */ +export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse200 = { + data: BoardOnboardingRead; + status: 200; +}; + +export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseSuccess = + startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse200 & { + headers: Headers; + }; +export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseError = + startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse422 & { + headers: Headers; + }; + +export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse = + | startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseSuccess + | startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseError; + +export const getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostUrl = ( + boardId: string, +) => { + return `/api/v1/boards/${boardId}/onboarding/start`; +}; + +export const startOnboardingApiV1BoardsBoardIdOnboardingStartPost = async ( + boardId: string, + boardOnboardingStart: BoardOnboardingStart, + options?: RequestInit, +): Promise => { + return customFetch( + getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostUrl(boardId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(boardOnboardingStart), + }, + ); +}; + +export const getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardOnboardingStart }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardOnboardingStart }, + TContext + > => { + const mutationKey = [ + "startOnboardingApiV1BoardsBoardIdOnboardingStartPost", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType + >, + { boardId: string; data: BoardOnboardingStart } + > = (props) => { + const { boardId, data } = props ?? {}; + + return startOnboardingApiV1BoardsBoardIdOnboardingStartPost( + boardId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationBody = + BoardOnboardingStart; +export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationError = + HTTPValidationError; + +/** + * @summary Start Onboarding + */ +export const useStartOnboardingApiV1BoardsBoardIdOnboardingStartPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardOnboardingStart }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardOnboardingStart }, + TContext +> => { + return useMutation( + getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationOptions( + options, + ), + queryClient, + ); +}; diff --git a/frontend/src/api/generated/board-webhooks/board-webhooks.ts b/frontend/src/api/generated/board-webhooks/board-webhooks.ts index 5cdeb22d..3170f958 100644 --- a/frontend/src/api/generated/board-webhooks/board-webhooks.ts +++ b/frontend/src/api/generated/board-webhooks/board-webhooks.ts @@ -435,6 +435,163 @@ export const useCreateBoardWebhookApiV1BoardsBoardIdWebhooksPost = < queryClient, ); }; +/** + * Delete a webhook and its stored payload rows. + * @summary Delete Board Webhook + */ +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse200 = + { + data: OkResponse; + status: 200; + }; + +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseSuccess = + deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseError = + deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse = + + | deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseSuccess + | deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseError; + +export const getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteUrl = + (boardId: string, webhookId: string) => { + return `/api/v1/boards/${boardId}/webhooks/${webhookId}`; + }; + +export const deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete = + async ( + boardId: string, + webhookId: string, + options?: RequestInit, + ): Promise => { + return customFetch( + getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteUrl( + boardId, + webhookId, + ), + { + ...options, + method: "DELETE", + }, + ); + }; + +export const getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + > => { + const mutationKey = [ + "deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + { boardId: string; webhookId: string } + > = (props) => { + const { boardId, webhookId } = props ?? {}; + + return deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete( + boardId, + webhookId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type DeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationResult = + NonNullable< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + > + >; + +export type DeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Delete Board Webhook + */ +export const useDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + TError, + { boardId: string; webhookId: string }, + TContext +> => { + return useMutation( + getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationOptions( + options, + ), + queryClient, + ); +}; /** * Get one board webhook configuration. * @summary Get Board Webhook @@ -874,163 +1031,6 @@ export const useUpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch = < queryClient, ); }; -/** - * Delete a webhook and its stored payload rows. - * @summary Delete Board Webhook - */ -export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse200 = - { - data: OkResponse; - status: 200; - }; - -export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse422 = - { - data: HTTPValidationError; - status: 422; - }; - -export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseSuccess = - deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseError = - deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse = - - | deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseSuccess - | deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseError; - -export const getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteUrl = - (boardId: string, webhookId: string) => { - return `/api/v1/boards/${boardId}/webhooks/${webhookId}`; - }; - -export const deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete = - async ( - boardId: string, - webhookId: string, - options?: RequestInit, - ): Promise => { - return customFetch( - getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteUrl( - boardId, - webhookId, - ), - { - ...options, - method: "DELETE", - }, - ); - }; - -export const getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete - > - >, - TError, - { boardId: string; webhookId: string }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType< - typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete - > - >, - TError, - { boardId: string; webhookId: string }, - TContext - > => { - const mutationKey = [ - "deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType< - typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete - > - >, - { boardId: string; webhookId: string } - > = (props) => { - const { boardId, webhookId } = props ?? {}; - - return deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete( - boardId, - webhookId, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type DeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationResult = - NonNullable< - Awaited< - ReturnType< - typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete - > - > - >; - -export type DeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Delete Board Webhook - */ -export const useDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete - > - >, - TError, - { boardId: string; webhookId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType< - typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete - > - >, - TError, - { boardId: string; webhookId: string }, - TContext -> => { - return useMutation( - getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationOptions( - options, - ), - queryClient, - ); -}; /** * Open inbound webhook endpoint that stores payloads and nudges the board lead. * @summary Ingest Board Webhook diff --git a/frontend/src/api/generated/boards/boards.ts b/frontend/src/api/generated/boards/boards.ts index e5919845..920d51d3 100644 --- a/frontend/src/api/generated/boards/boards.ts +++ b/frontend/src/api/generated/boards/boards.ts @@ -363,6 +363,123 @@ export const useCreateBoardApiV1BoardsPost = < queryClient, ); }; +/** + * Delete a board and all dependent records. + * @summary Delete Board + */ +export type deleteBoardApiV1BoardsBoardIdDeleteResponse200 = { + data: OkResponse; + status: 200; +}; + +export type deleteBoardApiV1BoardsBoardIdDeleteResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type deleteBoardApiV1BoardsBoardIdDeleteResponseSuccess = + deleteBoardApiV1BoardsBoardIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteBoardApiV1BoardsBoardIdDeleteResponseError = + deleteBoardApiV1BoardsBoardIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteBoardApiV1BoardsBoardIdDeleteResponse = + | deleteBoardApiV1BoardsBoardIdDeleteResponseSuccess + | deleteBoardApiV1BoardsBoardIdDeleteResponseError; + +export const getDeleteBoardApiV1BoardsBoardIdDeleteUrl = (boardId: string) => { + return `/api/v1/boards/${boardId}`; +}; + +export const deleteBoardApiV1BoardsBoardIdDelete = async ( + boardId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getDeleteBoardApiV1BoardsBoardIdDeleteUrl(boardId), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getDeleteBoardApiV1BoardsBoardIdDeleteMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { boardId: string }, + TContext +> => { + const mutationKey = ["deleteBoardApiV1BoardsBoardIdDelete"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { boardId: string } + > = (props) => { + const { boardId } = props ?? {}; + + return deleteBoardApiV1BoardsBoardIdDelete(boardId, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type DeleteBoardApiV1BoardsBoardIdDeleteMutationResult = NonNullable< + Awaited> +>; + +export type DeleteBoardApiV1BoardsBoardIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Delete Board + */ +export const useDeleteBoardApiV1BoardsBoardIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { boardId: string }, + TContext +> => { + return useMutation( + getDeleteBoardApiV1BoardsBoardIdDeleteMutationOptions(options), + queryClient, + ); +}; /** * Get a board by id. * @summary Get Board @@ -683,362 +800,6 @@ export const useUpdateBoardApiV1BoardsBoardIdPatch = < queryClient, ); }; -/** - * Delete a board and all dependent records. - * @summary Delete Board - */ -export type deleteBoardApiV1BoardsBoardIdDeleteResponse200 = { - data: OkResponse; - status: 200; -}; - -export type deleteBoardApiV1BoardsBoardIdDeleteResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteBoardApiV1BoardsBoardIdDeleteResponseSuccess = - deleteBoardApiV1BoardsBoardIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteBoardApiV1BoardsBoardIdDeleteResponseError = - deleteBoardApiV1BoardsBoardIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteBoardApiV1BoardsBoardIdDeleteResponse = - | deleteBoardApiV1BoardsBoardIdDeleteResponseSuccess - | deleteBoardApiV1BoardsBoardIdDeleteResponseError; - -export const getDeleteBoardApiV1BoardsBoardIdDeleteUrl = (boardId: string) => { - return `/api/v1/boards/${boardId}`; -}; - -export const deleteBoardApiV1BoardsBoardIdDelete = async ( - boardId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteBoardApiV1BoardsBoardIdDeleteUrl(boardId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteBoardApiV1BoardsBoardIdDeleteMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { boardId: string }, - TContext -> => { - const mutationKey = ["deleteBoardApiV1BoardsBoardIdDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { boardId: string } - > = (props) => { - const { boardId } = props ?? {}; - - return deleteBoardApiV1BoardsBoardIdDelete(boardId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteBoardApiV1BoardsBoardIdDeleteMutationResult = NonNullable< - Awaited> ->; - -export type DeleteBoardApiV1BoardsBoardIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Delete Board - */ -export const useDeleteBoardApiV1BoardsBoardIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { boardId: string }, - TContext -> => { - return useMutation( - getDeleteBoardApiV1BoardsBoardIdDeleteMutationOptions(options), - queryClient, - ); -}; -/** - * Get a board snapshot view model. - * @summary Get Board Snapshot - */ -export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse200 = { - data: BoardSnapshot; - status: 200; -}; - -export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseSuccess = - getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse200 & { - headers: Headers; - }; -export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseError = - getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse422 & { - headers: Headers; - }; - -export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse = - | getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseSuccess - | getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseError; - -export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetUrl = ( - boardId: string, -) => { - return `/api/v1/boards/${boardId}/snapshot`; -}; - -export const getBoardSnapshotApiV1BoardsBoardIdSnapshotGet = async ( - boardId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetUrl(boardId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryKey = ( - boardId: string, -) => { - return [`/api/v1/boards/${boardId}/snapshot`] as const; -}; - -export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryOptions = < - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryKey(boardId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getBoardSnapshotApiV1BoardsBoardIdSnapshotGet(boardId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!boardId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryResult = - NonNullable< - Awaited> - >; -export type GetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryError = - HTTPValidationError; - -export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Board Snapshot - */ - -export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryOptions( - boardId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - /** * Get a grouped snapshot across related boards. @@ -1343,3 +1104,242 @@ export function useGetBoardGroupSnapshotApiV1BoardsBoardIdGroupSnapshotGet< return { ...query, queryKey: queryOptions.queryKey }; } + +/** + * Get a board snapshot view model. + * @summary Get Board Snapshot + */ +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse200 = { + data: BoardSnapshot; + status: 200; +}; + +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseSuccess = + getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse200 & { + headers: Headers; + }; +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseError = + getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse422 & { + headers: Headers; + }; + +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse = + | getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseSuccess + | getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseError; + +export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetUrl = ( + boardId: string, +) => { + return `/api/v1/boards/${boardId}/snapshot`; +}; + +export const getBoardSnapshotApiV1BoardsBoardIdSnapshotGet = async ( + boardId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetUrl(boardId), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryKey = ( + boardId: string, +) => { + return [`/api/v1/boards/${boardId}/snapshot`] as const; +}; + +export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryKey(boardId); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getBoardSnapshotApiV1BoardsBoardIdSnapshotGet(boardId, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!boardId, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type GetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryResult = + NonNullable< + Awaited> + >; +export type GetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryError = + HTTPValidationError; + +export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Get Board Snapshot + */ + +export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryOptions( + boardId, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} diff --git a/frontend/src/api/generated/custom-fields/custom-fields.ts b/frontend/src/api/generated/custom-fields/custom-fields.ts index 984078bb..2f5e1c91 100644 --- a/frontend/src/api/generated/custom-fields/custom-fields.ts +++ b/frontend/src/api/generated/custom-fields/custom-fields.ts @@ -425,6 +425,158 @@ export const useCreateOrgCustomFieldApiV1OrganizationsMeCustomFieldsPost = < queryClient, ); }; +/** + * Delete an org-level definition when it has no persisted task values. + * @summary Delete Org Custom Field + */ +export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse200 = + { + data: OkResponse; + status: 200; + }; + +export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponseSuccess = + deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponseError = + deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse = + + | deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponseSuccess + | deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponseError; + +export const getDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteUrl = + (taskCustomFieldDefinitionId: string) => { + return `/api/v1/organizations/me/custom-fields/${taskCustomFieldDefinitionId}`; + }; + +export const deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete = + async ( + taskCustomFieldDefinitionId: string, + options?: RequestInit, + ): Promise => { + return customFetch( + getDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteUrl( + taskCustomFieldDefinitionId, + ), + { + ...options, + method: "DELETE", + }, + ); + }; + +export const getDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete + > + >, + TError, + { taskCustomFieldDefinitionId: string }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType< + typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete + > + >, + TError, + { taskCustomFieldDefinitionId: string }, + TContext + > => { + const mutationKey = [ + "deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType< + typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete + > + >, + { taskCustomFieldDefinitionId: string } + > = (props) => { + const { taskCustomFieldDefinitionId } = props ?? {}; + + return deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete( + taskCustomFieldDefinitionId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type DeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteMutationResult = + NonNullable< + Awaited< + ReturnType< + typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete + > + > + >; + +export type DeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Delete Org Custom Field + */ +export const useDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete = + ( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete + > + >, + TError, + { taskCustomFieldDefinitionId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, + ): UseMutationResult< + Awaited< + ReturnType< + typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete + > + >, + TError, + { taskCustomFieldDefinitionId: string }, + TContext + > => { + return useMutation( + getDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteMutationOptions( + options, + ), + queryClient, + ); + }; /** * Update an organization-level task custom field definition. * @summary Update Org Custom Field @@ -597,155 +749,3 @@ export const useUpdateOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFi queryClient, ); }; -/** - * Delete an org-level definition when it has no persisted task values. - * @summary Delete Org Custom Field - */ -export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse200 = - { - data: OkResponse; - status: 200; - }; - -export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse422 = - { - data: HTTPValidationError; - status: 422; - }; - -export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponseSuccess = - deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponseError = - deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponse = - - | deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponseSuccess - | deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteResponseError; - -export const getDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteUrl = - (taskCustomFieldDefinitionId: string) => { - return `/api/v1/organizations/me/custom-fields/${taskCustomFieldDefinitionId}`; - }; - -export const deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete = - async ( - taskCustomFieldDefinitionId: string, - options?: RequestInit, - ): Promise => { - return customFetch( - getDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteUrl( - taskCustomFieldDefinitionId, - ), - { - ...options, - method: "DELETE", - }, - ); - }; - -export const getDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete - > - >, - TError, - { taskCustomFieldDefinitionId: string }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType< - typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete - > - >, - TError, - { taskCustomFieldDefinitionId: string }, - TContext - > => { - const mutationKey = [ - "deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType< - typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete - > - >, - { taskCustomFieldDefinitionId: string } - > = (props) => { - const { taskCustomFieldDefinitionId } = props ?? {}; - - return deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete( - taskCustomFieldDefinitionId, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type DeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteMutationResult = - NonNullable< - Awaited< - ReturnType< - typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete - > - > - >; - -export type DeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Delete Org Custom Field - */ -export const useDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete = - ( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete - > - >, - TError, - { taskCustomFieldDefinitionId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, - ): UseMutationResult< - Awaited< - ReturnType< - typeof deleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDelete - > - >, - TError, - { taskCustomFieldDefinitionId: string }, - TContext - > => { - return useMutation( - getDeleteOrgCustomFieldApiV1OrganizationsMeCustomFieldsTaskCustomFieldDefinitionIdDeleteMutationOptions( - options, - ), - queryClient, - ); - }; diff --git a/frontend/src/api/generated/gateways/gateways.ts b/frontend/src/api/generated/gateways/gateways.ts index a19963bc..63e1f9b1 100644 --- a/frontend/src/api/generated/gateways/gateways.ts +++ b/frontend/src/api/generated/gateways/gateways.ts @@ -48,34 +48,34 @@ import { customFetch } from "../../mutator"; type SecondParameter unknown> = Parameters[1]; /** - * Return gateway connectivity and session status. - * @summary Gateways Status + * List gateways for the caller's organization. + * @summary List Gateways */ -export type gatewaysStatusApiV1GatewaysStatusGetResponse200 = { - data: GatewaysStatusResponse; +export type listGatewaysApiV1GatewaysGetResponse200 = { + data: LimitOffsetPageTypeVarCustomizedGatewayRead; status: 200; }; -export type gatewaysStatusApiV1GatewaysStatusGetResponse422 = { +export type listGatewaysApiV1GatewaysGetResponse422 = { data: HTTPValidationError; status: 422; }; -export type gatewaysStatusApiV1GatewaysStatusGetResponseSuccess = - gatewaysStatusApiV1GatewaysStatusGetResponse200 & { +export type listGatewaysApiV1GatewaysGetResponseSuccess = + listGatewaysApiV1GatewaysGetResponse200 & { headers: Headers; }; -export type gatewaysStatusApiV1GatewaysStatusGetResponseError = - gatewaysStatusApiV1GatewaysStatusGetResponse422 & { +export type listGatewaysApiV1GatewaysGetResponseError = + listGatewaysApiV1GatewaysGetResponse422 & { headers: Headers; }; -export type gatewaysStatusApiV1GatewaysStatusGetResponse = - | gatewaysStatusApiV1GatewaysStatusGetResponseSuccess - | gatewaysStatusApiV1GatewaysStatusGetResponseError; +export type listGatewaysApiV1GatewaysGetResponse = + | listGatewaysApiV1GatewaysGetResponseSuccess + | listGatewaysApiV1GatewaysGetResponseError; -export const getGatewaysStatusApiV1GatewaysStatusGetUrl = ( - params?: GatewaysStatusApiV1GatewaysStatusGetParams, +export const getListGatewaysApiV1GatewaysGetUrl = ( + params?: ListGatewaysApiV1GatewaysGetParams, ) => { const normalizedParams = new URLSearchParams(); @@ -88,16 +88,16 @@ export const getGatewaysStatusApiV1GatewaysStatusGetUrl = ( const stringifiedParams = normalizedParams.toString(); return stringifiedParams.length > 0 - ? `/api/v1/gateways/status?${stringifiedParams}` - : `/api/v1/gateways/status`; + ? `/api/v1/gateways?${stringifiedParams}` + : `/api/v1/gateways`; }; -export const gatewaysStatusApiV1GatewaysStatusGet = async ( - params?: GatewaysStatusApiV1GatewaysStatusGetParams, +export const listGatewaysApiV1GatewaysGet = async ( + params?: ListGatewaysApiV1GatewaysGetParams, options?: RequestInit, -): Promise => { - return customFetch( - getGatewaysStatusApiV1GatewaysStatusGetUrl(params), +): Promise => { + return customFetch( + getListGatewaysApiV1GatewaysGetUrl(params), { ...options, method: "GET", @@ -105,21 +105,21 @@ export const gatewaysStatusApiV1GatewaysStatusGet = async ( ); }; -export const getGatewaysStatusApiV1GatewaysStatusGetQueryKey = ( - params?: GatewaysStatusApiV1GatewaysStatusGetParams, +export const getListGatewaysApiV1GatewaysGetQueryKey = ( + params?: ListGatewaysApiV1GatewaysGetParams, ) => { - return [`/api/v1/gateways/status`, ...(params ? [params] : [])] as const; + return [`/api/v1/gateways`, ...(params ? [params] : [])] as const; }; -export const getGatewaysStatusApiV1GatewaysStatusGetQueryOptions = < - TData = Awaited>, +export const getListGatewaysApiV1GatewaysGetQueryOptions = < + TData = Awaited>, TError = HTTPValidationError, >( - params?: GatewaysStatusApiV1GatewaysStatusGetParams, + params?: ListGatewaysApiV1GatewaysGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > @@ -130,45 +130,43 @@ export const getGatewaysStatusApiV1GatewaysStatusGetQueryOptions = < const { query: queryOptions, request: requestOptions } = options ?? {}; const queryKey = - queryOptions?.queryKey ?? - getGatewaysStatusApiV1GatewaysStatusGetQueryKey(params); + queryOptions?.queryKey ?? getListGatewaysApiV1GatewaysGetQueryKey(params); const queryFn: QueryFunction< - Awaited> + Awaited> > = ({ signal }) => - gatewaysStatusApiV1GatewaysStatusGet(params, { signal, ...requestOptions }); + listGatewaysApiV1GatewaysGet(params, { signal, ...requestOptions }); return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, + Awaited>, TError, TData > & { queryKey: DataTag }; }; -export type GatewaysStatusApiV1GatewaysStatusGetQueryResult = NonNullable< - Awaited> +export type ListGatewaysApiV1GatewaysGetQueryResult = NonNullable< + Awaited> >; -export type GatewaysStatusApiV1GatewaysStatusGetQueryError = - HTTPValidationError; +export type ListGatewaysApiV1GatewaysGetQueryError = HTTPValidationError; -export function useGatewaysStatusApiV1GatewaysStatusGet< - TData = Awaited>, +export function useListGatewaysApiV1GatewaysGet< + TData = Awaited>, TError = HTTPValidationError, >( - params: undefined | GatewaysStatusApiV1GatewaysStatusGetParams, + params: undefined | ListGatewaysApiV1GatewaysGetParams, options: { query: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > > & Pick< DefinedInitialDataOptions< - Awaited>, + Awaited>, TError, - Awaited> + Awaited> >, "initialData" >; @@ -178,24 +176,24 @@ export function useGatewaysStatusApiV1GatewaysStatusGet< ): DefinedUseQueryResult & { queryKey: DataTag; }; -export function useGatewaysStatusApiV1GatewaysStatusGet< - TData = Awaited>, +export function useListGatewaysApiV1GatewaysGet< + TData = Awaited>, TError = HTTPValidationError, >( - params?: GatewaysStatusApiV1GatewaysStatusGetParams, + params?: ListGatewaysApiV1GatewaysGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > > & Pick< UndefinedInitialDataOptions< - Awaited>, + Awaited>, TError, - Awaited> + Awaited> >, "initialData" >; @@ -205,15 +203,15 @@ export function useGatewaysStatusApiV1GatewaysStatusGet< ): UseQueryResult & { queryKey: DataTag; }; -export function useGatewaysStatusApiV1GatewaysStatusGet< - TData = Awaited>, +export function useListGatewaysApiV1GatewaysGet< + TData = Awaited>, TError = HTTPValidationError, >( - params?: GatewaysStatusApiV1GatewaysStatusGetParams, + params?: ListGatewaysApiV1GatewaysGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > @@ -225,18 +223,18 @@ export function useGatewaysStatusApiV1GatewaysStatusGet< queryKey: DataTag; }; /** - * @summary Gateways Status + * @summary List Gateways */ -export function useGatewaysStatusApiV1GatewaysStatusGet< - TData = Awaited>, +export function useListGatewaysApiV1GatewaysGet< + TData = Awaited>, TError = HTTPValidationError, >( - params?: GatewaysStatusApiV1GatewaysStatusGetParams, + params?: ListGatewaysApiV1GatewaysGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > @@ -247,7 +245,7 @@ export function useGatewaysStatusApiV1GatewaysStatusGet< ): UseQueryResult & { queryKey: DataTag; } { - const queryOptions = getGatewaysStatusApiV1GatewaysStatusGetQueryOptions( + const queryOptions = getListGatewaysApiV1GatewaysGetQueryOptions( params, options, ); @@ -260,6 +258,299 @@ export function useGatewaysStatusApiV1GatewaysStatusGet< return { ...query, queryKey: queryOptions.queryKey }; } +/** + * Create a gateway and provision or refresh its main agent. + * @summary Create Gateway + */ +export type createGatewayApiV1GatewaysPostResponse200 = { + data: GatewayRead; + status: 200; +}; + +export type createGatewayApiV1GatewaysPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type createGatewayApiV1GatewaysPostResponseSuccess = + createGatewayApiV1GatewaysPostResponse200 & { + headers: Headers; + }; +export type createGatewayApiV1GatewaysPostResponseError = + createGatewayApiV1GatewaysPostResponse422 & { + headers: Headers; + }; + +export type createGatewayApiV1GatewaysPostResponse = + | createGatewayApiV1GatewaysPostResponseSuccess + | createGatewayApiV1GatewaysPostResponseError; + +export const getCreateGatewayApiV1GatewaysPostUrl = () => { + return `/api/v1/gateways`; +}; + +export const createGatewayApiV1GatewaysPost = async ( + gatewayCreate: GatewayCreate, + options?: RequestInit, +): Promise => { + return customFetch( + getCreateGatewayApiV1GatewaysPostUrl(), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(gatewayCreate), + }, + ); +}; + +export const getCreateGatewayApiV1GatewaysPostMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: GatewayCreate }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: GatewayCreate }, + TContext +> => { + const mutationKey = ["createGatewayApiV1GatewaysPost"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: GatewayCreate } + > = (props) => { + const { data } = props ?? {}; + + return createGatewayApiV1GatewaysPost(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type CreateGatewayApiV1GatewaysPostMutationResult = NonNullable< + Awaited> +>; +export type CreateGatewayApiV1GatewaysPostMutationBody = GatewayCreate; +export type CreateGatewayApiV1GatewaysPostMutationError = HTTPValidationError; + +/** + * @summary Create Gateway + */ +export const useCreateGatewayApiV1GatewaysPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: GatewayCreate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { data: GatewayCreate }, + TContext +> => { + return useMutation( + getCreateGatewayApiV1GatewaysPostMutationOptions(options), + queryClient, + ); +}; +/** + * Return supported gateway protocol methods and events. + * @summary Gateway Commands + */ +export type gatewayCommandsApiV1GatewaysCommandsGetResponse200 = { + data: GatewayCommandsResponse; + status: 200; +}; + +export type gatewayCommandsApiV1GatewaysCommandsGetResponseSuccess = + gatewayCommandsApiV1GatewaysCommandsGetResponse200 & { + headers: Headers; + }; +export type gatewayCommandsApiV1GatewaysCommandsGetResponse = + gatewayCommandsApiV1GatewaysCommandsGetResponseSuccess; + +export const getGatewayCommandsApiV1GatewaysCommandsGetUrl = () => { + return `/api/v1/gateways/commands`; +}; + +export const gatewayCommandsApiV1GatewaysCommandsGet = async ( + options?: RequestInit, +): Promise => { + return customFetch( + getGatewayCommandsApiV1GatewaysCommandsGetUrl(), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGatewayCommandsApiV1GatewaysCommandsGetQueryKey = () => { + return [`/api/v1/gateways/commands`] as const; +}; + +export const getGatewayCommandsApiV1GatewaysCommandsGetQueryOptions = < + TData = Awaited>, + TError = unknown, +>(options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGatewayCommandsApiV1GatewaysCommandsGetQueryKey(); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + gatewayCommandsApiV1GatewaysCommandsGet({ signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type GatewayCommandsApiV1GatewaysCommandsGetQueryResult = NonNullable< + Awaited> +>; +export type GatewayCommandsApiV1GatewaysCommandsGetQueryError = unknown; + +export function useGatewayCommandsApiV1GatewaysCommandsGet< + TData = Awaited>, + TError = unknown, +>( + options: { + query: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGatewayCommandsApiV1GatewaysCommandsGet< + TData = Awaited>, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useGatewayCommandsApiV1GatewaysCommandsGet< + TData = Awaited>, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Gateway Commands + */ + +export function useGatewayCommandsApiV1GatewaysCommandsGet< + TData = Awaited>, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getGatewayCommandsApiV1GatewaysCommandsGetQueryOptions(options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + /** * List sessions for a gateway associated with a board. * @summary List Gateway Sessions @@ -1283,209 +1574,34 @@ export const useSendGatewaySessionMessageApiV1GatewaysSessionsSessionIdMessagePo ); }; /** - * Return supported gateway protocol methods and events. - * @summary Gateway Commands + * Return gateway connectivity and session status. + * @summary Gateways Status */ -export type gatewayCommandsApiV1GatewaysCommandsGetResponse200 = { - data: GatewayCommandsResponse; +export type gatewaysStatusApiV1GatewaysStatusGetResponse200 = { + data: GatewaysStatusResponse; status: 200; }; -export type gatewayCommandsApiV1GatewaysCommandsGetResponseSuccess = - gatewayCommandsApiV1GatewaysCommandsGetResponse200 & { - headers: Headers; - }; -export type gatewayCommandsApiV1GatewaysCommandsGetResponse = - gatewayCommandsApiV1GatewaysCommandsGetResponseSuccess; - -export const getGatewayCommandsApiV1GatewaysCommandsGetUrl = () => { - return `/api/v1/gateways/commands`; -}; - -export const gatewayCommandsApiV1GatewaysCommandsGet = async ( - options?: RequestInit, -): Promise => { - return customFetch( - getGatewayCommandsApiV1GatewaysCommandsGetUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGatewayCommandsApiV1GatewaysCommandsGetQueryKey = () => { - return [`/api/v1/gateways/commands`] as const; -}; - -export const getGatewayCommandsApiV1GatewaysCommandsGetQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGatewayCommandsApiV1GatewaysCommandsGetQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - gatewayCommandsApiV1GatewaysCommandsGet({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GatewayCommandsApiV1GatewaysCommandsGetQueryResult = NonNullable< - Awaited> ->; -export type GatewayCommandsApiV1GatewaysCommandsGetQueryError = unknown; - -export function useGatewayCommandsApiV1GatewaysCommandsGet< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGatewayCommandsApiV1GatewaysCommandsGet< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGatewayCommandsApiV1GatewaysCommandsGet< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Gateway Commands - */ - -export function useGatewayCommandsApiV1GatewaysCommandsGet< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getGatewayCommandsApiV1GatewaysCommandsGetQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - -/** - * List gateways for the caller's organization. - * @summary List Gateways - */ -export type listGatewaysApiV1GatewaysGetResponse200 = { - data: LimitOffsetPageTypeVarCustomizedGatewayRead; - status: 200; -}; - -export type listGatewaysApiV1GatewaysGetResponse422 = { +export type gatewaysStatusApiV1GatewaysStatusGetResponse422 = { data: HTTPValidationError; status: 422; }; -export type listGatewaysApiV1GatewaysGetResponseSuccess = - listGatewaysApiV1GatewaysGetResponse200 & { +export type gatewaysStatusApiV1GatewaysStatusGetResponseSuccess = + gatewaysStatusApiV1GatewaysStatusGetResponse200 & { headers: Headers; }; -export type listGatewaysApiV1GatewaysGetResponseError = - listGatewaysApiV1GatewaysGetResponse422 & { +export type gatewaysStatusApiV1GatewaysStatusGetResponseError = + gatewaysStatusApiV1GatewaysStatusGetResponse422 & { headers: Headers; }; -export type listGatewaysApiV1GatewaysGetResponse = - | listGatewaysApiV1GatewaysGetResponseSuccess - | listGatewaysApiV1GatewaysGetResponseError; +export type gatewaysStatusApiV1GatewaysStatusGetResponse = + | gatewaysStatusApiV1GatewaysStatusGetResponseSuccess + | gatewaysStatusApiV1GatewaysStatusGetResponseError; -export const getListGatewaysApiV1GatewaysGetUrl = ( - params?: ListGatewaysApiV1GatewaysGetParams, +export const getGatewaysStatusApiV1GatewaysStatusGetUrl = ( + params?: GatewaysStatusApiV1GatewaysStatusGetParams, ) => { const normalizedParams = new URLSearchParams(); @@ -1498,16 +1614,16 @@ export const getListGatewaysApiV1GatewaysGetUrl = ( const stringifiedParams = normalizedParams.toString(); return stringifiedParams.length > 0 - ? `/api/v1/gateways?${stringifiedParams}` - : `/api/v1/gateways`; + ? `/api/v1/gateways/status?${stringifiedParams}` + : `/api/v1/gateways/status`; }; -export const listGatewaysApiV1GatewaysGet = async ( - params?: ListGatewaysApiV1GatewaysGetParams, +export const gatewaysStatusApiV1GatewaysStatusGet = async ( + params?: GatewaysStatusApiV1GatewaysStatusGetParams, options?: RequestInit, -): Promise => { - return customFetch( - getListGatewaysApiV1GatewaysGetUrl(params), +): Promise => { + return customFetch( + getGatewaysStatusApiV1GatewaysStatusGetUrl(params), { ...options, method: "GET", @@ -1515,21 +1631,21 @@ export const listGatewaysApiV1GatewaysGet = async ( ); }; -export const getListGatewaysApiV1GatewaysGetQueryKey = ( - params?: ListGatewaysApiV1GatewaysGetParams, +export const getGatewaysStatusApiV1GatewaysStatusGetQueryKey = ( + params?: GatewaysStatusApiV1GatewaysStatusGetParams, ) => { - return [`/api/v1/gateways`, ...(params ? [params] : [])] as const; + return [`/api/v1/gateways/status`, ...(params ? [params] : [])] as const; }; -export const getListGatewaysApiV1GatewaysGetQueryOptions = < - TData = Awaited>, +export const getGatewaysStatusApiV1GatewaysStatusGetQueryOptions = < + TData = Awaited>, TError = HTTPValidationError, >( - params?: ListGatewaysApiV1GatewaysGetParams, + params?: GatewaysStatusApiV1GatewaysStatusGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > @@ -1540,43 +1656,45 @@ export const getListGatewaysApiV1GatewaysGetQueryOptions = < const { query: queryOptions, request: requestOptions } = options ?? {}; const queryKey = - queryOptions?.queryKey ?? getListGatewaysApiV1GatewaysGetQueryKey(params); + queryOptions?.queryKey ?? + getGatewaysStatusApiV1GatewaysStatusGetQueryKey(params); const queryFn: QueryFunction< - Awaited> + Awaited> > = ({ signal }) => - listGatewaysApiV1GatewaysGet(params, { signal, ...requestOptions }); + gatewaysStatusApiV1GatewaysStatusGet(params, { signal, ...requestOptions }); return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, + Awaited>, TError, TData > & { queryKey: DataTag }; }; -export type ListGatewaysApiV1GatewaysGetQueryResult = NonNullable< - Awaited> +export type GatewaysStatusApiV1GatewaysStatusGetQueryResult = NonNullable< + Awaited> >; -export type ListGatewaysApiV1GatewaysGetQueryError = HTTPValidationError; +export type GatewaysStatusApiV1GatewaysStatusGetQueryError = + HTTPValidationError; -export function useListGatewaysApiV1GatewaysGet< - TData = Awaited>, +export function useGatewaysStatusApiV1GatewaysStatusGet< + TData = Awaited>, TError = HTTPValidationError, >( - params: undefined | ListGatewaysApiV1GatewaysGetParams, + params: undefined | GatewaysStatusApiV1GatewaysStatusGetParams, options: { query: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > > & Pick< DefinedInitialDataOptions< - Awaited>, + Awaited>, TError, - Awaited> + Awaited> >, "initialData" >; @@ -1586,24 +1704,24 @@ export function useListGatewaysApiV1GatewaysGet< ): DefinedUseQueryResult & { queryKey: DataTag; }; -export function useListGatewaysApiV1GatewaysGet< - TData = Awaited>, +export function useGatewaysStatusApiV1GatewaysStatusGet< + TData = Awaited>, TError = HTTPValidationError, >( - params?: ListGatewaysApiV1GatewaysGetParams, + params?: GatewaysStatusApiV1GatewaysStatusGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > > & Pick< UndefinedInitialDataOptions< - Awaited>, + Awaited>, TError, - Awaited> + Awaited> >, "initialData" >; @@ -1613,15 +1731,15 @@ export function useListGatewaysApiV1GatewaysGet< ): UseQueryResult & { queryKey: DataTag; }; -export function useListGatewaysApiV1GatewaysGet< - TData = Awaited>, +export function useGatewaysStatusApiV1GatewaysStatusGet< + TData = Awaited>, TError = HTTPValidationError, >( - params?: ListGatewaysApiV1GatewaysGetParams, + params?: GatewaysStatusApiV1GatewaysStatusGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > @@ -1633,18 +1751,18 @@ export function useListGatewaysApiV1GatewaysGet< queryKey: DataTag; }; /** - * @summary List Gateways + * @summary Gateways Status */ -export function useListGatewaysApiV1GatewaysGet< - TData = Awaited>, +export function useGatewaysStatusApiV1GatewaysStatusGet< + TData = Awaited>, TError = HTTPValidationError, >( - params?: ListGatewaysApiV1GatewaysGetParams, + params?: GatewaysStatusApiV1GatewaysStatusGetParams, options?: { query?: Partial< UseQueryOptions< - Awaited>, + Awaited>, TError, TData > @@ -1655,7 +1773,7 @@ export function useListGatewaysApiV1GatewaysGet< ): UseQueryResult & { queryKey: DataTag; } { - const queryOptions = getListGatewaysApiV1GatewaysGetQueryOptions( + const queryOptions = getGatewaysStatusApiV1GatewaysStatusGetQueryOptions( params, options, ); @@ -1669,69 +1787,69 @@ export function useListGatewaysApiV1GatewaysGet< } /** - * Create a gateway and provision or refresh its main agent. - * @summary Create Gateway + * Delete a gateway in the caller's organization. + * @summary Delete Gateway */ -export type createGatewayApiV1GatewaysPostResponse200 = { - data: GatewayRead; +export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponse200 = { + data: OkResponse; status: 200; }; -export type createGatewayApiV1GatewaysPostResponse422 = { +export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponse422 = { data: HTTPValidationError; status: 422; }; -export type createGatewayApiV1GatewaysPostResponseSuccess = - createGatewayApiV1GatewaysPostResponse200 & { +export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponseSuccess = + deleteGatewayApiV1GatewaysGatewayIdDeleteResponse200 & { headers: Headers; }; -export type createGatewayApiV1GatewaysPostResponseError = - createGatewayApiV1GatewaysPostResponse422 & { +export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponseError = + deleteGatewayApiV1GatewaysGatewayIdDeleteResponse422 & { headers: Headers; }; -export type createGatewayApiV1GatewaysPostResponse = - | createGatewayApiV1GatewaysPostResponseSuccess - | createGatewayApiV1GatewaysPostResponseError; +export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponse = + | deleteGatewayApiV1GatewaysGatewayIdDeleteResponseSuccess + | deleteGatewayApiV1GatewaysGatewayIdDeleteResponseError; -export const getCreateGatewayApiV1GatewaysPostUrl = () => { - return `/api/v1/gateways`; +export const getDeleteGatewayApiV1GatewaysGatewayIdDeleteUrl = ( + gatewayId: string, +) => { + return `/api/v1/gateways/${gatewayId}`; }; -export const createGatewayApiV1GatewaysPost = async ( - gatewayCreate: GatewayCreate, +export const deleteGatewayApiV1GatewaysGatewayIdDelete = async ( + gatewayId: string, options?: RequestInit, -): Promise => { - return customFetch( - getCreateGatewayApiV1GatewaysPostUrl(), +): Promise => { + return customFetch( + getDeleteGatewayApiV1GatewaysGatewayIdDeleteUrl(gatewayId), { ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(gatewayCreate), + method: "DELETE", }, ); }; -export const getCreateGatewayApiV1GatewaysPostMutationOptions = < +export const getDeleteGatewayApiV1GatewaysGatewayIdDeleteMutationOptions = < TError = HTTPValidationError, TContext = unknown, >(options?: { mutation?: UseMutationOptions< - Awaited>, + Awaited>, TError, - { data: GatewayCreate }, + { gatewayId: string }, TContext >; request?: SecondParameter; }): UseMutationOptions< - Awaited>, + Awaited>, TError, - { data: GatewayCreate }, + { gatewayId: string }, TContext > => { - const mutationKey = ["createGatewayApiV1GatewaysPost"]; + const mutationKey = ["deleteGatewayApiV1GatewaysGatewayIdDelete"]; const { mutation: mutationOptions, request: requestOptions } = options ? options.mutation && "mutationKey" in options.mutation && @@ -1741,48 +1859,50 @@ export const getCreateGatewayApiV1GatewaysPostMutationOptions = < : { mutation: { mutationKey }, request: undefined }; const mutationFn: MutationFunction< - Awaited>, - { data: GatewayCreate } + Awaited>, + { gatewayId: string } > = (props) => { - const { data } = props ?? {}; + const { gatewayId } = props ?? {}; - return createGatewayApiV1GatewaysPost(data, requestOptions); + return deleteGatewayApiV1GatewaysGatewayIdDelete(gatewayId, requestOptions); }; return { mutationFn, ...mutationOptions }; }; -export type CreateGatewayApiV1GatewaysPostMutationResult = NonNullable< - Awaited> ->; -export type CreateGatewayApiV1GatewaysPostMutationBody = GatewayCreate; -export type CreateGatewayApiV1GatewaysPostMutationError = HTTPValidationError; +export type DeleteGatewayApiV1GatewaysGatewayIdDeleteMutationResult = + NonNullable< + Awaited> + >; + +export type DeleteGatewayApiV1GatewaysGatewayIdDeleteMutationError = + HTTPValidationError; /** - * @summary Create Gateway + * @summary Delete Gateway */ -export const useCreateGatewayApiV1GatewaysPost = < +export const useDeleteGatewayApiV1GatewaysGatewayIdDelete = < TError = HTTPValidationError, TContext = unknown, >( options?: { mutation?: UseMutationOptions< - Awaited>, + Awaited>, TError, - { data: GatewayCreate }, + { gatewayId: string }, TContext >; request?: SecondParameter; }, queryClient?: QueryClient, ): UseMutationResult< - Awaited>, + Awaited>, TError, - { data: GatewayCreate }, + { gatewayId: string }, TContext > => { return useMutation( - getCreateGatewayApiV1GatewaysPostMutationOptions(options), + getDeleteGatewayApiV1GatewaysGatewayIdDeleteMutationOptions(options), queryClient, ); }; @@ -2122,126 +2242,6 @@ export const useUpdateGatewayApiV1GatewaysGatewayIdPatch = < queryClient, ); }; -/** - * Delete a gateway in the caller's organization. - * @summary Delete Gateway - */ -export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponse200 = { - data: OkResponse; - status: 200; -}; - -export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponseSuccess = - deleteGatewayApiV1GatewaysGatewayIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponseError = - deleteGatewayApiV1GatewaysGatewayIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteGatewayApiV1GatewaysGatewayIdDeleteResponse = - | deleteGatewayApiV1GatewaysGatewayIdDeleteResponseSuccess - | deleteGatewayApiV1GatewaysGatewayIdDeleteResponseError; - -export const getDeleteGatewayApiV1GatewaysGatewayIdDeleteUrl = ( - gatewayId: string, -) => { - return `/api/v1/gateways/${gatewayId}`; -}; - -export const deleteGatewayApiV1GatewaysGatewayIdDelete = async ( - gatewayId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteGatewayApiV1GatewaysGatewayIdDeleteUrl(gatewayId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteGatewayApiV1GatewaysGatewayIdDeleteMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { gatewayId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { gatewayId: string }, - TContext -> => { - const mutationKey = ["deleteGatewayApiV1GatewaysGatewayIdDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { gatewayId: string } - > = (props) => { - const { gatewayId } = props ?? {}; - - return deleteGatewayApiV1GatewaysGatewayIdDelete(gatewayId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteGatewayApiV1GatewaysGatewayIdDeleteMutationResult = - NonNullable< - Awaited> - >; - -export type DeleteGatewayApiV1GatewaysGatewayIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Delete Gateway - */ -export const useDeleteGatewayApiV1GatewaysGatewayIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { gatewayId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { gatewayId: string }, - TContext -> => { - return useMutation( - getDeleteGatewayApiV1GatewaysGatewayIdDeleteMutationOptions(options), - queryClient, - ); -}; /** * Sync templates for a gateway and optionally rotate runtime settings. * @summary Sync Gateway Templates diff --git a/frontend/src/api/generated/model/activityEventRead.ts b/frontend/src/api/generated/model/activityEventRead.ts index ca2b7c29..88163152 100644 --- a/frontend/src/api/generated/model/activityEventRead.ts +++ b/frontend/src/api/generated/model/activityEventRead.ts @@ -4,15 +4,19 @@ * Mission Control API * OpenAPI spec version: 0.1.0 */ +import type { ActivityEventReadRouteParams } from "./activityEventReadRouteParams"; /** * Serialized activity event payload returned by activity endpoints. */ export interface ActivityEventRead { - id: string; - event_type: string; - message: string | null; agent_id: string | null; - task_id: string | null; + board_id?: string | null; created_at: string; + event_type: string; + id: string; + message: string | null; + route_name?: string | null; + route_params?: ActivityEventReadRouteParams; + task_id: string | null; } diff --git a/frontend/src/api/generated/model/activityEventReadRouteParams.ts b/frontend/src/api/generated/model/activityEventReadRouteParams.ts new file mode 100644 index 00000000..1871643f --- /dev/null +++ b/frontend/src/api/generated/model/activityEventReadRouteParams.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ActivityEventReadRouteParams = { [key: string]: string } | null; diff --git a/frontend/src/api/generated/model/activityTaskCommentFeedItemRead.ts b/frontend/src/api/generated/model/activityTaskCommentFeedItemRead.ts index 9f46a58b..df534a51 100644 --- a/frontend/src/api/generated/model/activityTaskCommentFeedItemRead.ts +++ b/frontend/src/api/generated/model/activityTaskCommentFeedItemRead.ts @@ -9,14 +9,14 @@ * Denormalized task-comment feed item enriched with task and board fields. */ export interface ActivityTaskCommentFeedItemRead { - id: string; - created_at: string; - message: string | null; agent_id: string | null; agent_name?: string | null; agent_role?: string | null; - task_id: string; - task_title: string; board_id: string; board_name: string; + created_at: string; + id: string; + message: string | null; + task_id: string; + task_title: string; } diff --git a/frontend/src/api/generated/model/agentCreate.ts b/frontend/src/api/generated/model/agentCreate.ts index 3f30056f..f714f1ff 100644 --- a/frontend/src/api/generated/model/agentCreate.ts +++ b/frontend/src/api/generated/model/agentCreate.ts @@ -13,19 +13,19 @@ import type { AgentCreateIdentityProfile } from "./agentCreateIdentityProfile"; export interface AgentCreate { /** Board id that scopes this agent. Omit only when policy allows global agents. */ board_id?: string | null; - /** - * Human-readable agent display name. - * @minLength 1 - */ - name: string; - /** Current lifecycle state used by coordinator logic. */ - status?: string; /** Runtime heartbeat behavior overrides for this agent. */ heartbeat_config?: AgentCreateHeartbeatConfig; /** Optional profile hints used by routing and policy checks. */ identity_profile?: AgentCreateIdentityProfile; /** Template that helps define initial intent and behavior. */ identity_template?: string | null; + /** + * Human-readable agent display name. + * @minLength 1 + */ + name: string; /** Template representing deeper agent instructions. */ soul_template?: string | null; + /** Current lifecycle state used by coordinator logic. */ + status?: string; } diff --git a/frontend/src/api/generated/model/agentHealthStatusResponse.ts b/frontend/src/api/generated/model/agentHealthStatusResponse.ts new file mode 100644 index 00000000..20e8d16e --- /dev/null +++ b/frontend/src/api/generated/model/agentHealthStatusResponse.ts @@ -0,0 +1,24 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +/** + * Agent-authenticated liveness payload for agent route probes. + */ +export interface AgentHealthStatusResponse { + /** Authenticated agent id derived from `X-Agent-Token`. */ + agent_id: string; + /** Board scope for the authenticated agent, when applicable. */ + board_id?: string | null; + /** Gateway owning the authenticated agent. */ + gateway_id: string; + /** Whether the authenticated agent is the board lead. */ + is_board_lead: boolean; + /** Indicates whether the probe check succeeded. */ + ok: boolean; + /** Current persisted lifecycle status for the authenticated agent. */ + status: string; +} diff --git a/frontend/src/api/generated/model/agentHeartbeatCreate.ts b/frontend/src/api/generated/model/agentHeartbeatCreate.ts index 9620c266..7e83d622 100644 --- a/frontend/src/api/generated/model/agentHeartbeatCreate.ts +++ b/frontend/src/api/generated/model/agentHeartbeatCreate.ts @@ -9,13 +9,13 @@ * Heartbeat payload used to create an agent lazily. */ export interface AgentHeartbeatCreate { - /** Agent health status string. */ - status?: string | null; + /** Optional board context for bootstrap. */ + board_id?: string | null; /** * Display name assigned during first heartbeat bootstrap. * @minLength 1 */ name: string; - /** Optional board context for bootstrap. */ - board_id?: string | null; + /** Agent health status string. */ + status?: string | null; } diff --git a/frontend/src/api/generated/model/agentRead.ts b/frontend/src/api/generated/model/agentRead.ts index 4b9fa5fe..99658f7f 100644 --- a/frontend/src/api/generated/model/agentRead.ts +++ b/frontend/src/api/generated/model/agentRead.ts @@ -13,35 +13,35 @@ import type { AgentReadIdentityProfile } from "./agentReadIdentityProfile"; export interface AgentRead { /** Board id that scopes this agent. Omit only when policy allows global agents. */ board_id?: string | null; + /** Creation timestamp. */ + created_at: string; + /** Gateway UUID that manages this agent. */ + gateway_id: string; + /** Runtime heartbeat behavior overrides for this agent. */ + heartbeat_config?: AgentReadHeartbeatConfig; + /** Agent UUID. */ + id: string; + /** Optional profile hints used by routing and policy checks. */ + identity_profile?: AgentReadIdentityProfile; + /** Template that helps define initial intent and behavior. */ + identity_template?: string | null; + /** Whether this agent is the board lead. */ + is_board_lead?: boolean; + /** Whether this agent is the primary gateway agent. */ + is_gateway_main?: boolean; + /** Last heartbeat timestamp. */ + last_seen_at?: string | null; /** * Human-readable agent display name. * @minLength 1 */ name: string; - /** Current lifecycle state used by coordinator logic. */ - status?: string; - /** Runtime heartbeat behavior overrides for this agent. */ - heartbeat_config?: AgentReadHeartbeatConfig; - /** Optional profile hints used by routing and policy checks. */ - identity_profile?: AgentReadIdentityProfile; - /** Template that helps define initial intent and behavior. */ - identity_template?: string | null; - /** Template representing deeper agent instructions. */ - soul_template?: string | null; - /** Agent UUID. */ - id: string; - /** Gateway UUID that manages this agent. */ - gateway_id: string; - /** Whether this agent is the board lead. */ - is_board_lead?: boolean; - /** Whether this agent is the primary gateway agent. */ - is_gateway_main?: boolean; /** Optional openclaw session token. */ openclaw_session_id?: string | null; - /** Last heartbeat timestamp. */ - last_seen_at?: string | null; - /** Creation timestamp. */ - created_at: string; + /** Template representing deeper agent instructions. */ + soul_template?: string | null; + /** Current lifecycle state used by coordinator logic. */ + status?: string; /** Last update timestamp. */ updated_at: string; } diff --git a/frontend/src/api/generated/model/agentUpdate.ts b/frontend/src/api/generated/model/agentUpdate.ts index 56b04682..1ec302aa 100644 --- a/frontend/src/api/generated/model/agentUpdate.ts +++ b/frontend/src/api/generated/model/agentUpdate.ts @@ -13,18 +13,18 @@ import type { AgentUpdateIdentityProfile } from "./agentUpdateIdentityProfile"; export interface AgentUpdate { /** Optional new board assignment. */ board_id?: string | null; - /** Whether this agent is treated as the board gateway main. */ - is_gateway_main?: boolean | null; - /** Optional replacement display name. */ - name?: string | null; - /** Optional replacement lifecycle status. */ - status?: string | null; /** Optional heartbeat policy override. */ heartbeat_config?: AgentUpdateHeartbeatConfig; /** Optional identity profile update values. */ identity_profile?: AgentUpdateIdentityProfile; /** Optional replacement identity template. */ identity_template?: string | null; + /** Whether this agent is treated as the board gateway main. */ + is_gateway_main?: boolean | null; + /** Optional replacement display name. */ + name?: string | null; /** Optional replacement soul template. */ soul_template?: string | null; + /** Optional replacement lifecycle status. */ + status?: string | null; } diff --git a/frontend/src/api/generated/model/approvalCreate.ts b/frontend/src/api/generated/model/approvalCreate.ts index ad2159ff..320c7a0a 100644 --- a/frontend/src/api/generated/model/approvalCreate.ts +++ b/frontend/src/api/generated/model/approvalCreate.ts @@ -13,16 +13,16 @@ import type { ApprovalCreateStatus } from "./approvalCreateStatus"; */ export interface ApprovalCreate { action_type: string; - task_id?: string | null; - task_ids?: string[]; - payload?: ApprovalCreatePayload; + agent_id?: string | null; /** * @minimum 0 * @maximum 100 */ confidence: number; + lead_reasoning?: string | null; + payload?: ApprovalCreatePayload; rubric_scores?: ApprovalCreateRubricScores; status?: ApprovalCreateStatus; - agent_id?: string | null; - lead_reasoning?: string | null; + task_id?: string | null; + task_ids?: string[]; } diff --git a/frontend/src/api/generated/model/approvalRead.ts b/frontend/src/api/generated/model/approvalRead.ts index 48a9c348..97158858 100644 --- a/frontend/src/api/generated/model/approvalRead.ts +++ b/frontend/src/api/generated/model/approvalRead.ts @@ -13,20 +13,20 @@ import type { ApprovalReadStatus } from "./approvalReadStatus"; */ export interface ApprovalRead { action_type: string; - task_id?: string | null; - task_ids?: string[]; - payload?: ApprovalReadPayload; + agent_id?: string | null; + board_id: string; /** * @minimum 0 * @maximum 100 */ confidence: number; + created_at: string; + id: string; + payload?: ApprovalReadPayload; + resolved_at?: string | null; rubric_scores?: ApprovalReadRubricScores; status?: ApprovalReadStatus; - id: string; - board_id: string; + task_id?: string | null; + task_ids?: string[]; task_titles?: string[]; - agent_id?: string | null; - created_at: string; - resolved_at?: string | null; } diff --git a/frontend/src/api/generated/model/blockedTaskDetail.ts b/frontend/src/api/generated/model/blockedTaskDetail.ts index beac973b..d9e71124 100644 --- a/frontend/src/api/generated/model/blockedTaskDetail.ts +++ b/frontend/src/api/generated/model/blockedTaskDetail.ts @@ -9,6 +9,6 @@ * Error detail payload listing blocking dependency task identifiers. */ export interface BlockedTaskDetail { - message: string; blocked_by_task_ids?: string[]; + message: string; } diff --git a/frontend/src/api/generated/model/boardCreate.ts b/frontend/src/api/generated/model/boardCreate.ts index 44163dac..915e26f0 100644 --- a/frontend/src/api/generated/model/boardCreate.ts +++ b/frontend/src/api/generated/model/boardCreate.ts @@ -10,21 +10,22 @@ import type { BoardCreateSuccessMetrics } from "./boardCreateSuccessMetrics"; * Payload for creating a board. */ export interface BoardCreate { - name: string; - slug: string; - description: string; - gateway_id?: string | null; + block_status_changes_with_pending_approval?: boolean; board_group_id?: string | null; board_type?: string; - objective?: string | null; - success_metrics?: BoardCreateSuccessMetrics; - target_date?: string | null; + comment_required_for_review?: boolean; + description: string; + gateway_id?: string | null; goal_confirmed?: boolean; goal_source?: string | null; - require_approval_for_done?: boolean; - require_review_before_done?: boolean; - block_status_changes_with_pending_approval?: boolean; - only_lead_can_change_status?: boolean; /** @minimum 0 */ max_agents?: number; + name: string; + objective?: string | null; + only_lead_can_change_status?: boolean; + require_approval_for_done?: boolean; + require_review_before_done?: boolean; + slug: string; + success_metrics?: BoardCreateSuccessMetrics; + target_date?: string | null; } diff --git a/frontend/src/api/generated/model/boardGroupCreate.ts b/frontend/src/api/generated/model/boardGroupCreate.ts index 96ac5a33..bd7da983 100644 --- a/frontend/src/api/generated/model/boardGroupCreate.ts +++ b/frontend/src/api/generated/model/boardGroupCreate.ts @@ -9,7 +9,7 @@ * Payload for creating a board group. */ export interface BoardGroupCreate { + description?: string | null; name: string; slug: string; - description?: string | null; } diff --git a/frontend/src/api/generated/model/boardGroupHeartbeatApplyResult.ts b/frontend/src/api/generated/model/boardGroupHeartbeatApplyResult.ts index 298eb90e..d5bb950a 100644 --- a/frontend/src/api/generated/model/boardGroupHeartbeatApplyResult.ts +++ b/frontend/src/api/generated/model/boardGroupHeartbeatApplyResult.ts @@ -11,7 +11,7 @@ import type { BoardGroupHeartbeatApplyResultRequested } from "./boardGroupHeartb */ export interface BoardGroupHeartbeatApplyResult { board_group_id: string; + failed_agent_ids: string[]; requested: BoardGroupHeartbeatApplyResultRequested; updated_agent_ids: string[]; - failed_agent_ids: string[]; } diff --git a/frontend/src/api/generated/model/boardGroupMemoryCreate.ts b/frontend/src/api/generated/model/boardGroupMemoryCreate.ts index 547af52a..92bac015 100644 --- a/frontend/src/api/generated/model/boardGroupMemoryCreate.ts +++ b/frontend/src/api/generated/model/boardGroupMemoryCreate.ts @@ -11,6 +11,6 @@ export interface BoardGroupMemoryCreate { /** @minLength 1 */ content: string; - tags?: string[] | null; source?: string | null; + tags?: string[] | null; } diff --git a/frontend/src/api/generated/model/boardGroupMemoryRead.ts b/frontend/src/api/generated/model/boardGroupMemoryRead.ts index ba2d1a14..364d468d 100644 --- a/frontend/src/api/generated/model/boardGroupMemoryRead.ts +++ b/frontend/src/api/generated/model/boardGroupMemoryRead.ts @@ -9,11 +9,11 @@ * Serialized board-group memory entry returned from read endpoints. */ export interface BoardGroupMemoryRead { - id: string; board_group_id: string; content: string; - tags?: string[] | null; - source?: string | null; - is_chat?: boolean; created_at: string; + id: string; + is_chat?: boolean; + source?: string | null; + tags?: string[] | null; } diff --git a/frontend/src/api/generated/model/boardGroupRead.ts b/frontend/src/api/generated/model/boardGroupRead.ts index 91e0a584..174c3cc7 100644 --- a/frontend/src/api/generated/model/boardGroupRead.ts +++ b/frontend/src/api/generated/model/boardGroupRead.ts @@ -9,11 +9,11 @@ * Board-group payload returned from read endpoints. */ export interface BoardGroupRead { - name: string; - slug: string; + created_at: string; description?: string | null; id: string; + name: string; organization_id: string; - created_at: string; + slug: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/boardGroupSnapshot.ts b/frontend/src/api/generated/model/boardGroupSnapshot.ts index fc668108..7f6143d4 100644 --- a/frontend/src/api/generated/model/boardGroupSnapshot.ts +++ b/frontend/src/api/generated/model/boardGroupSnapshot.ts @@ -11,6 +11,6 @@ import type { BoardGroupRead } from "./boardGroupRead"; * Top-level board-group snapshot response payload. */ export interface BoardGroupSnapshot { - group?: BoardGroupRead | null; boards?: BoardGroupBoardSnapshot[]; + group?: BoardGroupRead | null; } diff --git a/frontend/src/api/generated/model/boardGroupTaskSummary.ts b/frontend/src/api/generated/model/boardGroupTaskSummary.ts index 28b76f12..f71cb04f 100644 --- a/frontend/src/api/generated/model/boardGroupTaskSummary.ts +++ b/frontend/src/api/generated/model/boardGroupTaskSummary.ts @@ -10,17 +10,17 @@ import type { TagRef } from "./tagRef"; * Task summary row used inside board-group snapshot responses. */ export interface BoardGroupTaskSummary { - id: string; - board_id: string; - board_name: string; - title: string; - status: string; - priority: string; assigned_agent_id?: string | null; assignee?: string | null; - due_at?: string | null; - in_progress_at?: string | null; - tags?: TagRef[]; + board_id: string; + board_name: string; created_at: string; + due_at?: string | null; + id: string; + in_progress_at?: string | null; + priority: string; + status: string; + tags?: TagRef[]; + title: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/boardGroupUpdate.ts b/frontend/src/api/generated/model/boardGroupUpdate.ts index b7a6e7a4..b8318905 100644 --- a/frontend/src/api/generated/model/boardGroupUpdate.ts +++ b/frontend/src/api/generated/model/boardGroupUpdate.ts @@ -9,7 +9,7 @@ * Payload for partial board-group updates. */ export interface BoardGroupUpdate { + description?: string | null; name?: string | null; slug?: string | null; - description?: string | null; } diff --git a/frontend/src/api/generated/model/boardMemoryCreate.ts b/frontend/src/api/generated/model/boardMemoryCreate.ts index 30900a0c..f2b1afbc 100644 --- a/frontend/src/api/generated/model/boardMemoryCreate.ts +++ b/frontend/src/api/generated/model/boardMemoryCreate.ts @@ -11,6 +11,6 @@ export interface BoardMemoryCreate { /** @minLength 1 */ content: string; - tags?: string[] | null; source?: string | null; + tags?: string[] | null; } diff --git a/frontend/src/api/generated/model/boardMemoryRead.ts b/frontend/src/api/generated/model/boardMemoryRead.ts index f8b1adcc..805c493b 100644 --- a/frontend/src/api/generated/model/boardMemoryRead.ts +++ b/frontend/src/api/generated/model/boardMemoryRead.ts @@ -9,11 +9,11 @@ * Serialized board memory entry returned from read endpoints. */ export interface BoardMemoryRead { - id: string; board_id: string; content: string; - tags?: string[] | null; - source?: string | null; - is_chat?: boolean; created_at: string; + id: string; + is_chat?: boolean; + source?: string | null; + tags?: string[] | null; } diff --git a/frontend/src/api/generated/model/boardOnboardingAgentComplete.ts b/frontend/src/api/generated/model/boardOnboardingAgentComplete.ts index 351f258c..a2be93e4 100644 --- a/frontend/src/api/generated/model/boardOnboardingAgentComplete.ts +++ b/frontend/src/api/generated/model/boardOnboardingAgentComplete.ts @@ -13,10 +13,10 @@ import type { BoardOnboardingUserProfile } from "./boardOnboardingUserProfile"; */ export interface BoardOnboardingAgentComplete { board_type: string; + lead_agent?: BoardOnboardingLeadAgentDraft | null; objective?: string | null; + status: "complete"; success_metrics?: BoardOnboardingAgentCompleteSuccessMetrics; target_date?: string | null; - status: "complete"; user_profile?: BoardOnboardingUserProfile | null; - lead_agent?: BoardOnboardingLeadAgentDraft | null; } diff --git a/frontend/src/api/generated/model/boardOnboardingAgentQuestion.ts b/frontend/src/api/generated/model/boardOnboardingAgentQuestion.ts index cef146c2..2ca429e9 100644 --- a/frontend/src/api/generated/model/boardOnboardingAgentQuestion.ts +++ b/frontend/src/api/generated/model/boardOnboardingAgentQuestion.ts @@ -10,8 +10,8 @@ import type { BoardOnboardingQuestionOption } from "./boardOnboardingQuestionOpt * Question payload emitted by the onboarding assistant. */ export interface BoardOnboardingAgentQuestion { - /** @minLength 1 */ - question: string; /** @minItems 1 */ options: BoardOnboardingQuestionOption[]; + /** @minLength 1 */ + question: string; } diff --git a/frontend/src/api/generated/model/boardOnboardingLeadAgentDraft.ts b/frontend/src/api/generated/model/boardOnboardingLeadAgentDraft.ts index 5778dcdb..a1bf356d 100644 --- a/frontend/src/api/generated/model/boardOnboardingLeadAgentDraft.ts +++ b/frontend/src/api/generated/model/boardOnboardingLeadAgentDraft.ts @@ -10,11 +10,11 @@ import type { BoardOnboardingLeadAgentDraftIdentityProfile } from "./boardOnboar * Editable lead-agent draft configuration. */ export interface BoardOnboardingLeadAgentDraft { - name?: string | null; - identity_profile?: BoardOnboardingLeadAgentDraftIdentityProfile; autonomy_level?: "ask_first" | "balanced" | "autonomous" | null; - verbosity?: "concise" | "balanced" | "detailed" | null; + custom_instructions?: string | null; + identity_profile?: BoardOnboardingLeadAgentDraftIdentityProfile; + name?: string | null; output_format?: "bullets" | "mixed" | "narrative" | null; update_cadence?: "asap" | "hourly" | "daily" | "weekly" | null; - custom_instructions?: string | null; + verbosity?: "concise" | "balanced" | "detailed" | null; } diff --git a/frontend/src/api/generated/model/boardOnboardingRead.ts b/frontend/src/api/generated/model/boardOnboardingRead.ts index 61135086..e93ae067 100644 --- a/frontend/src/api/generated/model/boardOnboardingRead.ts +++ b/frontend/src/api/generated/model/boardOnboardingRead.ts @@ -11,12 +11,12 @@ import type { BoardOnboardingReadMessages } from "./boardOnboardingReadMessages" * Stored onboarding session state returned by API endpoints. */ export interface BoardOnboardingRead { - id: string; board_id: string; + created_at: string; + draft_goal?: BoardOnboardingAgentComplete | null; + id: string; + messages?: BoardOnboardingReadMessages; session_key: string; status: string; - messages?: BoardOnboardingReadMessages; - draft_goal?: BoardOnboardingAgentComplete | null; - created_at: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/boardOnboardingUserProfile.ts b/frontend/src/api/generated/model/boardOnboardingUserProfile.ts index 10d2599a..8c933ac7 100644 --- a/frontend/src/api/generated/model/boardOnboardingUserProfile.ts +++ b/frontend/src/api/generated/model/boardOnboardingUserProfile.ts @@ -9,9 +9,9 @@ * User-profile preferences gathered during onboarding. */ export interface BoardOnboardingUserProfile { + context?: string | null; + notes?: string | null; preferred_name?: string | null; pronouns?: string | null; timezone?: string | null; - notes?: string | null; - context?: string | null; } diff --git a/frontend/src/api/generated/model/boardRead.ts b/frontend/src/api/generated/model/boardRead.ts index f7482496..20dc5850 100644 --- a/frontend/src/api/generated/model/boardRead.ts +++ b/frontend/src/api/generated/model/boardRead.ts @@ -10,25 +10,26 @@ import type { BoardReadSuccessMetrics } from "./boardReadSuccessMetrics"; * Board payload returned from read endpoints. */ export interface BoardRead { - name: string; - slug: string; - description: string; - gateway_id?: string | null; + block_status_changes_with_pending_approval?: boolean; board_group_id?: string | null; board_type?: string; - objective?: string | null; - success_metrics?: BoardReadSuccessMetrics; - target_date?: string | null; + comment_required_for_review?: boolean; + created_at: string; + description: string; + gateway_id?: string | null; goal_confirmed?: boolean; goal_source?: string | null; - require_approval_for_done?: boolean; - require_review_before_done?: boolean; - block_status_changes_with_pending_approval?: boolean; - only_lead_can_change_status?: boolean; + id: string; /** @minimum 0 */ max_agents?: number; - id: string; + name: string; + objective?: string | null; + only_lead_can_change_status?: boolean; organization_id: string; - created_at: string; + require_approval_for_done?: boolean; + require_review_before_done?: boolean; + slug: string; + success_metrics?: BoardReadSuccessMetrics; + target_date?: string | null; updated_at: string; } diff --git a/frontend/src/api/generated/model/boardSnapshot.ts b/frontend/src/api/generated/model/boardSnapshot.ts index 10bee2fd..a7528218 100644 --- a/frontend/src/api/generated/model/boardSnapshot.ts +++ b/frontend/src/api/generated/model/boardSnapshot.ts @@ -14,10 +14,10 @@ import type { TaskCardRead } from "./taskCardRead"; * Aggregated board payload used by board snapshot endpoints. */ export interface BoardSnapshot { - board: BoardRead; - tasks: TaskCardRead[]; agents: AgentRead[]; approvals: ApprovalRead[]; + board: BoardRead; chat_messages: BoardMemoryRead[]; pending_approvals_count?: number; + tasks: TaskCardRead[]; } diff --git a/frontend/src/api/generated/model/boardUpdate.ts b/frontend/src/api/generated/model/boardUpdate.ts index 19c57fed..bb0a8900 100644 --- a/frontend/src/api/generated/model/boardUpdate.ts +++ b/frontend/src/api/generated/model/boardUpdate.ts @@ -10,20 +10,21 @@ import type { BoardUpdateSuccessMetrics } from "./boardUpdateSuccessMetrics"; * Payload for partial board updates. */ export interface BoardUpdate { - name?: string | null; - slug?: string | null; - description?: string | null; - gateway_id?: string | null; + block_status_changes_with_pending_approval?: boolean | null; board_group_id?: string | null; board_type?: string | null; - objective?: string | null; - success_metrics?: BoardUpdateSuccessMetrics; - target_date?: string | null; + comment_required_for_review?: boolean | null; + description?: string | null; + gateway_id?: string | null; goal_confirmed?: boolean | null; goal_source?: string | null; + max_agents?: number | null; + name?: string | null; + objective?: string | null; + only_lead_can_change_status?: boolean | null; require_approval_for_done?: boolean | null; require_review_before_done?: boolean | null; - block_status_changes_with_pending_approval?: boolean | null; - only_lead_can_change_status?: boolean | null; - max_agents?: number | null; + slug?: string | null; + success_metrics?: BoardUpdateSuccessMetrics; + target_date?: string | null; } diff --git a/frontend/src/api/generated/model/boardWebhookCreate.ts b/frontend/src/api/generated/model/boardWebhookCreate.ts index a1874834..dea943c9 100644 --- a/frontend/src/api/generated/model/boardWebhookCreate.ts +++ b/frontend/src/api/generated/model/boardWebhookCreate.ts @@ -9,8 +9,8 @@ * Payload for creating a board webhook. */ export interface BoardWebhookCreate { + agent_id?: string | null; /** @minLength 1 */ description: string; enabled?: boolean; - agent_id?: string | null; } diff --git a/frontend/src/api/generated/model/boardWebhookIngestResponse.ts b/frontend/src/api/generated/model/boardWebhookIngestResponse.ts index 179c77d7..806dcb4c 100644 --- a/frontend/src/api/generated/model/boardWebhookIngestResponse.ts +++ b/frontend/src/api/generated/model/boardWebhookIngestResponse.ts @@ -9,8 +9,8 @@ * Response payload for inbound webhook ingestion. */ export interface BoardWebhookIngestResponse { - ok?: boolean; board_id: string; - webhook_id: string; + ok?: boolean; payload_id: string; + webhook_id: string; } diff --git a/frontend/src/api/generated/model/boardWebhookPayloadRead.ts b/frontend/src/api/generated/model/boardWebhookPayloadRead.ts index ebd79a3b..846b388c 100644 --- a/frontend/src/api/generated/model/boardWebhookPayloadRead.ts +++ b/frontend/src/api/generated/model/boardWebhookPayloadRead.ts @@ -11,12 +11,12 @@ import type { BoardWebhookPayloadReadPayload } from "./boardWebhookPayloadReadPa * Serialized stored webhook payload. */ export interface BoardWebhookPayloadRead { - id: string; board_id: string; - webhook_id: string; - payload?: BoardWebhookPayloadReadPayload; - headers?: BoardWebhookPayloadReadHeaders; - source_ip?: string | null; content_type?: string | null; + headers?: BoardWebhookPayloadReadHeaders; + id: string; + payload?: BoardWebhookPayloadReadPayload; received_at: string; + source_ip?: string | null; + webhook_id: string; } diff --git a/frontend/src/api/generated/model/boardWebhookRead.ts b/frontend/src/api/generated/model/boardWebhookRead.ts index 2c8d5445..b8171337 100644 --- a/frontend/src/api/generated/model/boardWebhookRead.ts +++ b/frontend/src/api/generated/model/boardWebhookRead.ts @@ -9,13 +9,13 @@ * Serialized board webhook configuration. */ export interface BoardWebhookRead { - id: string; - board_id: string; agent_id?: string | null; + board_id: string; + created_at: string; description: string; enabled: boolean; endpoint_path: string; endpoint_url?: string | null; - created_at: string; + id: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/boardWebhookUpdate.ts b/frontend/src/api/generated/model/boardWebhookUpdate.ts index 543896b1..26356b5a 100644 --- a/frontend/src/api/generated/model/boardWebhookUpdate.ts +++ b/frontend/src/api/generated/model/boardWebhookUpdate.ts @@ -9,7 +9,7 @@ * Payload for updating a board webhook. */ export interface BoardWebhookUpdate { + agent_id?: string | null; description?: string | null; enabled?: boolean | null; - agent_id?: string | null; } diff --git a/frontend/src/api/generated/model/dashboardKpis.ts b/frontend/src/api/generated/model/dashboardKpis.ts index f35f615f..f0d8a053 100644 --- a/frontend/src/api/generated/model/dashboardKpis.ts +++ b/frontend/src/api/generated/model/dashboardKpis.ts @@ -10,7 +10,11 @@ */ export interface DashboardKpis { active_agents: number; - tasks_in_progress: number; + done_tasks: number; error_rate_pct: number; + in_progress_tasks: number; + inbox_tasks: number; median_cycle_time_hours_7d: number | null; + review_tasks: number; + tasks_in_progress: number; } diff --git a/frontend/src/api/generated/model/dashboardMetrics.ts b/frontend/src/api/generated/model/dashboardMetrics.ts index 478a3d97..ce457839 100644 --- a/frontend/src/api/generated/model/dashboardMetrics.ts +++ b/frontend/src/api/generated/model/dashboardMetrics.ts @@ -6,6 +6,7 @@ */ import type { DashboardKpis } from "./dashboardKpis"; import type { DashboardMetricsRange } from "./dashboardMetricsRange"; +import type { DashboardPendingApprovals } from "./dashboardPendingApprovals"; import type { DashboardSeriesSet } from "./dashboardSeriesSet"; import type { DashboardWipSeriesSet } from "./dashboardWipSeriesSet"; @@ -13,11 +14,12 @@ import type { DashboardWipSeriesSet } from "./dashboardWipSeriesSet"; * Complete dashboard metrics response payload. */ export interface DashboardMetrics { - range: DashboardMetricsRange; - generated_at: string; - kpis: DashboardKpis; - throughput: DashboardSeriesSet; cycle_time: DashboardSeriesSet; error_rate: DashboardSeriesSet; + generated_at: string; + kpis: DashboardKpis; + pending_approvals: DashboardPendingApprovals; + range: DashboardMetricsRange; + throughput: DashboardSeriesSet; wip: DashboardWipSeriesSet; } diff --git a/frontend/src/api/generated/model/dashboardPendingApproval.ts b/frontend/src/api/generated/model/dashboardPendingApproval.ts new file mode 100644 index 00000000..66fab8a6 --- /dev/null +++ b/frontend/src/api/generated/model/dashboardPendingApproval.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +/** + * Single pending approval item for cross-board dashboard listing. + */ +export interface DashboardPendingApproval { + action_type: string; + approval_id: string; + board_id: string; + board_name: string; + confidence: number; + created_at: string; + task_title?: string | null; +} diff --git a/frontend/src/api/generated/model/dashboardPendingApprovals.ts b/frontend/src/api/generated/model/dashboardPendingApprovals.ts new file mode 100644 index 00000000..26dbe842 --- /dev/null +++ b/frontend/src/api/generated/model/dashboardPendingApprovals.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { DashboardPendingApproval } from "./dashboardPendingApproval"; + +/** + * Pending approval snapshot used on the dashboard. + */ +export interface DashboardPendingApprovals { + items: DashboardPendingApproval[]; + total: number; +} diff --git a/frontend/src/api/generated/model/dashboardRangeSeries.ts b/frontend/src/api/generated/model/dashboardRangeSeries.ts index 9e3fd7ae..f996bcfa 100644 --- a/frontend/src/api/generated/model/dashboardRangeSeries.ts +++ b/frontend/src/api/generated/model/dashboardRangeSeries.ts @@ -12,7 +12,7 @@ import type { DashboardSeriesPoint } from "./dashboardSeriesPoint"; * Series payload for a single range/bucket combination. */ export interface DashboardRangeSeries { - range: DashboardRangeSeriesRange; bucket: DashboardRangeSeriesBucket; points: DashboardSeriesPoint[]; + range: DashboardRangeSeriesRange; } diff --git a/frontend/src/api/generated/model/dashboardSeriesSet.ts b/frontend/src/api/generated/model/dashboardSeriesSet.ts index c7742a2b..086f4159 100644 --- a/frontend/src/api/generated/model/dashboardSeriesSet.ts +++ b/frontend/src/api/generated/model/dashboardSeriesSet.ts @@ -10,6 +10,6 @@ import type { DashboardRangeSeries } from "./dashboardRangeSeries"; * Primary vs comparison pair for generic series metrics. */ export interface DashboardSeriesSet { - primary: DashboardRangeSeries; comparison: DashboardRangeSeries; + primary: DashboardRangeSeries; } diff --git a/frontend/src/api/generated/model/dashboardWipPoint.ts b/frontend/src/api/generated/model/dashboardWipPoint.ts index b36c941b..7390dee8 100644 --- a/frontend/src/api/generated/model/dashboardWipPoint.ts +++ b/frontend/src/api/generated/model/dashboardWipPoint.ts @@ -9,9 +9,9 @@ * Work-in-progress point split by task status buckets. */ export interface DashboardWipPoint { - period: string; - inbox: number; - in_progress: number; - review: number; done: number; + in_progress: number; + inbox: number; + period: string; + review: number; } diff --git a/frontend/src/api/generated/model/dashboardWipRangeSeries.ts b/frontend/src/api/generated/model/dashboardWipRangeSeries.ts index 26545332..e439b044 100644 --- a/frontend/src/api/generated/model/dashboardWipRangeSeries.ts +++ b/frontend/src/api/generated/model/dashboardWipRangeSeries.ts @@ -12,7 +12,7 @@ import type { DashboardWipRangeSeriesRange } from "./dashboardWipRangeSeriesRang * WIP series payload for a single range/bucket combination. */ export interface DashboardWipRangeSeries { - range: DashboardWipRangeSeriesRange; bucket: DashboardWipRangeSeriesBucket; points: DashboardWipPoint[]; + range: DashboardWipRangeSeriesRange; } diff --git a/frontend/src/api/generated/model/dashboardWipSeriesSet.ts b/frontend/src/api/generated/model/dashboardWipSeriesSet.ts index ff4c59b2..f2f9e9c6 100644 --- a/frontend/src/api/generated/model/dashboardWipSeriesSet.ts +++ b/frontend/src/api/generated/model/dashboardWipSeriesSet.ts @@ -10,6 +10,6 @@ import type { DashboardWipRangeSeries } from "./dashboardWipRangeSeries"; * Primary vs comparison pair for WIP status series metrics. */ export interface DashboardWipSeriesSet { - primary: DashboardWipRangeSeries; comparison: DashboardWipRangeSeries; + primary: DashboardWipRangeSeries; } diff --git a/frontend/src/api/generated/model/gatewayCommandsResponse.ts b/frontend/src/api/generated/model/gatewayCommandsResponse.ts index 592df7e7..d763c033 100644 --- a/frontend/src/api/generated/model/gatewayCommandsResponse.ts +++ b/frontend/src/api/generated/model/gatewayCommandsResponse.ts @@ -9,7 +9,7 @@ * Gateway command catalog and protocol metadata. */ export interface GatewayCommandsResponse { - protocol_version: number; - methods: string[]; events: string[]; + methods: string[]; + protocol_version: number; } diff --git a/frontend/src/api/generated/model/gatewayCreate.ts b/frontend/src/api/generated/model/gatewayCreate.ts index 0fff7e4a..ee4ea5d9 100644 --- a/frontend/src/api/generated/model/gatewayCreate.ts +++ b/frontend/src/api/generated/model/gatewayCreate.ts @@ -9,8 +9,10 @@ * Payload for creating a gateway configuration. */ export interface GatewayCreate { + allow_insecure_tls?: boolean; + disable_device_pairing?: boolean; name: string; + token?: string | null; url: string; workspace_root: string; - token?: string | null; } diff --git a/frontend/src/api/generated/model/gatewayLeadBroadcastBoardResult.ts b/frontend/src/api/generated/model/gatewayLeadBroadcastBoardResult.ts index dac6c6df..e490d3f8 100644 --- a/frontend/src/api/generated/model/gatewayLeadBroadcastBoardResult.ts +++ b/frontend/src/api/generated/model/gatewayLeadBroadcastBoardResult.ts @@ -11,12 +11,12 @@ export interface GatewayLeadBroadcastBoardResult { /** Target board id for this result. */ board_id: string; + /** Failure reason if this board failed. */ + error?: string | null; /** Resolved lead agent id for the target board. */ lead_agent_id?: string | null; /** Resolved lead agent display name. */ lead_agent_name?: string | null; /** Whether this board delivery succeeded. */ ok?: boolean; - /** Failure reason if this board failed. */ - error?: string | null; } diff --git a/frontend/src/api/generated/model/gatewayLeadBroadcastRequest.ts b/frontend/src/api/generated/model/gatewayLeadBroadcastRequest.ts index 1fb5fb28..e4d78dd9 100644 --- a/frontend/src/api/generated/model/gatewayLeadBroadcastRequest.ts +++ b/frontend/src/api/generated/model/gatewayLeadBroadcastRequest.ts @@ -10,19 +10,19 @@ import type { GatewayLeadBroadcastRequestKind } from "./gatewayLeadBroadcastRequ * Request payload for broadcasting a message to multiple board leads. */ export interface GatewayLeadBroadcastRequest { - /** Broadcast intent. `question` asks for responses; `handoff` requests transfer. */ - kind?: GatewayLeadBroadcastRequestKind; - /** Optional correlation token shared with downstream handlers. */ - correlation_id?: string | null; + /** Optional explicit list of board IDs; omit for lead-scoped defaults. */ + board_ids?: string[] | null; /** * Message content distributed to selected board leads. * @minLength 1 */ content: string; - /** Optional explicit list of board IDs; omit for lead-scoped defaults. */ - board_ids?: string[] | null; - /** Tags required by reply templates when each lead responds. */ - reply_tags?: string[]; + /** Optional correlation token shared with downstream handlers. */ + correlation_id?: string | null; + /** Broadcast intent. `question` asks for responses; `handoff` requests transfer. */ + kind?: GatewayLeadBroadcastRequestKind; /** Reply destination key for broadcast responses. */ reply_source?: string | null; + /** Tags required by reply templates when each lead responds. */ + reply_tags?: string[]; } diff --git a/frontend/src/api/generated/model/gatewayLeadBroadcastResponse.ts b/frontend/src/api/generated/model/gatewayLeadBroadcastResponse.ts index 42013c6e..36b905c8 100644 --- a/frontend/src/api/generated/model/gatewayLeadBroadcastResponse.ts +++ b/frontend/src/api/generated/model/gatewayLeadBroadcastResponse.ts @@ -10,11 +10,11 @@ import type { GatewayLeadBroadcastBoardResult } from "./gatewayLeadBroadcastBoar * Aggregate response for a lead broadcast operation. */ export interface GatewayLeadBroadcastResponse { - /** Whether broadcast execution succeeded. */ - ok?: boolean; - /** Number of boards successfully messaged. */ - sent?: number; /** Number of boards that failed messaging. */ failed?: number; + /** Whether broadcast execution succeeded. */ + ok?: boolean; results?: GatewayLeadBroadcastBoardResult[]; + /** Number of boards successfully messaged. */ + sent?: number; } diff --git a/frontend/src/api/generated/model/gatewayLeadMessageRequest.ts b/frontend/src/api/generated/model/gatewayLeadMessageRequest.ts index 3e64d707..0f8e1506 100644 --- a/frontend/src/api/generated/model/gatewayLeadMessageRequest.ts +++ b/frontend/src/api/generated/model/gatewayLeadMessageRequest.ts @@ -10,17 +10,17 @@ import type { GatewayLeadMessageRequestKind } from "./gatewayLeadMessageRequestK * Request payload for sending a message to a board lead agent. */ export interface GatewayLeadMessageRequest { - /** Routing mode for lead messages. */ - kind?: GatewayLeadMessageRequestKind; - /** Optional correlation token shared across upstream and downstream systems. */ - correlation_id?: string | null; /** * Human-readable body sent to lead agents. * @minLength 1 */ content: string; - /** Tags required by reply templates when the lead responds. */ - reply_tags?: string[]; + /** Optional correlation token shared across upstream and downstream systems. */ + correlation_id?: string | null; + /** Routing mode for lead messages. */ + kind?: GatewayLeadMessageRequestKind; /** Reply destination key for the orchestrator. */ reply_source?: string | null; + /** Tags required by reply templates when the lead responds. */ + reply_tags?: string[]; } diff --git a/frontend/src/api/generated/model/gatewayLeadMessageResponse.ts b/frontend/src/api/generated/model/gatewayLeadMessageResponse.ts index d890c49a..a4d35701 100644 --- a/frontend/src/api/generated/model/gatewayLeadMessageResponse.ts +++ b/frontend/src/api/generated/model/gatewayLeadMessageResponse.ts @@ -9,8 +9,6 @@ * Response payload for a lead-message dispatch attempt. */ export interface GatewayLeadMessageResponse { - /** Whether dispatch was accepted. */ - ok?: boolean; /** Board receiving the message. */ board_id: string; /** Resolved lead agent id when present. */ @@ -19,4 +17,6 @@ export interface GatewayLeadMessageResponse { lead_agent_name?: string | null; /** Whether a lead fallback actor was created during routing. */ lead_created?: boolean; + /** Whether dispatch was accepted. */ + ok?: boolean; } diff --git a/frontend/src/api/generated/model/gatewayMainAskUserRequest.ts b/frontend/src/api/generated/model/gatewayMainAskUserRequest.ts index 77c77d5a..10d58c38 100644 --- a/frontend/src/api/generated/model/gatewayMainAskUserRequest.ts +++ b/frontend/src/api/generated/model/gatewayMainAskUserRequest.ts @@ -9,17 +9,17 @@ * Request payload for asking the end user via a main gateway agent. */ export interface GatewayMainAskUserRequest { - /** Optional correlation token for tracing request/response flow. */ - correlation_id?: string | null; /** * Prompt that should be asked to the human. * @minLength 1 */ content: string; + /** Optional correlation token for tracing request/response flow. */ + correlation_id?: string | null; /** Optional preferred messaging channel. */ preferred_channel?: string | null; - /** Tags required for routing the user response. */ - reply_tags?: string[]; /** Reply destination key for user confirmation loops. */ reply_source?: string | null; + /** Tags required for routing the user response. */ + reply_tags?: string[]; } diff --git a/frontend/src/api/generated/model/gatewayMainAskUserResponse.ts b/frontend/src/api/generated/model/gatewayMainAskUserResponse.ts index 69a5c17b..599081b7 100644 --- a/frontend/src/api/generated/model/gatewayMainAskUserResponse.ts +++ b/frontend/src/api/generated/model/gatewayMainAskUserResponse.ts @@ -9,12 +9,12 @@ * Response payload for user-question dispatch via gateway main agent. */ export interface GatewayMainAskUserResponse { - /** Whether ask-user dispatch was accepted. */ - ok?: boolean; /** Board context used for the request. */ board_id: string; /** Resolved main agent id handling the ask. */ main_agent_id?: string | null; /** Resolved main agent display name. */ main_agent_name?: string | null; + /** Whether ask-user dispatch was accepted. */ + ok?: boolean; } diff --git a/frontend/src/api/generated/model/gatewayRead.ts b/frontend/src/api/generated/model/gatewayRead.ts index 03dcc40c..def49115 100644 --- a/frontend/src/api/generated/model/gatewayRead.ts +++ b/frontend/src/api/generated/model/gatewayRead.ts @@ -9,12 +9,14 @@ * Gateway payload returned from read endpoints. */ export interface GatewayRead { - name: string; - url: string; - workspace_root: string; + allow_insecure_tls?: boolean; + created_at: string; + disable_device_pairing?: boolean; id: string; + name: string; organization_id: string; token?: string | null; - created_at: string; updated_at: string; + url: string; + workspace_root: string; } diff --git a/frontend/src/api/generated/model/gatewaySessionsResponse.ts b/frontend/src/api/generated/model/gatewaySessionsResponse.ts index fba71fc7..8cf60e10 100644 --- a/frontend/src/api/generated/model/gatewaySessionsResponse.ts +++ b/frontend/src/api/generated/model/gatewaySessionsResponse.ts @@ -9,6 +9,6 @@ * Gateway sessions list response payload. */ export interface GatewaySessionsResponse { - sessions: unknown[]; main_session?: unknown | null; + sessions: unknown[]; } diff --git a/frontend/src/api/generated/model/gatewayTemplatesSyncResult.ts b/frontend/src/api/generated/model/gatewayTemplatesSyncResult.ts index 2eb14d87..f6e19d8c 100644 --- a/frontend/src/api/generated/model/gatewayTemplatesSyncResult.ts +++ b/frontend/src/api/generated/model/gatewayTemplatesSyncResult.ts @@ -10,11 +10,11 @@ import type { GatewayTemplatesSyncError } from "./gatewayTemplatesSyncError"; * Summary payload returned by gateway template sync endpoints. */ export interface GatewayTemplatesSyncResult { + agents_skipped: number; + agents_updated: number; + errors?: GatewayTemplatesSyncError[]; gateway_id: string; include_main: boolean; - reset_sessions: boolean; - agents_updated: number; - agents_skipped: number; main_updated: boolean; - errors?: GatewayTemplatesSyncError[]; + reset_sessions: boolean; } diff --git a/frontend/src/api/generated/model/gatewayUpdate.ts b/frontend/src/api/generated/model/gatewayUpdate.ts index e5f237ef..e284f972 100644 --- a/frontend/src/api/generated/model/gatewayUpdate.ts +++ b/frontend/src/api/generated/model/gatewayUpdate.ts @@ -9,8 +9,10 @@ * Payload for partial gateway updates. */ export interface GatewayUpdate { + allow_insecure_tls?: boolean | null; + disable_device_pairing?: boolean | null; name?: string | null; - url?: string | null; token?: string | null; + url?: string | null; workspace_root?: string | null; } diff --git a/frontend/src/api/generated/model/gatewaysStatusApiV1GatewaysStatusGetParams.ts b/frontend/src/api/generated/model/gatewaysStatusApiV1GatewaysStatusGetParams.ts index 1c4bc7ce..e26eefc5 100644 --- a/frontend/src/api/generated/model/gatewaysStatusApiV1GatewaysStatusGetParams.ts +++ b/frontend/src/api/generated/model/gatewaysStatusApiV1GatewaysStatusGetParams.ts @@ -9,4 +9,6 @@ export type GatewaysStatusApiV1GatewaysStatusGetParams = { board_id?: string | null; gateway_url?: string | null; gateway_token?: string | null; + gateway_disable_device_pairing?: boolean | null; + gateway_allow_insecure_tls?: boolean | null; }; diff --git a/frontend/src/api/generated/model/gatewaysStatusResponse.ts b/frontend/src/api/generated/model/gatewaysStatusResponse.ts index 0c591e17..cea5a7a7 100644 --- a/frontend/src/api/generated/model/gatewaysStatusResponse.ts +++ b/frontend/src/api/generated/model/gatewaysStatusResponse.ts @@ -10,10 +10,10 @@ */ export interface GatewaysStatusResponse { connected: boolean; + error?: string | null; gateway_url: string; - sessions_count?: number | null; - sessions?: unknown[] | null; main_session?: unknown | null; main_session_error?: string | null; - error?: string | null; + sessions?: unknown[] | null; + sessions_count?: number | null; } diff --git a/frontend/src/api/generated/model/index.ts b/frontend/src/api/generated/model/index.ts index b2357468..437fc1e0 100644 --- a/frontend/src/api/generated/model/index.ts +++ b/frontend/src/api/generated/model/index.ts @@ -6,10 +6,12 @@ */ export * from "./activityEventRead"; +export * from "./activityEventReadRouteParams"; export * from "./activityTaskCommentFeedItemRead"; export * from "./agentCreate"; export * from "./agentCreateHeartbeatConfig"; export * from "./agentCreateIdentityProfile"; +export * from "./agentHealthStatusResponse"; export * from "./agentHeartbeat"; export * from "./agentHeartbeatCreate"; export * from "./agentNudge"; @@ -76,6 +78,8 @@ export * from "./dashboardMetrics"; export * from "./dashboardMetricsApiV1MetricsDashboardGetParams"; export * from "./dashboardMetricsApiV1MetricsDashboardGetRangeKey"; export * from "./dashboardMetricsRange"; +export * from "./dashboardPendingApproval"; +export * from "./dashboardPendingApprovals"; export * from "./dashboardRangeSeries"; export * from "./dashboardRangeSeriesBucket"; export * from "./dashboardRangeSeriesRange"; diff --git a/frontend/src/api/generated/model/lLMErrorResponse.ts b/frontend/src/api/generated/model/lLMErrorResponse.ts index f8abaaf9..683bd0b0 100644 --- a/frontend/src/api/generated/model/lLMErrorResponse.ts +++ b/frontend/src/api/generated/model/lLMErrorResponse.ts @@ -10,12 +10,12 @@ import type { LLMErrorResponseDetail } from "./lLMErrorResponseDetail"; * Standardized LLM-facing error payload used by API contracts. */ export interface LLMErrorResponse { + /** Optional machine-readable error code. */ + code?: string | null; /** Error payload. Agents should rely on `code` when present and default to `message` for fallback display. */ detail: LLMErrorResponseDetail; /** Request correlation identifier injected by middleware. */ request_id?: string | null; - /** Optional machine-readable error code. */ - code?: string | null; /** Whether a client should retry the call after remediating transient conditions. */ retryable?: boolean | null; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityEventRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityEventRead.ts index 2cb816c0..78b90866 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityEventRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityEventRead.ts @@ -8,10 +8,10 @@ import type { ActivityEventRead } from "./activityEventRead"; export interface LimitOffsetPageTypeVarCustomizedActivityEventRead { items: ActivityEventRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead.ts index bf550703..f39d782b 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead.ts @@ -8,10 +8,10 @@ import type { ActivityTaskCommentFeedItemRead } from "./activityTaskCommentFeedI export interface LimitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead { items: ActivityTaskCommentFeedItemRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedAgentRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedAgentRead.ts index 64dba628..7c254542 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedAgentRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedAgentRead.ts @@ -8,10 +8,10 @@ import type { AgentRead } from "./agentRead"; export interface LimitOffsetPageTypeVarCustomizedAgentRead { items: AgentRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedApprovalRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedApprovalRead.ts index 7b912d65..0d66176c 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedApprovalRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedApprovalRead.ts @@ -8,10 +8,10 @@ import type { ApprovalRead } from "./approvalRead"; export interface LimitOffsetPageTypeVarCustomizedApprovalRead { items: ApprovalRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardGroupMemoryRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardGroupMemoryRead.ts index 94e07678..5588cd95 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardGroupMemoryRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardGroupMemoryRead.ts @@ -8,10 +8,10 @@ import type { BoardGroupMemoryRead } from "./boardGroupMemoryRead"; export interface LimitOffsetPageTypeVarCustomizedBoardGroupMemoryRead { items: BoardGroupMemoryRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardGroupRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardGroupRead.ts index 95689ab5..ea0633d8 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardGroupRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardGroupRead.ts @@ -8,10 +8,10 @@ import type { BoardGroupRead } from "./boardGroupRead"; export interface LimitOffsetPageTypeVarCustomizedBoardGroupRead { items: BoardGroupRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardMemoryRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardMemoryRead.ts index 5af7a346..fdf4acda 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardMemoryRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardMemoryRead.ts @@ -8,10 +8,10 @@ import type { BoardMemoryRead } from "./boardMemoryRead"; export interface LimitOffsetPageTypeVarCustomizedBoardMemoryRead { items: BoardMemoryRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardRead.ts index dffce1a2..d182bde6 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardRead.ts @@ -8,10 +8,10 @@ import type { BoardRead } from "./boardRead"; export interface LimitOffsetPageTypeVarCustomizedBoardRead { items: BoardRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead.ts index 67191dad..e9fe9f71 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead.ts @@ -8,10 +8,10 @@ import type { BoardWebhookPayloadRead } from "./boardWebhookPayloadRead"; export interface LimitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead { items: BoardWebhookPayloadRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookRead.ts index 948770b8..6bde11ec 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookRead.ts @@ -8,10 +8,10 @@ import type { BoardWebhookRead } from "./boardWebhookRead"; export interface LimitOffsetPageTypeVarCustomizedBoardWebhookRead { items: BoardWebhookRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedGatewayRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedGatewayRead.ts index 26228745..3bae8a3d 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedGatewayRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedGatewayRead.ts @@ -8,10 +8,10 @@ import type { GatewayRead } from "./gatewayRead"; export interface LimitOffsetPageTypeVarCustomizedGatewayRead { items: GatewayRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedOrganizationInviteRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedOrganizationInviteRead.ts index 0640e516..cb770aa4 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedOrganizationInviteRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedOrganizationInviteRead.ts @@ -8,10 +8,10 @@ import type { OrganizationInviteRead } from "./organizationInviteRead"; export interface LimitOffsetPageTypeVarCustomizedOrganizationInviteRead { items: OrganizationInviteRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedOrganizationMemberRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedOrganizationMemberRead.ts index 733e3cb6..f616673f 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedOrganizationMemberRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedOrganizationMemberRead.ts @@ -8,10 +8,10 @@ import type { OrganizationMemberRead } from "./organizationMemberRead"; export interface LimitOffsetPageTypeVarCustomizedOrganizationMemberRead { items: OrganizationMemberRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTagRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTagRead.ts index d1e4e40f..521dff88 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTagRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTagRead.ts @@ -8,10 +8,10 @@ import type { TagRead } from "./tagRead"; export interface LimitOffsetPageTypeVarCustomizedTagRead { items: TagRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskCommentRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskCommentRead.ts index 9fcfca2c..dcb134f1 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskCommentRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskCommentRead.ts @@ -8,10 +8,10 @@ import type { TaskCommentRead } from "./taskCommentRead"; export interface LimitOffsetPageTypeVarCustomizedTaskCommentRead { items: TaskCommentRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskRead.ts index a22715a0..789f482b 100644 --- a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskRead.ts +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskRead.ts @@ -8,10 +8,10 @@ import type { TaskRead } from "./taskRead"; export interface LimitOffsetPageTypeVarCustomizedTaskRead { items: TaskRead[]; - /** @minimum 0 */ - total: number; /** @minimum 1 */ limit: number; /** @minimum 0 */ offset: number; + /** @minimum 0 */ + total: number; } diff --git a/frontend/src/api/generated/model/marketplaceSkillActionResponse.ts b/frontend/src/api/generated/model/marketplaceSkillActionResponse.ts index 5b081626..218a0789 100644 --- a/frontend/src/api/generated/model/marketplaceSkillActionResponse.ts +++ b/frontend/src/api/generated/model/marketplaceSkillActionResponse.ts @@ -9,8 +9,8 @@ * Install/uninstall action response payload. */ export interface MarketplaceSkillActionResponse { - ok?: boolean; - skill_id: string; gateway_id: string; installed: boolean; + ok?: boolean; + skill_id: string; } diff --git a/frontend/src/api/generated/model/marketplaceSkillCardRead.ts b/frontend/src/api/generated/model/marketplaceSkillCardRead.ts index 15051071..9d674fe7 100644 --- a/frontend/src/api/generated/model/marketplaceSkillCardRead.ts +++ b/frontend/src/api/generated/model/marketplaceSkillCardRead.ts @@ -10,17 +10,17 @@ import type { MarketplaceSkillCardReadMetadata } from "./marketplaceSkillCardRea * Marketplace card payload with gateway-specific install state. */ export interface MarketplaceSkillCardRead { - id: string; - organization_id: string; - name: string; - description?: string | null; category?: string | null; + created_at: string; + description?: string | null; + id: string; + installed: boolean; + installed_at?: string | null; + metadata?: MarketplaceSkillCardReadMetadata; + name: string; + organization_id: string; risk?: string | null; source?: string | null; source_url: string; - metadata?: MarketplaceSkillCardReadMetadata; - created_at: string; updated_at: string; - installed: boolean; - installed_at?: string | null; } diff --git a/frontend/src/api/generated/model/marketplaceSkillCreate.ts b/frontend/src/api/generated/model/marketplaceSkillCreate.ts index 7c3c0380..8b9f6763 100644 --- a/frontend/src/api/generated/model/marketplaceSkillCreate.ts +++ b/frontend/src/api/generated/model/marketplaceSkillCreate.ts @@ -9,8 +9,8 @@ * Payload used to register a skill URL in the organization marketplace. */ export interface MarketplaceSkillCreate { + description?: string | null; + name?: string | null; /** @minLength 1 */ source_url: string; - name?: string | null; - description?: string | null; } diff --git a/frontend/src/api/generated/model/marketplaceSkillRead.ts b/frontend/src/api/generated/model/marketplaceSkillRead.ts index 096f53b4..60e860bd 100644 --- a/frontend/src/api/generated/model/marketplaceSkillRead.ts +++ b/frontend/src/api/generated/model/marketplaceSkillRead.ts @@ -10,15 +10,15 @@ import type { MarketplaceSkillReadMetadata } from "./marketplaceSkillReadMetadat * Serialized marketplace skill catalog record. */ export interface MarketplaceSkillRead { - id: string; - organization_id: string; - name: string; - description?: string | null; category?: string | null; + created_at: string; + description?: string | null; + id: string; + metadata?: MarketplaceSkillReadMetadata; + name: string; + organization_id: string; risk?: string | null; source?: string | null; source_url: string; - metadata?: MarketplaceSkillReadMetadata; - created_at: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/organizationBoardAccessRead.ts b/frontend/src/api/generated/model/organizationBoardAccessRead.ts index 927cc9b0..19b75f29 100644 --- a/frontend/src/api/generated/model/organizationBoardAccessRead.ts +++ b/frontend/src/api/generated/model/organizationBoardAccessRead.ts @@ -9,10 +9,10 @@ * Board access payload returned from read endpoints. */ export interface OrganizationBoardAccessRead { - id: string; board_id: string; can_read: boolean; can_write: boolean; created_at: string; + id: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/organizationInviteCreate.ts b/frontend/src/api/generated/model/organizationInviteCreate.ts index 6cd653f0..ef8e9074 100644 --- a/frontend/src/api/generated/model/organizationInviteCreate.ts +++ b/frontend/src/api/generated/model/organizationInviteCreate.ts @@ -10,9 +10,9 @@ import type { OrganizationBoardAccessSpec } from "./organizationBoardAccessSpec" * Payload for creating an organization invite. */ export interface OrganizationInviteCreate { - invited_email: string; - role?: string; all_boards_read?: boolean; all_boards_write?: boolean; board_access?: OrganizationBoardAccessSpec[]; + invited_email: string; + role?: string; } diff --git a/frontend/src/api/generated/model/organizationInviteRead.ts b/frontend/src/api/generated/model/organizationInviteRead.ts index 643a2691..5cba137c 100644 --- a/frontend/src/api/generated/model/organizationInviteRead.ts +++ b/frontend/src/api/generated/model/organizationInviteRead.ts @@ -9,16 +9,16 @@ * Organization invite payload returned from read endpoints. */ export interface OrganizationInviteRead { - id: string; - organization_id: string; - invited_email: string; - role: string; + accepted_at?: string | null; + accepted_by_user_id?: string | null; all_boards_read: boolean; all_boards_write: boolean; - token: string; - created_by_user_id?: string | null; - accepted_by_user_id?: string | null; - accepted_at?: string | null; created_at: string; + created_by_user_id?: string | null; + id: string; + invited_email: string; + organization_id: string; + role: string; + token: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/organizationListItem.ts b/frontend/src/api/generated/model/organizationListItem.ts index 2283299c..4c3cc08f 100644 --- a/frontend/src/api/generated/model/organizationListItem.ts +++ b/frontend/src/api/generated/model/organizationListItem.ts @@ -10,7 +10,7 @@ */ export interface OrganizationListItem { id: string; + is_active: boolean; name: string; role: string; - is_active: boolean; } diff --git a/frontend/src/api/generated/model/organizationMemberRead.ts b/frontend/src/api/generated/model/organizationMemberRead.ts index 9516d4ed..5e91c9a9 100644 --- a/frontend/src/api/generated/model/organizationMemberRead.ts +++ b/frontend/src/api/generated/model/organizationMemberRead.ts @@ -11,14 +11,14 @@ import type { OrganizationUserRead } from "./organizationUserRead"; * Organization member payload including board-level access overrides. */ export interface OrganizationMemberRead { - id: string; - organization_id: string; - user_id: string; - role: string; all_boards_read: boolean; all_boards_write: boolean; + board_access?: OrganizationBoardAccessRead[]; created_at: string; + id: string; + organization_id: string; + role: string; updated_at: string; user?: OrganizationUserRead | null; - board_access?: OrganizationBoardAccessRead[]; + user_id: string; } diff --git a/frontend/src/api/generated/model/organizationRead.ts b/frontend/src/api/generated/model/organizationRead.ts index 7c161e21..8e45126d 100644 --- a/frontend/src/api/generated/model/organizationRead.ts +++ b/frontend/src/api/generated/model/organizationRead.ts @@ -9,8 +9,8 @@ * Organization payload returned by read endpoints. */ export interface OrganizationRead { + created_at: string; id: string; name: string; - created_at: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/organizationUserRead.ts b/frontend/src/api/generated/model/organizationUserRead.ts index 592145dc..490973d0 100644 --- a/frontend/src/api/generated/model/organizationUserRead.ts +++ b/frontend/src/api/generated/model/organizationUserRead.ts @@ -9,8 +9,8 @@ * Embedded user fields included in organization member payloads. */ export interface OrganizationUserRead { - id: string; email?: string | null; + id: string; name?: string | null; preferred_name?: string | null; } diff --git a/frontend/src/api/generated/model/skillPackCreate.ts b/frontend/src/api/generated/model/skillPackCreate.ts index 0e63b676..a0fa15cd 100644 --- a/frontend/src/api/generated/model/skillPackCreate.ts +++ b/frontend/src/api/generated/model/skillPackCreate.ts @@ -10,10 +10,10 @@ import type { SkillPackCreateMetadata } from "./skillPackCreateMetadata"; * Payload used to register a pack URL in the organization. */ export interface SkillPackCreate { + branch?: string; + description?: string | null; + metadata?: SkillPackCreateMetadata; + name?: string | null; /** @minLength 1 */ source_url: string; - name?: string | null; - description?: string | null; - branch?: string; - metadata?: SkillPackCreateMetadata; } diff --git a/frontend/src/api/generated/model/skillPackRead.ts b/frontend/src/api/generated/model/skillPackRead.ts index 2c0b6888..ece72578 100644 --- a/frontend/src/api/generated/model/skillPackRead.ts +++ b/frontend/src/api/generated/model/skillPackRead.ts @@ -10,14 +10,14 @@ import type { SkillPackReadMetadata } from "./skillPackReadMetadata"; * Serialized skill pack record. */ export interface SkillPackRead { - id: string; - organization_id: string; - name: string; - description?: string | null; - source_url: string; branch: string; - metadata?: SkillPackReadMetadata; - skill_count?: number; created_at: string; + description?: string | null; + id: string; + metadata?: SkillPackReadMetadata; + name: string; + organization_id: string; + skill_count?: number; + source_url: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/skillPackSyncResponse.ts b/frontend/src/api/generated/model/skillPackSyncResponse.ts index 6ac3db7b..fdcbbbcb 100644 --- a/frontend/src/api/generated/model/skillPackSyncResponse.ts +++ b/frontend/src/api/generated/model/skillPackSyncResponse.ts @@ -9,10 +9,10 @@ * Pack sync summary payload. */ export interface SkillPackSyncResponse { + created: number; ok?: boolean; pack_id: string; synced: number; - created: number; updated: number; warnings?: string[]; } diff --git a/frontend/src/api/generated/model/soulUpdateRequest.ts b/frontend/src/api/generated/model/soulUpdateRequest.ts index 7d7865a8..4e0e840f 100644 --- a/frontend/src/api/generated/model/soulUpdateRequest.ts +++ b/frontend/src/api/generated/model/soulUpdateRequest.ts @@ -10,6 +10,6 @@ */ export interface SoulUpdateRequest { content: string; - source_url?: string | null; reason?: string | null; + source_url?: string | null; } diff --git a/frontend/src/api/generated/model/soulsDirectoryMarkdownResponse.ts b/frontend/src/api/generated/model/soulsDirectoryMarkdownResponse.ts index e10e4545..a7af0d70 100644 --- a/frontend/src/api/generated/model/soulsDirectoryMarkdownResponse.ts +++ b/frontend/src/api/generated/model/soulsDirectoryMarkdownResponse.ts @@ -9,7 +9,7 @@ * Response payload containing rendered markdown for a soul. */ export interface SoulsDirectoryMarkdownResponse { + content: string; handle: string; slug: string; - content: string; } diff --git a/frontend/src/api/generated/model/soulsDirectorySoulRef.ts b/frontend/src/api/generated/model/soulsDirectorySoulRef.ts index 01bdf80a..6c1bb0a4 100644 --- a/frontend/src/api/generated/model/soulsDirectorySoulRef.ts +++ b/frontend/src/api/generated/model/soulsDirectorySoulRef.ts @@ -10,7 +10,7 @@ */ export interface SoulsDirectorySoulRef { handle: string; - slug: string; page_url: string; raw_md_url: string; + slug: string; } diff --git a/frontend/src/api/generated/model/tagCreate.ts b/frontend/src/api/generated/model/tagCreate.ts index 5c5575c6..9c754724 100644 --- a/frontend/src/api/generated/model/tagCreate.ts +++ b/frontend/src/api/generated/model/tagCreate.ts @@ -9,9 +9,9 @@ * Payload for creating a tag. */ export interface TagCreate { + color?: string; + description?: string | null; /** @minLength 1 */ name: string; slug?: string | null; - color?: string; - description?: string | null; } diff --git a/frontend/src/api/generated/model/tagRead.ts b/frontend/src/api/generated/model/tagRead.ts index 49590325..af1d3bd3 100644 --- a/frontend/src/api/generated/model/tagRead.ts +++ b/frontend/src/api/generated/model/tagRead.ts @@ -9,13 +9,13 @@ * Tag payload returned from API endpoints. */ export interface TagRead { - name: string; - slug: string; color?: string; + created_at: string; description?: string | null; id: string; + name: string; organization_id: string; + slug: string; task_count?: number; - created_at: string; updated_at: string; } diff --git a/frontend/src/api/generated/model/tagRef.ts b/frontend/src/api/generated/model/tagRef.ts index 2493c4fd..9b7d745b 100644 --- a/frontend/src/api/generated/model/tagRef.ts +++ b/frontend/src/api/generated/model/tagRef.ts @@ -9,8 +9,8 @@ * Compact tag representation embedded in task payloads. */ export interface TagRef { + color: string; id: string; name: string; slug: string; - color: string; } diff --git a/frontend/src/api/generated/model/tagUpdate.ts b/frontend/src/api/generated/model/tagUpdate.ts index 7877bcb8..4a18b9df 100644 --- a/frontend/src/api/generated/model/tagUpdate.ts +++ b/frontend/src/api/generated/model/tagUpdate.ts @@ -9,8 +9,8 @@ * Payload for partial tag updates. */ export interface TagUpdate { - name?: string | null; - slug?: string | null; color?: string | null; description?: string | null; + name?: string | null; + slug?: string | null; } diff --git a/frontend/src/api/generated/model/taskCardRead.ts b/frontend/src/api/generated/model/taskCardRead.ts index 67a51b98..02025221 100644 --- a/frontend/src/api/generated/model/taskCardRead.ts +++ b/frontend/src/api/generated/model/taskCardRead.ts @@ -12,25 +12,25 @@ import type { TaskCardReadStatus } from "./taskCardReadStatus"; * Task read model enriched with assignee and approval counters. */ export interface TaskCardRead { - title: string; - description?: string | null; - status?: TaskCardReadStatus; - priority?: string; - due_at?: string | null; - assigned_agent_id?: string | null; - depends_on_task_ids?: string[]; - tag_ids?: string[]; - id: string; - board_id: string | null; - created_by_user_id: string | null; - in_progress_at: string | null; - created_at: string; - updated_at: string; - blocked_by_task_ids?: string[]; - is_blocked?: boolean; - tags?: TagRef[]; - custom_field_values?: TaskCardReadCustomFieldValues; - assignee?: string | null; approvals_count?: number; approvals_pending_count?: number; + assigned_agent_id?: string | null; + assignee?: string | null; + blocked_by_task_ids?: string[]; + board_id: string | null; + created_at: string; + created_by_user_id: string | null; + custom_field_values?: TaskCardReadCustomFieldValues; + depends_on_task_ids?: string[]; + description?: string | null; + due_at?: string | null; + id: string; + in_progress_at: string | null; + is_blocked?: boolean; + priority?: string; + status?: TaskCardReadStatus; + tag_ids?: string[]; + tags?: TagRef[]; + title: string; + updated_at: string; } diff --git a/frontend/src/api/generated/model/taskCommentRead.ts b/frontend/src/api/generated/model/taskCommentRead.ts index 689f082c..faec4310 100644 --- a/frontend/src/api/generated/model/taskCommentRead.ts +++ b/frontend/src/api/generated/model/taskCommentRead.ts @@ -9,9 +9,9 @@ * Task comment payload returned from read endpoints. */ export interface TaskCommentRead { + agent_id: string | null; + created_at: string; id: string; message: string | null; - agent_id: string | null; task_id: string | null; - created_at: string; } diff --git a/frontend/src/api/generated/model/taskCreate.ts b/frontend/src/api/generated/model/taskCreate.ts index a3e2f7ef..a8572c23 100644 --- a/frontend/src/api/generated/model/taskCreate.ts +++ b/frontend/src/api/generated/model/taskCreate.ts @@ -11,14 +11,14 @@ import type { TaskCreateStatus } from "./taskCreateStatus"; * Payload for creating a task. */ export interface TaskCreate { - title: string; - description?: string | null; - status?: TaskCreateStatus; - priority?: string; - due_at?: string | null; assigned_agent_id?: string | null; - depends_on_task_ids?: string[]; - tag_ids?: string[]; created_by_user_id?: string | null; custom_field_values?: TaskCreateCustomFieldValues; + depends_on_task_ids?: string[]; + description?: string | null; + due_at?: string | null; + priority?: string; + status?: TaskCreateStatus; + tag_ids?: string[]; + title: string; } diff --git a/frontend/src/api/generated/model/taskCustomFieldDefinitionCreate.ts b/frontend/src/api/generated/model/taskCustomFieldDefinitionCreate.ts index 4a88dc97..b3486163 100644 --- a/frontend/src/api/generated/model/taskCustomFieldDefinitionCreate.ts +++ b/frontend/src/api/generated/model/taskCustomFieldDefinitionCreate.ts @@ -11,15 +11,15 @@ import type { TaskCustomFieldDefinitionCreateUiVisibility } from "./taskCustomFi * Payload for creating a task custom field definition. */ export interface TaskCustomFieldDefinitionCreate { - /** @minLength 1 */ - field_key: string; - label?: string | null; - field_type?: TaskCustomFieldDefinitionCreateFieldType; - ui_visibility?: TaskCustomFieldDefinitionCreateUiVisibility; - validation_regex?: string | null; - description?: string | null; - required?: boolean; - default_value?: unknown | null; /** @minItems 1 */ board_ids: string[]; + default_value?: unknown | null; + description?: string | null; + /** @minLength 1 */ + field_key: string; + field_type?: TaskCustomFieldDefinitionCreateFieldType; + label?: string | null; + required?: boolean; + ui_visibility?: TaskCustomFieldDefinitionCreateUiVisibility; + validation_regex?: string | null; } diff --git a/frontend/src/api/generated/model/taskCustomFieldDefinitionRead.ts b/frontend/src/api/generated/model/taskCustomFieldDefinitionRead.ts index 2063ba0a..52c80665 100644 --- a/frontend/src/api/generated/model/taskCustomFieldDefinitionRead.ts +++ b/frontend/src/api/generated/model/taskCustomFieldDefinitionRead.ts @@ -11,17 +11,17 @@ import type { TaskCustomFieldDefinitionReadUiVisibility } from "./taskCustomFiel * Payload returned for custom field definitions. */ export interface TaskCustomFieldDefinitionRead { - field_key: string; - label: string; - field_type: TaskCustomFieldDefinitionReadFieldType; - ui_visibility: TaskCustomFieldDefinitionReadUiVisibility; - validation_regex?: string | null; - description?: string | null; - required?: boolean; - default_value?: unknown | null; - id: string; - organization_id: string; board_ids?: string[]; created_at: string; + default_value?: unknown | null; + description?: string | null; + field_key: string; + field_type: TaskCustomFieldDefinitionReadFieldType; + id: string; + label: string; + organization_id: string; + required?: boolean; + ui_visibility: TaskCustomFieldDefinitionReadUiVisibility; updated_at: string; + validation_regex?: string | null; } diff --git a/frontend/src/api/generated/model/taskCustomFieldDefinitionUpdate.ts b/frontend/src/api/generated/model/taskCustomFieldDefinitionUpdate.ts index 68e28175..7beeb437 100644 --- a/frontend/src/api/generated/model/taskCustomFieldDefinitionUpdate.ts +++ b/frontend/src/api/generated/model/taskCustomFieldDefinitionUpdate.ts @@ -9,7 +9,9 @@ * Payload for editing an existing task custom field definition. */ export interface TaskCustomFieldDefinitionUpdate { - label?: string | null; + board_ids?: string[] | null; + default_value?: unknown | null; + description?: string | null; field_type?: | "text" | "text_long" @@ -21,10 +23,8 @@ export interface TaskCustomFieldDefinitionUpdate { | "url" | "json" | null; + label?: string | null; + required?: boolean | null; ui_visibility?: "always" | "if_set" | "hidden" | null; validation_regex?: string | null; - description?: string | null; - required?: boolean | null; - default_value?: unknown | null; - board_ids?: string[] | null; } diff --git a/frontend/src/api/generated/model/taskRead.ts b/frontend/src/api/generated/model/taskRead.ts index 25c21d8a..5d874413 100644 --- a/frontend/src/api/generated/model/taskRead.ts +++ b/frontend/src/api/generated/model/taskRead.ts @@ -12,22 +12,22 @@ import type { TaskReadStatus } from "./taskReadStatus"; * Task payload returned from read endpoints. */ export interface TaskRead { - title: string; - description?: string | null; - status?: TaskReadStatus; - priority?: string; - due_at?: string | null; assigned_agent_id?: string | null; - depends_on_task_ids?: string[]; - tag_ids?: string[]; - id: string; - board_id: string | null; - created_by_user_id: string | null; - in_progress_at: string | null; - created_at: string; - updated_at: string; blocked_by_task_ids?: string[]; - is_blocked?: boolean; - tags?: TagRef[]; + board_id: string | null; + created_at: string; + created_by_user_id: string | null; custom_field_values?: TaskReadCustomFieldValues; + depends_on_task_ids?: string[]; + description?: string | null; + due_at?: string | null; + id: string; + in_progress_at: string | null; + is_blocked?: boolean; + priority?: string; + status?: TaskReadStatus; + tag_ids?: string[]; + tags?: TagRef[]; + title: string; + updated_at: string; } diff --git a/frontend/src/api/generated/model/taskUpdate.ts b/frontend/src/api/generated/model/taskUpdate.ts index 7476476f..ddaa9936 100644 --- a/frontend/src/api/generated/model/taskUpdate.ts +++ b/frontend/src/api/generated/model/taskUpdate.ts @@ -10,14 +10,14 @@ import type { TaskUpdateCustomFieldValues } from "./taskUpdateCustomFieldValues" * Payload for partial task updates. */ export interface TaskUpdate { - title?: string | null; - description?: string | null; - status?: "inbox" | "in_progress" | "review" | "done" | null; - priority?: string | null; - due_at?: string | null; assigned_agent_id?: string | null; - depends_on_task_ids?: string[] | null; - tag_ids?: string[] | null; - custom_field_values?: TaskUpdateCustomFieldValues; comment?: string | null; + custom_field_values?: TaskUpdateCustomFieldValues; + depends_on_task_ids?: string[] | null; + description?: string | null; + due_at?: string | null; + priority?: string | null; + status?: "inbox" | "in_progress" | "review" | "done" | null; + tag_ids?: string[] | null; + title?: string | null; } diff --git a/frontend/src/api/generated/model/userRead.ts b/frontend/src/api/generated/model/userRead.ts index 6082df94..1a75f6a3 100644 --- a/frontend/src/api/generated/model/userRead.ts +++ b/frontend/src/api/generated/model/userRead.ts @@ -11,22 +11,22 @@ export interface UserRead { /** External auth provider user identifier (Clerk). */ clerk_user_id: string; + /** Additional context used by the system for personalization. */ + context?: string | null; /** Primary email address for the user. */ email?: string | null; + /** Internal user UUID. */ + id: string; + /** Whether this user has tenant-wide super-admin privileges. */ + is_super_admin: boolean; /** Full display name. */ name?: string | null; + /** Internal notes for operators. */ + notes?: string | null; /** Preferred short name used in UI. */ preferred_name?: string | null; /** Preferred pronouns. */ pronouns?: string | null; /** IANA timezone identifier. */ timezone?: string | null; - /** Internal notes for operators. */ - notes?: string | null; - /** Additional context used by the system for personalization. */ - context?: string | null; - /** Internal user UUID. */ - id: string; - /** Whether this user has tenant-wide super-admin privileges. */ - is_super_admin: boolean; } diff --git a/frontend/src/api/generated/model/userUpdate.ts b/frontend/src/api/generated/model/userUpdate.ts index 7e196385..cc9ab997 100644 --- a/frontend/src/api/generated/model/userUpdate.ts +++ b/frontend/src/api/generated/model/userUpdate.ts @@ -9,10 +9,10 @@ * Payload for partial user profile updates. */ export interface UserUpdate { + context?: string | null; name?: string | null; + notes?: string | null; preferred_name?: string | null; pronouns?: string | null; timezone?: string | null; - notes?: string | null; - context?: string | null; } diff --git a/frontend/src/api/generated/model/validationError.ts b/frontend/src/api/generated/model/validationError.ts index 1308b621..b40de80c 100644 --- a/frontend/src/api/generated/model/validationError.ts +++ b/frontend/src/api/generated/model/validationError.ts @@ -7,9 +7,9 @@ import type { ValidationErrorCtx } from "./validationErrorCtx"; export interface ValidationError { + ctx?: ValidationErrorCtx; + input?: unknown; loc: (string | number)[]; msg: string; type: string; - input?: unknown; - ctx?: ValidationErrorCtx; } diff --git a/frontend/src/api/generated/organizations/organizations.ts b/frontend/src/api/generated/organizations/organizations.ts index f330f1c4..cb756331 100644 --- a/frontend/src/api/generated/organizations/organizations.ts +++ b/frontend/src/api/generated/organizations/organizations.ts @@ -165,276 +165,191 @@ export const useCreateOrganizationApiV1OrganizationsPost = < ); }; /** - * List organizations where the current user is a member. - * @summary List My Organizations + * Accept an invite and return resulting membership. + * @summary Accept Org Invite */ -export type listMyOrganizationsApiV1OrganizationsMeListGetResponse200 = { - data: OrganizationListItem[]; +export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse200 = { + data: OrganizationMemberRead; status: 200; }; -export type listMyOrganizationsApiV1OrganizationsMeListGetResponseSuccess = - listMyOrganizationsApiV1OrganizationsMeListGetResponse200 & { - headers: Headers; - }; -export type listMyOrganizationsApiV1OrganizationsMeListGetResponse = - listMyOrganizationsApiV1OrganizationsMeListGetResponseSuccess; - -export const getListMyOrganizationsApiV1OrganizationsMeListGetUrl = () => { - return `/api/v1/organizations/me/list`; -}; - -export const listMyOrganizationsApiV1OrganizationsMeListGet = async ( - options?: RequestInit, -): Promise => { - return customFetch( - getListMyOrganizationsApiV1OrganizationsMeListGetUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getListMyOrganizationsApiV1OrganizationsMeListGetQueryKey = () => { - return [`/api/v1/organizations/me/list`] as const; -}; - -export const getListMyOrganizationsApiV1OrganizationsMeListGetQueryOptions = < - TData = Awaited< - ReturnType - >, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getListMyOrganizationsApiV1OrganizationsMeListGetQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - listMyOrganizationsApiV1OrganizationsMeListGet({ - signal, - ...requestOptions, - }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type ListMyOrganizationsApiV1OrganizationsMeListGetQueryResult = - NonNullable< - Awaited> - >; -export type ListMyOrganizationsApiV1OrganizationsMeListGetQueryError = unknown; - -export function useListMyOrganizationsApiV1OrganizationsMeListGet< - TData = Awaited< - ReturnType - >, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useListMyOrganizationsApiV1OrganizationsMeListGet< - TData = Awaited< - ReturnType - >, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useListMyOrganizationsApiV1OrganizationsMeListGet< - TData = Awaited< - ReturnType - >, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List My Organizations - */ - -export function useListMyOrganizationsApiV1OrganizationsMeListGet< - TData = Awaited< - ReturnType - >, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getListMyOrganizationsApiV1OrganizationsMeListGetQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - -/** - * Set the caller's active organization. - * @summary Set Active Org - */ -export type setActiveOrgApiV1OrganizationsMeActivePatchResponse200 = { - data: OrganizationRead; - status: 200; -}; - -export type setActiveOrgApiV1OrganizationsMeActivePatchResponse422 = { +export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse422 = { data: HTTPValidationError; status: 422; }; -export type setActiveOrgApiV1OrganizationsMeActivePatchResponseSuccess = - setActiveOrgApiV1OrganizationsMeActivePatchResponse200 & { +export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponseSuccess = + acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse200 & { headers: Headers; }; -export type setActiveOrgApiV1OrganizationsMeActivePatchResponseError = - setActiveOrgApiV1OrganizationsMeActivePatchResponse422 & { +export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponseError = + acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse422 & { headers: Headers; }; -export type setActiveOrgApiV1OrganizationsMeActivePatchResponse = - | setActiveOrgApiV1OrganizationsMeActivePatchResponseSuccess - | setActiveOrgApiV1OrganizationsMeActivePatchResponseError; +export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse = + | acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponseSuccess + | acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponseError; -export const getSetActiveOrgApiV1OrganizationsMeActivePatchUrl = () => { - return `/api/v1/organizations/me/active`; +export const getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostUrl = () => { + return `/api/v1/organizations/invites/accept`; }; -export const setActiveOrgApiV1OrganizationsMeActivePatch = async ( - organizationActiveUpdate: OrganizationActiveUpdate, +export const acceptOrgInviteApiV1OrganizationsInvitesAcceptPost = async ( + organizationInviteAccept: OrganizationInviteAccept, options?: RequestInit, -): Promise => { - return customFetch( - getSetActiveOrgApiV1OrganizationsMeActivePatchUrl(), +): Promise => { + return customFetch( + getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostUrl(), { ...options, - method: "PATCH", + method: "POST", headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(organizationActiveUpdate), + body: JSON.stringify(organizationInviteAccept), }, ); }; -export const getSetActiveOrgApiV1OrganizationsMeActivePatchMutationOptions = < +export const getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { data: OrganizationInviteAccept }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { data: OrganizationInviteAccept }, + TContext + > => { + const mutationKey = ["acceptOrgInviteApiV1OrganizationsInvitesAcceptPost"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType + >, + { data: OrganizationInviteAccept } + > = (props) => { + const { data } = props ?? {}; + + return acceptOrgInviteApiV1OrganizationsInvitesAcceptPost( + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type AcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type AcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationBody = + OrganizationInviteAccept; +export type AcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationError = + HTTPValidationError; + +/** + * @summary Accept Org Invite + */ +export const useAcceptOrgInviteApiV1OrganizationsInvitesAcceptPost = < TError = HTTPValidationError, TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { data: OrganizationInviteAccept }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { data: OrganizationInviteAccept }, + TContext +> => { + return useMutation( + getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationOptions( + options, + ), + queryClient, + ); +}; +/** + * Delete the active organization and related entities. + * @summary Delete My Org + */ +export type deleteMyOrgApiV1OrganizationsMeDeleteResponse200 = { + data: OkResponse; + status: 200; +}; + +export type deleteMyOrgApiV1OrganizationsMeDeleteResponseSuccess = + deleteMyOrgApiV1OrganizationsMeDeleteResponse200 & { + headers: Headers; + }; +export type deleteMyOrgApiV1OrganizationsMeDeleteResponse = + deleteMyOrgApiV1OrganizationsMeDeleteResponseSuccess; + +export const getDeleteMyOrgApiV1OrganizationsMeDeleteUrl = () => { + return `/api/v1/organizations/me`; +}; + +export const deleteMyOrgApiV1OrganizationsMeDelete = async ( + options?: RequestInit, +): Promise => { + return customFetch( + getDeleteMyOrgApiV1OrganizationsMeDeleteUrl(), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getDeleteMyOrgApiV1OrganizationsMeDeleteMutationOptions = < + TError = unknown, + TContext = unknown, >(options?: { mutation?: UseMutationOptions< - Awaited>, + Awaited>, TError, - { data: OrganizationActiveUpdate }, + void, TContext >; request?: SecondParameter; }): UseMutationOptions< - Awaited>, + Awaited>, TError, - { data: OrganizationActiveUpdate }, + void, TContext > => { - const mutationKey = ["setActiveOrgApiV1OrganizationsMeActivePatch"]; + const mutationKey = ["deleteMyOrgApiV1OrganizationsMeDelete"]; const { mutation: mutationOptions, request: requestOptions } = options ? options.mutation && "mutationKey" in options.mutation && @@ -444,51 +359,46 @@ export const getSetActiveOrgApiV1OrganizationsMeActivePatchMutationOptions = < : { mutation: { mutationKey }, request: undefined }; const mutationFn: MutationFunction< - Awaited>, - { data: OrganizationActiveUpdate } - > = (props) => { - const { data } = props ?? {}; - - return setActiveOrgApiV1OrganizationsMeActivePatch(data, requestOptions); + Awaited>, + void + > = () => { + return deleteMyOrgApiV1OrganizationsMeDelete(requestOptions); }; return { mutationFn, ...mutationOptions }; }; -export type SetActiveOrgApiV1OrganizationsMeActivePatchMutationResult = - NonNullable< - Awaited> - >; -export type SetActiveOrgApiV1OrganizationsMeActivePatchMutationBody = - OrganizationActiveUpdate; -export type SetActiveOrgApiV1OrganizationsMeActivePatchMutationError = - HTTPValidationError; +export type DeleteMyOrgApiV1OrganizationsMeDeleteMutationResult = NonNullable< + Awaited> +>; + +export type DeleteMyOrgApiV1OrganizationsMeDeleteMutationError = unknown; /** - * @summary Set Active Org + * @summary Delete My Org */ -export const useSetActiveOrgApiV1OrganizationsMeActivePatch = < - TError = HTTPValidationError, +export const useDeleteMyOrgApiV1OrganizationsMeDelete = < + TError = unknown, TContext = unknown, >( options?: { mutation?: UseMutationOptions< - Awaited>, + Awaited>, TError, - { data: OrganizationActiveUpdate }, + void, TContext >; request?: SecondParameter; }, queryClient?: QueryClient, ): UseMutationResult< - Awaited>, + Awaited>, TError, - { data: OrganizationActiveUpdate }, + void, TContext > => { return useMutation( - getSetActiveOrgApiV1OrganizationsMeActivePatchMutationOptions(options), + getDeleteMyOrgApiV1OrganizationsMeDeleteMutationOptions(options), queryClient, ); }; @@ -666,55 +576,69 @@ export function useGetMyOrgApiV1OrganizationsMeGet< } /** - * Delete the active organization and related entities. - * @summary Delete My Org + * Set the caller's active organization. + * @summary Set Active Org */ -export type deleteMyOrgApiV1OrganizationsMeDeleteResponse200 = { - data: OkResponse; +export type setActiveOrgApiV1OrganizationsMeActivePatchResponse200 = { + data: OrganizationRead; status: 200; }; -export type deleteMyOrgApiV1OrganizationsMeDeleteResponseSuccess = - deleteMyOrgApiV1OrganizationsMeDeleteResponse200 & { - headers: Headers; - }; -export type deleteMyOrgApiV1OrganizationsMeDeleteResponse = - deleteMyOrgApiV1OrganizationsMeDeleteResponseSuccess; - -export const getDeleteMyOrgApiV1OrganizationsMeDeleteUrl = () => { - return `/api/v1/organizations/me`; +export type setActiveOrgApiV1OrganizationsMeActivePatchResponse422 = { + data: HTTPValidationError; + status: 422; }; -export const deleteMyOrgApiV1OrganizationsMeDelete = async ( +export type setActiveOrgApiV1OrganizationsMeActivePatchResponseSuccess = + setActiveOrgApiV1OrganizationsMeActivePatchResponse200 & { + headers: Headers; + }; +export type setActiveOrgApiV1OrganizationsMeActivePatchResponseError = + setActiveOrgApiV1OrganizationsMeActivePatchResponse422 & { + headers: Headers; + }; + +export type setActiveOrgApiV1OrganizationsMeActivePatchResponse = + | setActiveOrgApiV1OrganizationsMeActivePatchResponseSuccess + | setActiveOrgApiV1OrganizationsMeActivePatchResponseError; + +export const getSetActiveOrgApiV1OrganizationsMeActivePatchUrl = () => { + return `/api/v1/organizations/me/active`; +}; + +export const setActiveOrgApiV1OrganizationsMeActivePatch = async ( + organizationActiveUpdate: OrganizationActiveUpdate, options?: RequestInit, -): Promise => { - return customFetch( - getDeleteMyOrgApiV1OrganizationsMeDeleteUrl(), +): Promise => { + return customFetch( + getSetActiveOrgApiV1OrganizationsMeActivePatchUrl(), { ...options, - method: "DELETE", + method: "PATCH", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(organizationActiveUpdate), }, ); }; -export const getDeleteMyOrgApiV1OrganizationsMeDeleteMutationOptions = < - TError = unknown, +export const getSetActiveOrgApiV1OrganizationsMeActivePatchMutationOptions = < + TError = HTTPValidationError, TContext = unknown, >(options?: { mutation?: UseMutationOptions< - Awaited>, + Awaited>, TError, - void, + { data: OrganizationActiveUpdate }, TContext >; request?: SecondParameter; }): UseMutationOptions< - Awaited>, + Awaited>, TError, - void, + { data: OrganizationActiveUpdate }, TContext > => { - const mutationKey = ["deleteMyOrgApiV1OrganizationsMeDelete"]; + const mutationKey = ["setActiveOrgApiV1OrganizationsMeActivePatch"]; const { mutation: mutationOptions, request: requestOptions } = options ? options.mutation && "mutationKey" in options.mutation && @@ -724,1212 +648,54 @@ export const getDeleteMyOrgApiV1OrganizationsMeDeleteMutationOptions = < : { mutation: { mutationKey }, request: undefined }; const mutationFn: MutationFunction< - Awaited>, - void - > = () => { - return deleteMyOrgApiV1OrganizationsMeDelete(requestOptions); + Awaited>, + { data: OrganizationActiveUpdate } + > = (props) => { + const { data } = props ?? {}; + + return setActiveOrgApiV1OrganizationsMeActivePatch(data, requestOptions); }; return { mutationFn, ...mutationOptions }; }; -export type DeleteMyOrgApiV1OrganizationsMeDeleteMutationResult = NonNullable< - Awaited> ->; - -export type DeleteMyOrgApiV1OrganizationsMeDeleteMutationError = unknown; +export type SetActiveOrgApiV1OrganizationsMeActivePatchMutationResult = + NonNullable< + Awaited> + >; +export type SetActiveOrgApiV1OrganizationsMeActivePatchMutationBody = + OrganizationActiveUpdate; +export type SetActiveOrgApiV1OrganizationsMeActivePatchMutationError = + HTTPValidationError; /** - * @summary Delete My Org + * @summary Set Active Org */ -export const useDeleteMyOrgApiV1OrganizationsMeDelete = < - TError = unknown, +export const useSetActiveOrgApiV1OrganizationsMeActivePatch = < + TError = HTTPValidationError, TContext = unknown, >( options?: { mutation?: UseMutationOptions< - Awaited>, + Awaited>, TError, - void, + { data: OrganizationActiveUpdate }, TContext >; request?: SecondParameter; }, queryClient?: QueryClient, ): UseMutationResult< - Awaited>, + Awaited>, TError, - void, + { data: OrganizationActiveUpdate }, TContext > => { return useMutation( - getDeleteMyOrgApiV1OrganizationsMeDeleteMutationOptions(options), + getSetActiveOrgApiV1OrganizationsMeActivePatchMutationOptions(options), queryClient, ); }; -/** - * Get the caller's membership record in the active organization. - * @summary Get My Membership - */ -export type getMyMembershipApiV1OrganizationsMeMemberGetResponse200 = { - data: OrganizationMemberRead; - status: 200; -}; - -export type getMyMembershipApiV1OrganizationsMeMemberGetResponseSuccess = - getMyMembershipApiV1OrganizationsMeMemberGetResponse200 & { - headers: Headers; - }; -export type getMyMembershipApiV1OrganizationsMeMemberGetResponse = - getMyMembershipApiV1OrganizationsMeMemberGetResponseSuccess; - -export const getGetMyMembershipApiV1OrganizationsMeMemberGetUrl = () => { - return `/api/v1/organizations/me/member`; -}; - -export const getMyMembershipApiV1OrganizationsMeMemberGet = async ( - options?: RequestInit, -): Promise => { - return customFetch( - getGetMyMembershipApiV1OrganizationsMeMemberGetUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetMyMembershipApiV1OrganizationsMeMemberGetQueryKey = () => { - return [`/api/v1/organizations/me/member`] as const; -}; - -export const getGetMyMembershipApiV1OrganizationsMeMemberGetQueryOptions = < - TData = Awaited< - ReturnType - >, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetMyMembershipApiV1OrganizationsMeMemberGetQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getMyMembershipApiV1OrganizationsMeMemberGet({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetMyMembershipApiV1OrganizationsMeMemberGetQueryResult = - NonNullable< - Awaited> - >; -export type GetMyMembershipApiV1OrganizationsMeMemberGetQueryError = unknown; - -export function useGetMyMembershipApiV1OrganizationsMeMemberGet< - TData = Awaited< - ReturnType - >, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetMyMembershipApiV1OrganizationsMeMemberGet< - TData = Awaited< - ReturnType - >, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetMyMembershipApiV1OrganizationsMeMemberGet< - TData = Awaited< - ReturnType - >, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get My Membership - */ - -export function useGetMyMembershipApiV1OrganizationsMeMemberGet< - TData = Awaited< - ReturnType - >, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getGetMyMembershipApiV1OrganizationsMeMemberGetQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - -/** - * List members for the active organization. - * @summary List Org Members - */ -export type listOrgMembersApiV1OrganizationsMeMembersGetResponse200 = { - data: LimitOffsetPageTypeVarCustomizedOrganizationMemberRead; - status: 200; -}; - -export type listOrgMembersApiV1OrganizationsMeMembersGetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type listOrgMembersApiV1OrganizationsMeMembersGetResponseSuccess = - listOrgMembersApiV1OrganizationsMeMembersGetResponse200 & { - headers: Headers; - }; -export type listOrgMembersApiV1OrganizationsMeMembersGetResponseError = - listOrgMembersApiV1OrganizationsMeMembersGetResponse422 & { - headers: Headers; - }; - -export type listOrgMembersApiV1OrganizationsMeMembersGetResponse = - | listOrgMembersApiV1OrganizationsMeMembersGetResponseSuccess - | listOrgMembersApiV1OrganizationsMeMembersGetResponseError; - -export const getListOrgMembersApiV1OrganizationsMeMembersGetUrl = ( - params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/v1/organizations/me/members?${stringifiedParams}` - : `/api/v1/organizations/me/members`; -}; - -export const listOrgMembersApiV1OrganizationsMeMembersGet = async ( - params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, - options?: RequestInit, -): Promise => { - return customFetch( - getListOrgMembersApiV1OrganizationsMeMembersGetUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getListOrgMembersApiV1OrganizationsMeMembersGetQueryKey = ( - params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, -) => { - return [ - `/api/v1/organizations/me/members`, - ...(params ? [params] : []), - ] as const; -}; - -export const getListOrgMembersApiV1OrganizationsMeMembersGetQueryOptions = < - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getListOrgMembersApiV1OrganizationsMeMembersGetQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - listOrgMembersApiV1OrganizationsMeMembersGet(params, { - signal, - ...requestOptions, - }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type ListOrgMembersApiV1OrganizationsMeMembersGetQueryResult = - NonNullable< - Awaited> - >; -export type ListOrgMembersApiV1OrganizationsMeMembersGetQueryError = - HTTPValidationError; - -export function useListOrgMembersApiV1OrganizationsMeMembersGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - params: undefined | ListOrgMembersApiV1OrganizationsMeMembersGetParams, - options: { - query: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useListOrgMembersApiV1OrganizationsMeMembersGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useListOrgMembersApiV1OrganizationsMeMembersGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List Org Members - */ - -export function useListOrgMembersApiV1OrganizationsMeMembersGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getListOrgMembersApiV1OrganizationsMeMembersGetQueryOptions( - params, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - -/** - * Get a specific organization member by id. - * @summary Get Org Member - */ -export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse200 = { - data: OrganizationMemberRead; - status: 200; -}; - -export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponseSuccess = - getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse200 & { - headers: Headers; - }; -export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponseError = - getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse422 & { - headers: Headers; - }; - -export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse = - | getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponseSuccess - | getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponseError; - -export const getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetUrl = ( - memberId: string, -) => { - return `/api/v1/organizations/me/members/${memberId}`; -}; - -export const getOrgMemberApiV1OrganizationsMeMembersMemberIdGet = async ( - memberId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetUrl(memberId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryKey = ( - memberId: string, -) => { - return [`/api/v1/organizations/me/members/${memberId}`] as const; -}; - -export const getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryOptions = - < - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, - >( - memberId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType< - typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet - > - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - ) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryKey(memberId); - - const queryFn: QueryFunction< - Awaited< - ReturnType - > - > = ({ signal }) => - getOrgMemberApiV1OrganizationsMeMembersMemberIdGet(memberId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!memberId, - ...queryOptions, - } as UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > & { queryKey: DataTag }; - }; - -export type GetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryResult = - NonNullable< - Awaited< - ReturnType - > - >; -export type GetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryError = - HTTPValidationError; - -export function useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - memberId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType< - typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet - > - >, - TError, - Awaited< - ReturnType< - typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet - > - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - memberId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType< - typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet - > - >, - TError, - Awaited< - ReturnType< - typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet - > - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - memberId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Org Member - */ - -export function useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - memberId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited< - ReturnType - >, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryOptions( - memberId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - -/** - * Update a member's role in the organization. - * @summary Update Org Member - */ -export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse200 = - { - data: OrganizationMemberRead; - status: 200; - }; - -export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse422 = - { - data: HTTPValidationError; - status: 422; - }; - -export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponseSuccess = - updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse200 & { - headers: Headers; - }; -export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponseError = - updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse422 & { - headers: Headers; - }; - -export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse = - | updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponseSuccess - | updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponseError; - -export const getUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchUrl = ( - memberId: string, -) => { - return `/api/v1/organizations/me/members/${memberId}`; -}; - -export const updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch = async ( - memberId: string, - organizationMemberUpdate: OrganizationMemberUpdate, - options?: RequestInit, -): Promise => { - return customFetch( - getUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchUrl(memberId), - { - ...options, - method: "PATCH", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(organizationMemberUpdate), - }, - ); -}; - -export const getUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch - > - >, - TError, - { memberId: string; data: OrganizationMemberUpdate }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType - >, - TError, - { memberId: string; data: OrganizationMemberUpdate }, - TContext - > => { - const mutationKey = [ - "updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType< - typeof updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch - > - >, - { memberId: string; data: OrganizationMemberUpdate } - > = (props) => { - const { memberId, data } = props ?? {}; - - return updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch( - memberId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type UpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationResult = - NonNullable< - Awaited< - ReturnType - > - >; -export type UpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationBody = - OrganizationMemberUpdate; -export type UpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationError = - HTTPValidationError; - -/** - * @summary Update Org Member - */ -export const useUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch - > - >, - TError, - { memberId: string; data: OrganizationMemberUpdate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType - >, - TError, - { memberId: string; data: OrganizationMemberUpdate }, - TContext -> => { - return useMutation( - getUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationOptions( - options, - ), - queryClient, - ); -}; -/** - * Remove a member from the active organization. - * @summary Remove Org Member - */ -export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse200 = - { - data: OkResponse; - status: 200; - }; - -export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse422 = - { - data: HTTPValidationError; - status: 422; - }; - -export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponseSuccess = - removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse200 & { - headers: Headers; - }; -export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponseError = - removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse422 & { - headers: Headers; - }; - -export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse = - | removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponseSuccess - | removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponseError; - -export const getRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteUrl = ( - memberId: string, -) => { - return `/api/v1/organizations/me/members/${memberId}`; -}; - -export const removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete = async ( - memberId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteUrl(memberId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete - > - >, - TError, - { memberId: string }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType< - typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete - > - >, - TError, - { memberId: string }, - TContext - > => { - const mutationKey = [ - "removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType< - typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete - > - >, - { memberId: string } - > = (props) => { - const { memberId } = props ?? {}; - - return removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete( - memberId, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type RemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteMutationResult = - NonNullable< - Awaited< - ReturnType< - typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete - > - > - >; - -export type RemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Remove Org Member - */ -export const useRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete - > - >, - TError, - { memberId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited< - ReturnType - >, - TError, - { memberId: string }, - TContext -> => { - return useMutation( - getRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteMutationOptions( - options, - ), - queryClient, - ); -}; -/** - * Update board-level access settings for a member. - * @summary Update Member Access - */ -export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse200 = - { - data: OrganizationMemberRead; - status: 200; - }; - -export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse422 = - { - data: HTTPValidationError; - status: 422; - }; - -export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponseSuccess = - updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse200 & { - headers: Headers; - }; -export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponseError = - updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse422 & { - headers: Headers; - }; - -export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse = - - | updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponseSuccess - | updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponseError; - -export const getUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutUrl = - (memberId: string) => { - return `/api/v1/organizations/me/members/${memberId}/access`; - }; - -export const updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut = - async ( - memberId: string, - organizationMemberAccessUpdate: OrganizationMemberAccessUpdate, - options?: RequestInit, - ): Promise => { - return customFetch( - getUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutUrl( - memberId, - ), - { - ...options, - method: "PUT", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(organizationMemberAccessUpdate), - }, - ); - }; - -export const getUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationOptions = - (options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut - > - >, - TError, - { memberId: string; data: OrganizationMemberAccessUpdate }, - TContext - >; - request?: SecondParameter; - }): UseMutationOptions< - Awaited< - ReturnType< - typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut - > - >, - TError, - { memberId: string; data: OrganizationMemberAccessUpdate }, - TContext - > => { - const mutationKey = [ - "updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut", - ]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited< - ReturnType< - typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut - > - >, - { memberId: string; data: OrganizationMemberAccessUpdate } - > = (props) => { - const { memberId, data } = props ?? {}; - - return updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut( - memberId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; - }; - -export type UpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationResult = - NonNullable< - Awaited< - ReturnType< - typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut - > - > - >; -export type UpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationBody = - OrganizationMemberAccessUpdate; -export type UpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationError = - HTTPValidationError; - -/** - * @summary Update Member Access - */ -export const useUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut = - ( - options?: { - mutation?: UseMutationOptions< - Awaited< - ReturnType< - typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut - > - >, - TError, - { memberId: string; data: OrganizationMemberAccessUpdate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, - ): UseMutationResult< - Awaited< - ReturnType< - typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut - > - >, - TError, - { memberId: string; data: OrganizationMemberAccessUpdate }, - TContext - > => { - return useMutation( - getUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationOptions( - options, - ), - queryClient, - ); - }; /** * List pending invites for the active organization. * @summary List Org Invites @@ -2457,71 +1223,737 @@ export const useRevokeOrgInviteApiV1OrganizationsMeInvitesInviteIdDelete = < ); }; /** - * Accept an invite and return resulting membership. - * @summary Accept Org Invite + * List organizations where the current user is a member. + * @summary List My Organizations */ -export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse200 = { - data: OrganizationMemberRead; +export type listMyOrganizationsApiV1OrganizationsMeListGetResponse200 = { + data: OrganizationListItem[]; status: 200; }; -export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponseSuccess = - acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse200 & { - headers: Headers; - }; -export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponseError = - acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse422 & { +export type listMyOrganizationsApiV1OrganizationsMeListGetResponseSuccess = + listMyOrganizationsApiV1OrganizationsMeListGetResponse200 & { headers: Headers; }; +export type listMyOrganizationsApiV1OrganizationsMeListGetResponse = + listMyOrganizationsApiV1OrganizationsMeListGetResponseSuccess; -export type acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponse = - | acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponseSuccess - | acceptOrgInviteApiV1OrganizationsInvitesAcceptPostResponseError; - -export const getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostUrl = () => { - return `/api/v1/organizations/invites/accept`; +export const getListMyOrganizationsApiV1OrganizationsMeListGetUrl = () => { + return `/api/v1/organizations/me/list`; }; -export const acceptOrgInviteApiV1OrganizationsInvitesAcceptPost = async ( - organizationInviteAccept: OrganizationInviteAccept, +export const listMyOrganizationsApiV1OrganizationsMeListGet = async ( options?: RequestInit, -): Promise => { - return customFetch( - getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostUrl(), +): Promise => { + return customFetch( + getListMyOrganizationsApiV1OrganizationsMeListGetUrl(), { ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(organizationInviteAccept), + method: "GET", }, ); }; -export const getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationOptions = +export const getListMyOrganizationsApiV1OrganizationsMeListGetQueryKey = () => { + return [`/api/v1/organizations/me/list`] as const; +}; + +export const getListMyOrganizationsApiV1OrganizationsMeListGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = unknown, +>(options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getListMyOrganizationsApiV1OrganizationsMeListGetQueryKey(); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + listMyOrganizationsApiV1OrganizationsMeListGet({ + signal, + ...requestOptions, + }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListMyOrganizationsApiV1OrganizationsMeListGetQueryResult = + NonNullable< + Awaited> + >; +export type ListMyOrganizationsApiV1OrganizationsMeListGetQueryError = unknown; + +export function useListMyOrganizationsApiV1OrganizationsMeListGet< + TData = Awaited< + ReturnType + >, + TError = unknown, +>( + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListMyOrganizationsApiV1OrganizationsMeListGet< + TData = Awaited< + ReturnType + >, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListMyOrganizationsApiV1OrganizationsMeListGet< + TData = Awaited< + ReturnType + >, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List My Organizations + */ + +export function useListMyOrganizationsApiV1OrganizationsMeListGet< + TData = Awaited< + ReturnType + >, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getListMyOrganizationsApiV1OrganizationsMeListGetQueryOptions(options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Get the caller's membership record in the active organization. + * @summary Get My Membership + */ +export type getMyMembershipApiV1OrganizationsMeMemberGetResponse200 = { + data: OrganizationMemberRead; + status: 200; +}; + +export type getMyMembershipApiV1OrganizationsMeMemberGetResponseSuccess = + getMyMembershipApiV1OrganizationsMeMemberGetResponse200 & { + headers: Headers; + }; +export type getMyMembershipApiV1OrganizationsMeMemberGetResponse = + getMyMembershipApiV1OrganizationsMeMemberGetResponseSuccess; + +export const getGetMyMembershipApiV1OrganizationsMeMemberGetUrl = () => { + return `/api/v1/organizations/me/member`; +}; + +export const getMyMembershipApiV1OrganizationsMeMemberGet = async ( + options?: RequestInit, +): Promise => { + return customFetch( + getGetMyMembershipApiV1OrganizationsMeMemberGetUrl(), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetMyMembershipApiV1OrganizationsMeMemberGetQueryKey = () => { + return [`/api/v1/organizations/me/member`] as const; +}; + +export const getGetMyMembershipApiV1OrganizationsMeMemberGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = unknown, +>(options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetMyMembershipApiV1OrganizationsMeMemberGetQueryKey(); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getMyMembershipApiV1OrganizationsMeMemberGet({ signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type GetMyMembershipApiV1OrganizationsMeMemberGetQueryResult = + NonNullable< + Awaited> + >; +export type GetMyMembershipApiV1OrganizationsMeMemberGetQueryError = unknown; + +export function useGetMyMembershipApiV1OrganizationsMeMemberGet< + TData = Awaited< + ReturnType + >, + TError = unknown, +>( + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetMyMembershipApiV1OrganizationsMeMemberGet< + TData = Awaited< + ReturnType + >, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useGetMyMembershipApiV1OrganizationsMeMemberGet< + TData = Awaited< + ReturnType + >, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Get My Membership + */ + +export function useGetMyMembershipApiV1OrganizationsMeMemberGet< + TData = Awaited< + ReturnType + >, + TError = unknown, +>( + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getGetMyMembershipApiV1OrganizationsMeMemberGetQueryOptions(options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * List members for the active organization. + * @summary List Org Members + */ +export type listOrgMembersApiV1OrganizationsMeMembersGetResponse200 = { + data: LimitOffsetPageTypeVarCustomizedOrganizationMemberRead; + status: 200; +}; + +export type listOrgMembersApiV1OrganizationsMeMembersGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type listOrgMembersApiV1OrganizationsMeMembersGetResponseSuccess = + listOrgMembersApiV1OrganizationsMeMembersGetResponse200 & { + headers: Headers; + }; +export type listOrgMembersApiV1OrganizationsMeMembersGetResponseError = + listOrgMembersApiV1OrganizationsMeMembersGetResponse422 & { + headers: Headers; + }; + +export type listOrgMembersApiV1OrganizationsMeMembersGetResponse = + | listOrgMembersApiV1OrganizationsMeMembersGetResponseSuccess + | listOrgMembersApiV1OrganizationsMeMembersGetResponseError; + +export const getListOrgMembersApiV1OrganizationsMeMembersGetUrl = ( + params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/organizations/me/members?${stringifiedParams}` + : `/api/v1/organizations/me/members`; +}; + +export const listOrgMembersApiV1OrganizationsMeMembersGet = async ( + params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, + options?: RequestInit, +): Promise => { + return customFetch( + getListOrgMembersApiV1OrganizationsMeMembersGetUrl(params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getListOrgMembersApiV1OrganizationsMeMembersGetQueryKey = ( + params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, +) => { + return [ + `/api/v1/organizations/me/members`, + ...(params ? [params] : []), + ] as const; +}; + +export const getListOrgMembersApiV1OrganizationsMeMembersGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getListOrgMembersApiV1OrganizationsMeMembersGetQueryKey(params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + listOrgMembersApiV1OrganizationsMeMembersGet(params, { + signal, + ...requestOptions, + }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListOrgMembersApiV1OrganizationsMeMembersGetQueryResult = + NonNullable< + Awaited> + >; +export type ListOrgMembersApiV1OrganizationsMeMembersGetQueryError = + HTTPValidationError; + +export function useListOrgMembersApiV1OrganizationsMeMembersGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + params: undefined | ListOrgMembersApiV1OrganizationsMeMembersGetParams, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListOrgMembersApiV1OrganizationsMeMembersGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListOrgMembersApiV1OrganizationsMeMembersGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List Org Members + */ + +export function useListOrgMembersApiV1OrganizationsMeMembersGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + params?: ListOrgMembersApiV1OrganizationsMeMembersGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getListOrgMembersApiV1OrganizationsMeMembersGetQueryOptions( + params, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Remove a member from the active organization. + * @summary Remove Org Member + */ +export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse200 = + { + data: OkResponse; + status: 200; + }; + +export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponseSuccess = + removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse200 & { + headers: Headers; + }; +export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponseError = + removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse422 & { + headers: Headers; + }; + +export type removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponse = + | removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponseSuccess + | removeOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteResponseError; + +export const getRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteUrl = ( + memberId: string, +) => { + return `/api/v1/organizations/me/members/${memberId}`; +}; + +export const removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete = async ( + memberId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteUrl(memberId), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteMutationOptions = (options?: { mutation?: UseMutationOptions< Awaited< - ReturnType + ReturnType< + typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete + > >, TError, - { data: OrganizationInviteAccept }, + { memberId: string }, TContext >; request?: SecondParameter; }): UseMutationOptions< Awaited< - ReturnType + ReturnType< + typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete + > >, TError, - { data: OrganizationInviteAccept }, + { memberId: string }, TContext > => { - const mutationKey = ["acceptOrgInviteApiV1OrganizationsInvitesAcceptPost"]; + const mutationKey = [ + "removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete", + ]; const { mutation: mutationOptions, request: requestOptions } = options ? options.mutation && "mutationKey" in options.mutation && @@ -2532,13 +1964,422 @@ export const getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationOption const mutationFn: MutationFunction< Awaited< - ReturnType + ReturnType< + typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete + > >, - { data: OrganizationInviteAccept } + { memberId: string } > = (props) => { - const { data } = props ?? {}; + const { memberId } = props ?? {}; - return acceptOrgInviteApiV1OrganizationsInvitesAcceptPost( + return removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete( + memberId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type RemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteMutationResult = + NonNullable< + Awaited< + ReturnType< + typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete + > + > + >; + +export type RemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Remove Org Member + */ +export const useRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof removeOrgMemberApiV1OrganizationsMeMembersMemberIdDelete + > + >, + TError, + { memberId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { memberId: string }, + TContext +> => { + return useMutation( + getRemoveOrgMemberApiV1OrganizationsMeMembersMemberIdDeleteMutationOptions( + options, + ), + queryClient, + ); +}; +/** + * Get a specific organization member by id. + * @summary Get Org Member + */ +export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse200 = { + data: OrganizationMemberRead; + status: 200; +}; + +export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponseSuccess = + getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse200 & { + headers: Headers; + }; +export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponseError = + getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse422 & { + headers: Headers; + }; + +export type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse = + | getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponseSuccess + | getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponseError; + +export const getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetUrl = ( + memberId: string, +) => { + return `/api/v1/organizations/me/members/${memberId}`; +}; + +export const getOrgMemberApiV1OrganizationsMeMembersMemberIdGet = async ( + memberId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetUrl(memberId), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryKey = ( + memberId: string, +) => { + return [`/api/v1/organizations/me/members/${memberId}`] as const; +}; + +export const getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryOptions = + < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, + >( + memberId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + ) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryKey(memberId); + + const queryFn: QueryFunction< + Awaited< + ReturnType + > + > = ({ signal }) => + getOrgMemberApiV1OrganizationsMeMembersMemberIdGet(memberId, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!memberId, + ...queryOptions, + } as UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > & { queryKey: DataTag }; + }; + +export type GetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type GetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryError = + HTTPValidationError; + +export function useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + memberId: string, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType< + typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet + > + >, + TError, + Awaited< + ReturnType< + typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + memberId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType< + typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet + > + >, + TError, + Awaited< + ReturnType< + typeof getOrgMemberApiV1OrganizationsMeMembersMemberIdGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + memberId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Get Org Member + */ + +export function useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + memberId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryOptions( + memberId, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Update a member's role in the organization. + * @summary Update Org Member + */ +export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse200 = + { + data: OrganizationMemberRead; + status: 200; + }; + +export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponseSuccess = + updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse200 & { + headers: Headers; + }; +export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponseError = + updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse422 & { + headers: Headers; + }; + +export type updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponse = + | updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponseSuccess + | updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchResponseError; + +export const getUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchUrl = ( + memberId: string, +) => { + return `/api/v1/organizations/me/members/${memberId}`; +}; + +export const updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch = async ( + memberId: string, + organizationMemberUpdate: OrganizationMemberUpdate, + options?: RequestInit, +): Promise => { + return customFetch( + getUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchUrl(memberId), + { + ...options, + method: "PATCH", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(organizationMemberUpdate), + }, + ); +}; + +export const getUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch + > + >, + TError, + { memberId: string; data: OrganizationMemberUpdate }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { memberId: string; data: OrganizationMemberUpdate }, + TContext + > => { + const mutationKey = [ + "updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType< + typeof updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch + > + >, + { memberId: string; data: OrganizationMemberUpdate } + > = (props) => { + const { memberId, data } = props ?? {}; + + return updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch( + memberId, data, requestOptions, ); @@ -2547,31 +2388,33 @@ export const getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationOption return { mutationFn, ...mutationOptions }; }; -export type AcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationResult = +export type UpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationResult = NonNullable< Awaited< - ReturnType + ReturnType > >; -export type AcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationBody = - OrganizationInviteAccept; -export type AcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationError = +export type UpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationBody = + OrganizationMemberUpdate; +export type UpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationError = HTTPValidationError; /** - * @summary Accept Org Invite + * @summary Update Org Member */ -export const useAcceptOrgInviteApiV1OrganizationsInvitesAcceptPost = < +export const useUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch = < TError = HTTPValidationError, TContext = unknown, >( options?: { mutation?: UseMutationOptions< Awaited< - ReturnType + ReturnType< + typeof updateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch + > >, TError, - { data: OrganizationInviteAccept }, + { memberId: string; data: OrganizationMemberUpdate }, TContext >; request?: SecondParameter; @@ -2579,16 +2422,173 @@ export const useAcceptOrgInviteApiV1OrganizationsInvitesAcceptPost = < queryClient?: QueryClient, ): UseMutationResult< Awaited< - ReturnType + ReturnType >, TError, - { data: OrganizationInviteAccept }, + { memberId: string; data: OrganizationMemberUpdate }, TContext > => { return useMutation( - getAcceptOrgInviteApiV1OrganizationsInvitesAcceptPostMutationOptions( + getUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatchMutationOptions( options, ), queryClient, ); }; +/** + * Update board-level access settings for a member. + * @summary Update Member Access + */ +export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse200 = + { + data: OrganizationMemberRead; + status: 200; + }; + +export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponseSuccess = + updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse200 & { + headers: Headers; + }; +export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponseError = + updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse422 & { + headers: Headers; + }; + +export type updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponse = + + | updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponseSuccess + | updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutResponseError; + +export const getUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutUrl = + (memberId: string) => { + return `/api/v1/organizations/me/members/${memberId}/access`; + }; + +export const updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut = + async ( + memberId: string, + organizationMemberAccessUpdate: OrganizationMemberAccessUpdate, + options?: RequestInit, + ): Promise => { + return customFetch( + getUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutUrl( + memberId, + ), + { + ...options, + method: "PUT", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(organizationMemberAccessUpdate), + }, + ); + }; + +export const getUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut + > + >, + TError, + { memberId: string; data: OrganizationMemberAccessUpdate }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType< + typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut + > + >, + TError, + { memberId: string; data: OrganizationMemberAccessUpdate }, + TContext + > => { + const mutationKey = [ + "updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut", + ]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited< + ReturnType< + typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut + > + >, + { memberId: string; data: OrganizationMemberAccessUpdate } + > = (props) => { + const { memberId, data } = props ?? {}; + + return updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut( + memberId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type UpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationResult = + NonNullable< + Awaited< + ReturnType< + typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut + > + > + >; +export type UpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationBody = + OrganizationMemberAccessUpdate; +export type UpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationError = + HTTPValidationError; + +/** + * @summary Update Member Access + */ +export const useUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut = + ( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut + > + >, + TError, + { memberId: string; data: OrganizationMemberAccessUpdate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, + ): UseMutationResult< + Awaited< + ReturnType< + typeof updateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut + > + >, + TError, + { memberId: string; data: OrganizationMemberAccessUpdate }, + TContext + > => { + return useMutation( + getUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPutMutationOptions( + options, + ), + queryClient, + ); + }; diff --git a/frontend/src/api/generated/skills/skills.ts b/frontend/src/api/generated/skills/skills.ts index af142e6f..1e34e5ed 100644 --- a/frontend/src/api/generated/skills/skills.ts +++ b/frontend/src/api/generated/skills/skills.ts @@ -1233,6 +1233,126 @@ export const useCreateSkillPackApiV1SkillsPacksPost = < queryClient, ); }; +/** + * Delete one pack source from the organization. + * @summary Delete Skill Pack + */ +export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse200 = { + data: OkResponse; + status: 200; +}; + +export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponseSuccess = + deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponseError = + deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse = + | deleteSkillPackApiV1SkillsPacksPackIdDeleteResponseSuccess + | deleteSkillPackApiV1SkillsPacksPackIdDeleteResponseError; + +export const getDeleteSkillPackApiV1SkillsPacksPackIdDeleteUrl = ( + packId: string, +) => { + return `/api/v1/skills/packs/${packId}`; +}; + +export const deleteSkillPackApiV1SkillsPacksPackIdDelete = async ( + packId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getDeleteSkillPackApiV1SkillsPacksPackIdDeleteUrl(packId), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getDeleteSkillPackApiV1SkillsPacksPackIdDeleteMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { packId: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { packId: string }, + TContext +> => { + const mutationKey = ["deleteSkillPackApiV1SkillsPacksPackIdDelete"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { packId: string } + > = (props) => { + const { packId } = props ?? {}; + + return deleteSkillPackApiV1SkillsPacksPackIdDelete(packId, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type DeleteSkillPackApiV1SkillsPacksPackIdDeleteMutationResult = + NonNullable< + Awaited> + >; + +export type DeleteSkillPackApiV1SkillsPacksPackIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Delete Skill Pack + */ +export const useDeleteSkillPackApiV1SkillsPacksPackIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { packId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { packId: string }, + TContext +> => { + return useMutation( + getDeleteSkillPackApiV1SkillsPacksPackIdDeleteMutationOptions(options), + queryClient, + ); +}; /** * Get one skill pack by ID. * @summary Get Skill Pack @@ -1568,126 +1688,6 @@ export const useUpdateSkillPackApiV1SkillsPacksPackIdPatch = < queryClient, ); }; -/** - * Delete one pack source from the organization. - * @summary Delete Skill Pack - */ -export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse200 = { - data: OkResponse; - status: 200; -}; - -export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponseSuccess = - deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponseError = - deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteSkillPackApiV1SkillsPacksPackIdDeleteResponse = - | deleteSkillPackApiV1SkillsPacksPackIdDeleteResponseSuccess - | deleteSkillPackApiV1SkillsPacksPackIdDeleteResponseError; - -export const getDeleteSkillPackApiV1SkillsPacksPackIdDeleteUrl = ( - packId: string, -) => { - return `/api/v1/skills/packs/${packId}`; -}; - -export const deleteSkillPackApiV1SkillsPacksPackIdDelete = async ( - packId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteSkillPackApiV1SkillsPacksPackIdDeleteUrl(packId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteSkillPackApiV1SkillsPacksPackIdDeleteMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { packId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { packId: string }, - TContext -> => { - const mutationKey = ["deleteSkillPackApiV1SkillsPacksPackIdDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { packId: string } - > = (props) => { - const { packId } = props ?? {}; - - return deleteSkillPackApiV1SkillsPacksPackIdDelete(packId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteSkillPackApiV1SkillsPacksPackIdDeleteMutationResult = - NonNullable< - Awaited> - >; - -export type DeleteSkillPackApiV1SkillsPacksPackIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Delete Skill Pack - */ -export const useDeleteSkillPackApiV1SkillsPacksPackIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { packId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { packId: string }, - TContext -> => { - return useMutation( - getDeleteSkillPackApiV1SkillsPacksPackIdDeleteMutationOptions(options), - queryClient, - ); -}; /** * Clone a pack repository and upsert discovered skills from `skills/**\/SKILL.md`. * @summary Sync Skill Pack diff --git a/frontend/src/api/generated/tags/tags.ts b/frontend/src/api/generated/tags/tags.ts index 4f2e2a8c..36278bd7 100644 --- a/frontend/src/api/generated/tags/tags.ts +++ b/frontend/src/api/generated/tags/tags.ts @@ -360,6 +360,122 @@ export const useCreateTagApiV1TagsPost = < queryClient, ); }; +/** + * Delete a tag and remove all associated tag links. + * @summary Delete Tag + */ +export type deleteTagApiV1TagsTagIdDeleteResponse200 = { + data: OkResponse; + status: 200; +}; + +export type deleteTagApiV1TagsTagIdDeleteResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type deleteTagApiV1TagsTagIdDeleteResponseSuccess = + deleteTagApiV1TagsTagIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteTagApiV1TagsTagIdDeleteResponseError = + deleteTagApiV1TagsTagIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteTagApiV1TagsTagIdDeleteResponse = + | deleteTagApiV1TagsTagIdDeleteResponseSuccess + | deleteTagApiV1TagsTagIdDeleteResponseError; + +export const getDeleteTagApiV1TagsTagIdDeleteUrl = (tagId: string) => { + return `/api/v1/tags/${tagId}`; +}; + +export const deleteTagApiV1TagsTagIdDelete = async ( + tagId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getDeleteTagApiV1TagsTagIdDeleteUrl(tagId), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getDeleteTagApiV1TagsTagIdDeleteMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { tagId: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { tagId: string }, + TContext +> => { + const mutationKey = ["deleteTagApiV1TagsTagIdDelete"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { tagId: string } + > = (props) => { + const { tagId } = props ?? {}; + + return deleteTagApiV1TagsTagIdDelete(tagId, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type DeleteTagApiV1TagsTagIdDeleteMutationResult = NonNullable< + Awaited> +>; + +export type DeleteTagApiV1TagsTagIdDeleteMutationError = HTTPValidationError; + +/** + * @summary Delete Tag + */ +export const useDeleteTagApiV1TagsTagIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { tagId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { tagId: string }, + TContext +> => { + return useMutation( + getDeleteTagApiV1TagsTagIdDeleteMutationOptions(options), + queryClient, + ); +}; /** * Get a single tag in the active organization. * @summary Get Tag @@ -676,119 +792,3 @@ export const useUpdateTagApiV1TagsTagIdPatch = < queryClient, ); }; -/** - * Delete a tag and remove all associated tag links. - * @summary Delete Tag - */ -export type deleteTagApiV1TagsTagIdDeleteResponse200 = { - data: OkResponse; - status: 200; -}; - -export type deleteTagApiV1TagsTagIdDeleteResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteTagApiV1TagsTagIdDeleteResponseSuccess = - deleteTagApiV1TagsTagIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteTagApiV1TagsTagIdDeleteResponseError = - deleteTagApiV1TagsTagIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteTagApiV1TagsTagIdDeleteResponse = - | deleteTagApiV1TagsTagIdDeleteResponseSuccess - | deleteTagApiV1TagsTagIdDeleteResponseError; - -export const getDeleteTagApiV1TagsTagIdDeleteUrl = (tagId: string) => { - return `/api/v1/tags/${tagId}`; -}; - -export const deleteTagApiV1TagsTagIdDelete = async ( - tagId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteTagApiV1TagsTagIdDeleteUrl(tagId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteTagApiV1TagsTagIdDeleteMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { tagId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { tagId: string }, - TContext -> => { - const mutationKey = ["deleteTagApiV1TagsTagIdDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { tagId: string } - > = (props) => { - const { tagId } = props ?? {}; - - return deleteTagApiV1TagsTagIdDelete(tagId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteTagApiV1TagsTagIdDeleteMutationResult = NonNullable< - Awaited> ->; - -export type DeleteTagApiV1TagsTagIdDeleteMutationError = HTTPValidationError; - -/** - * @summary Delete Tag - */ -export const useDeleteTagApiV1TagsTagIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { tagId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { tagId: string }, - TContext -> => { - return useMutation( - getDeleteTagApiV1TagsTagIdDeleteMutationOptions(options), - queryClient, - ); -}; diff --git a/frontend/src/api/generated/tasks/tasks.ts b/frontend/src/api/generated/tasks/tasks.ts index 1c246410..cf1f6ac2 100644 --- a/frontend/src/api/generated/tasks/tasks.ts +++ b/frontend/src/api/generated/tasks/tasks.ts @@ -40,259 +40,6 @@ import { customFetch } from "../../mutator"; type SecondParameter unknown> = Parameters[1]; -/** - * Stream task and task-comment events as SSE payloads. - * @summary Stream Tasks - */ -export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponse200 = { - data: unknown; - status: 200; -}; - -export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponseSuccess = - streamTasksApiV1BoardsBoardIdTasksStreamGetResponse200 & { - headers: Headers; - }; -export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponseError = - streamTasksApiV1BoardsBoardIdTasksStreamGetResponse422 & { - headers: Headers; - }; - -export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponse = - | streamTasksApiV1BoardsBoardIdTasksStreamGetResponseSuccess - | streamTasksApiV1BoardsBoardIdTasksStreamGetResponseError; - -export const getStreamTasksApiV1BoardsBoardIdTasksStreamGetUrl = ( - boardId: string, - params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/v1/boards/${boardId}/tasks/stream?${stringifiedParams}` - : `/api/v1/boards/${boardId}/tasks/stream`; -}; - -export const streamTasksApiV1BoardsBoardIdTasksStreamGet = async ( - boardId: string, - params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, - options?: RequestInit, -): Promise => { - return customFetch( - getStreamTasksApiV1BoardsBoardIdTasksStreamGetUrl(boardId, params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getStreamTasksApiV1BoardsBoardIdTasksStreamGetQueryKey = ( - boardId: string, - params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, -) => { - return [ - `/api/v1/boards/${boardId}/tasks/stream`, - ...(params ? [params] : []), - ] as const; -}; - -export const getStreamTasksApiV1BoardsBoardIdTasksStreamGetQueryOptions = < - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getStreamTasksApiV1BoardsBoardIdTasksStreamGetQueryKey(boardId, params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - streamTasksApiV1BoardsBoardIdTasksStreamGet(boardId, params, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!boardId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type StreamTasksApiV1BoardsBoardIdTasksStreamGetQueryResult = - NonNullable< - Awaited> - >; -export type StreamTasksApiV1BoardsBoardIdTasksStreamGetQueryError = - HTTPValidationError; - -export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params: undefined | StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited< - ReturnType - >, - TError, - Awaited< - ReturnType - > - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Stream Tasks - */ - -export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< - TData = Awaited< - ReturnType - >, - TError = HTTPValidationError, ->( - boardId: string, - params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getStreamTasksApiV1BoardsBoardIdTasksStreamGetQueryOptions( - boardId, - params, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - return { ...query, queryKey: queryOptions.queryKey }; -} - /** * List board tasks with optional status and assignment filters. * @summary List Tasks @@ -655,6 +402,385 @@ export const useCreateTaskApiV1BoardsBoardIdTasksPost = < queryClient, ); }; +/** + * Stream task and task-comment events as SSE payloads. + * @summary Stream Tasks + */ +export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponse200 = { + data: unknown; + status: 200; +}; + +export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponseSuccess = + streamTasksApiV1BoardsBoardIdTasksStreamGetResponse200 & { + headers: Headers; + }; +export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponseError = + streamTasksApiV1BoardsBoardIdTasksStreamGetResponse422 & { + headers: Headers; + }; + +export type streamTasksApiV1BoardsBoardIdTasksStreamGetResponse = + | streamTasksApiV1BoardsBoardIdTasksStreamGetResponseSuccess + | streamTasksApiV1BoardsBoardIdTasksStreamGetResponseError; + +export const getStreamTasksApiV1BoardsBoardIdTasksStreamGetUrl = ( + boardId: string, + params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/boards/${boardId}/tasks/stream?${stringifiedParams}` + : `/api/v1/boards/${boardId}/tasks/stream`; +}; + +export const streamTasksApiV1BoardsBoardIdTasksStreamGet = async ( + boardId: string, + params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, + options?: RequestInit, +): Promise => { + return customFetch( + getStreamTasksApiV1BoardsBoardIdTasksStreamGetUrl(boardId, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getStreamTasksApiV1BoardsBoardIdTasksStreamGetQueryKey = ( + boardId: string, + params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, +) => { + return [ + `/api/v1/boards/${boardId}/tasks/stream`, + ...(params ? [params] : []), + ] as const; +}; + +export const getStreamTasksApiV1BoardsBoardIdTasksStreamGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getStreamTasksApiV1BoardsBoardIdTasksStreamGetQueryKey(boardId, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + streamTasksApiV1BoardsBoardIdTasksStreamGet(boardId, params, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!boardId, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type StreamTasksApiV1BoardsBoardIdTasksStreamGetQueryResult = + NonNullable< + Awaited> + >; +export type StreamTasksApiV1BoardsBoardIdTasksStreamGetQueryError = + HTTPValidationError; + +export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params: undefined | StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, + options: { + query: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Stream Tasks + */ + +export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getStreamTasksApiV1BoardsBoardIdTasksStreamGetQueryOptions( + boardId, + params, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Delete a task and related records. + * @summary Delete Task + */ +export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse200 = { + data: OkResponse; + status: 200; +}; + +export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponseSuccess = + deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponseError = + deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse = + | deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponseSuccess + | deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponseError; + +export const getDeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteUrl = ( + boardId: string, + taskId: string, +) => { + return `/api/v1/boards/${boardId}/tasks/${taskId}`; +}; + +export const deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete = async ( + boardId: string, + taskId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getDeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteUrl(boardId, taskId), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getDeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; taskId: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { boardId: string; taskId: string }, + TContext +> => { + const mutationKey = ["deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { boardId: string; taskId: string } + > = (props) => { + const { boardId, taskId } = props ?? {}; + + return deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete( + boardId, + taskId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type DeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteMutationResult = + NonNullable< + Awaited> + >; + +export type DeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Delete Task + */ +export const useDeleteTaskApiV1BoardsBoardIdTasksTaskIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { boardId: string; taskId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { boardId: string; taskId: string }, + TContext +> => { + return useMutation( + getDeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteMutationOptions(options), + queryClient, + ); +}; /** * Update task status, assignment, comment, and dependency state. * @summary Update Task @@ -794,132 +920,6 @@ export const useUpdateTaskApiV1BoardsBoardIdTasksTaskIdPatch = < queryClient, ); }; -/** - * Delete a task and related records. - * @summary Delete Task - */ -export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse200 = { - data: OkResponse; - status: 200; -}; - -export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponseSuccess = - deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse200 & { - headers: Headers; - }; -export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponseError = - deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse422 & { - headers: Headers; - }; - -export type deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponse = - | deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponseSuccess - | deleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteResponseError; - -export const getDeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteUrl = ( - boardId: string, - taskId: string, -) => { - return `/api/v1/boards/${boardId}/tasks/${taskId}`; -}; - -export const deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete = async ( - boardId: string, - taskId: string, - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteUrl(boardId, taskId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; taskId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { boardId: string; taskId: string }, - TContext -> => { - const mutationKey = ["deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { boardId: string; taskId: string } - > = (props) => { - const { boardId, taskId } = props ?? {}; - - return deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete( - boardId, - taskId, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteMutationResult = - NonNullable< - Awaited> - >; - -export type DeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteMutationError = - HTTPValidationError; - -/** - * @summary Delete Task - */ -export const useDeleteTaskApiV1BoardsBoardIdTasksTaskIdDelete = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { boardId: string; taskId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { boardId: string; taskId: string }, - TContext -> => { - return useMutation( - getDeleteTaskApiV1BoardsBoardIdTasksTaskIdDeleteMutationOptions(options), - queryClient, - ); -}; /** * List comments for a task in chronological order. * @summary List Task Comments diff --git a/frontend/src/api/generated/users/users.ts b/frontend/src/api/generated/users/users.ts index 825fc351..31c86e07 100644 --- a/frontend/src/api/generated/users/users.ts +++ b/frontend/src/api/generated/users/users.ts @@ -31,6 +31,108 @@ import { customFetch } from "../../mutator"; type SecondParameter unknown> = Parameters[1]; +/** + * Delete the authenticated account and any personal-only organizations. + * @summary Delete Me + */ +export type deleteMeApiV1UsersMeDeleteResponse200 = { + data: OkResponse; + status: 200; +}; + +export type deleteMeApiV1UsersMeDeleteResponseSuccess = + deleteMeApiV1UsersMeDeleteResponse200 & { + headers: Headers; + }; +export type deleteMeApiV1UsersMeDeleteResponse = + deleteMeApiV1UsersMeDeleteResponseSuccess; + +export const getDeleteMeApiV1UsersMeDeleteUrl = () => { + return `/api/v1/users/me`; +}; + +export const deleteMeApiV1UsersMeDelete = async ( + options?: RequestInit, +): Promise => { + return customFetch( + getDeleteMeApiV1UsersMeDeleteUrl(), + { + ...options, + method: "DELETE", + }, + ); +}; + +export const getDeleteMeApiV1UsersMeDeleteMutationOptions = < + TError = unknown, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + void, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + void, + TContext +> => { + const mutationKey = ["deleteMeApiV1UsersMeDelete"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + void + > = () => { + return deleteMeApiV1UsersMeDelete(requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type DeleteMeApiV1UsersMeDeleteMutationResult = NonNullable< + Awaited> +>; + +export type DeleteMeApiV1UsersMeDeleteMutationError = unknown; + +/** + * @summary Delete Me + */ +export const useDeleteMeApiV1UsersMeDelete = < + TError = unknown, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + void, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + void, + TContext +> => { + return useMutation( + getDeleteMeApiV1UsersMeDeleteMutationOptions(options), + queryClient, + ); +}; /** * Return the authenticated user's current profile payload. * @summary Get Me @@ -201,108 +303,6 @@ export function useGetMeApiV1UsersMeGet< return { ...query, queryKey: queryOptions.queryKey }; } -/** - * Delete the authenticated account and any personal-only organizations. - * @summary Delete Me - */ -export type deleteMeApiV1UsersMeDeleteResponse200 = { - data: OkResponse; - status: 200; -}; - -export type deleteMeApiV1UsersMeDeleteResponseSuccess = - deleteMeApiV1UsersMeDeleteResponse200 & { - headers: Headers; - }; -export type deleteMeApiV1UsersMeDeleteResponse = - deleteMeApiV1UsersMeDeleteResponseSuccess; - -export const getDeleteMeApiV1UsersMeDeleteUrl = () => { - return `/api/v1/users/me`; -}; - -export const deleteMeApiV1UsersMeDelete = async ( - options?: RequestInit, -): Promise => { - return customFetch( - getDeleteMeApiV1UsersMeDeleteUrl(), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteMeApiV1UsersMeDeleteMutationOptions = < - TError = unknown, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - void, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - void, - TContext -> => { - const mutationKey = ["deleteMeApiV1UsersMeDelete"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - void - > = () => { - return deleteMeApiV1UsersMeDelete(requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteMeApiV1UsersMeDeleteMutationResult = NonNullable< - Awaited> ->; - -export type DeleteMeApiV1UsersMeDeleteMutationError = unknown; - -/** - * @summary Delete Me - */ -export const useDeleteMeApiV1UsersMeDelete = < - TError = unknown, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - void, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - void, - TContext -> => { - return useMutation( - getDeleteMeApiV1UsersMeDeleteMutationOptions(options), - queryClient, - ); -}; /** * Apply partial profile updates for the authenticated user. * @summary Update Me diff --git a/frontend/src/api/mutator.ts b/frontend/src/api/mutator.ts index 610125f3..0c0037cd 100644 --- a/frontend/src/api/mutator.ts +++ b/frontend/src/api/mutator.ts @@ -1,4 +1,5 @@ import { getLocalAuthToken, isLocalAuthMode } from "@/auth/localAuth"; +import { getApiBaseUrl } from "@/lib/api-base"; type ClerkSession = { getToken: () => Promise; @@ -39,11 +40,7 @@ export const customFetch = async ( url: string, options: RequestInit, ): Promise => { - const rawBaseUrl = process.env.NEXT_PUBLIC_API_URL; - if (!rawBaseUrl) { - throw new Error("NEXT_PUBLIC_API_URL is not set."); - } - const baseUrl = rawBaseUrl.replace(/\/+$/, ""); + const baseUrl = getApiBaseUrl(); const headers = new Headers(options.headers); const hasBody = options.body !== undefined && options.body !== null; diff --git a/frontend/src/app/activity/page.test.tsx b/frontend/src/app/activity/page.test.tsx index 981934c1..cabeeb78 100644 --- a/frontend/src/app/activity/page.test.tsx +++ b/frontend/src/app/activity/page.test.tsx @@ -62,19 +62,34 @@ vi.mock("@clerk/nextjs", () => { describe("/activity auth boundary", () => { it("renders without ClerkProvider runtime errors when publishable key is a placeholder", () => { + const previousAuthMode = process.env.NEXT_PUBLIC_AUTH_MODE; + const previousPublishableKey = + process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + // Simulate CI/secretless env where an arbitrary placeholder value may be present. // AuthProvider should treat this as disabled, and the auth wrappers must not render // Clerk SignedOut/SignedIn components. + process.env.NEXT_PUBLIC_AUTH_MODE = "local"; process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = "placeholder"; + window.sessionStorage.clear(); - render( - - - - - , - ); + try { + render( + + + + + , + ); - expect(screen.getByText(/sign in to view the feed/i)).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: /local authentication/i }), + ).toBeInTheDocument(); + expect(screen.getByLabelText(/access token/i)).toBeInTheDocument(); + } finally { + process.env.NEXT_PUBLIC_AUTH_MODE = previousAuthMode; + process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = previousPublishableKey; + window.sessionStorage.clear(); + } }); }); diff --git a/frontend/src/app/activity/page.tsx b/frontend/src/app/activity/page.tsx index 3a382956..d9d884ac 100644 --- a/frontend/src/app/activity/page.tsx +++ b/frontend/src/app/activity/page.tsx @@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; +import { useSearchParams } from "next/navigation"; import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { Activity as ActivityIcon } from "lucide-react"; @@ -82,14 +83,17 @@ type FeedItem = { created_at: string; event_type: FeedEventType; message: string | null; + source_event_id: string | null; agent_id: string | null; actor_name: string; actor_role: string | null; board_id: string | null; board_name: string | null; + board_href: string | null; task_id: string | null; task_title: string | null; title: string; + context_href: string | null; }; type TaskMeta = { @@ -97,6 +101,10 @@ type TaskMeta = { boardId: string | null; }; +type ActivityRouteParams = Record; + +const ACTIVITY_FEED_PATH = "/activity"; + const TASK_EVENT_TYPES = new Set([ "task.comment", "task.created", @@ -118,6 +126,76 @@ const formatShortTimestamp = (value: string) => { }); }; +const normalizeRouteParams = ( + params: ActivityEventRead["route_params"] | ActivityRouteParams | null | undefined, +): ActivityRouteParams => { + if (!params || typeof params !== "object") return {}; + return Object.entries(params).reduce((acc, [key, value]) => { + if (typeof value === "string" && value.length > 0) { + acc[key] = value; + } + return acc; + }, {}); +}; + +const buildRouteHref = ( + routeName: string | null | undefined, + routeParams: ActivityRouteParams, + fallback: { + eventId: string; + eventType: string; + createdAt: string; + taskId: string | null; + }, +): string => { + if (routeName === "board.approvals") { + const boardId = routeParams.boardId; + if (boardId) { + return `/boards/${encodeURIComponent(boardId)}/approvals`; + } + } + + if (routeName === "board") { + const boardId = routeParams.boardId; + if (boardId) { + const params = new URLSearchParams(); + Object.entries(routeParams).forEach(([key, value]) => { + if (key !== "boardId") params.set(key, value); + }); + const query = params.toString(); + return query + ? `/boards/${encodeURIComponent(boardId)}?${query}` + : `/boards/${encodeURIComponent(boardId)}`; + } + } + + const params = new URLSearchParams( + Object.keys(routeParams).length > 0 + ? routeParams + : { + eventId: fallback.eventId, + eventType: fallback.eventType, + createdAt: fallback.createdAt, + }, + ); + if (fallback.taskId && !params.has("taskId")) { + params.set("taskId", fallback.taskId); + } + return `${ACTIVITY_FEED_PATH}?${params.toString()}`; +}; + +const buildBoardHref = ( + routeParams: ActivityRouteParams, + boardId: string | null, +): string | null => { + const resolved = routeParams.boardId ?? boardId; + if (!resolved) return null; + return `/boards/${encodeURIComponent(resolved)}`; +}; + +const feedItemElementId = (id: string): string => + `activity-item-${id.replace(/[^a-zA-Z0-9_-]/g, "-")}`; + const normalizeAgent = (agent: AgentRead): Agent => ({ ...agent, status: (agent.status ?? "offline").trim() || "offline", @@ -205,26 +283,35 @@ const eventPillClass = (eventType: FeedEventType): string => { return "border-slate-200 bg-slate-100 text-slate-700"; }; -const FeedCard = memo(function FeedCard({ item }: { item: FeedItem }) { +const FeedCard = memo(function FeedCard({ + item, + isHighlighted = false, +}: { + item: FeedItem; + isHighlighted?: boolean; +}) { const message = (item.message ?? "").trim(); const authorAvatar = (item.actor_name[0] ?? "A").toUpperCase(); - const taskHref = - item.board_id && item.task_id - ? `/boards/${item.board_id}?taskId=${item.task_id}` - : null; - const boardHref = item.board_id ? `/boards/${item.board_id}` : null; return ( -
+
{authorAvatar}
- {taskHref ? ( + {item.context_href ? ( {eventLabel(item.event_type)} - {boardHref && item.board_name ? ( + {item.board_href && item.board_name ? ( {item.board_name} @@ -302,7 +389,15 @@ export default function ActivityPage() { }, []); const { isSignedIn } = useAuth(); + const searchParams = useSearchParams(); const isPageActive = usePageActive(); + const selectedEventId = useMemo(() => { + const value = searchParams.get("eventId"); + if (!value) return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; + }, [searchParams]); + const [highlightedFeedItemId, setHighlightedFeedItemId] = useState(null); const membershipQuery = useGetMyMembershipApiV1OrganizationsMeMemberGet< getMyMembershipApiV1OrganizationsMeMemberGetResponse, @@ -397,27 +492,53 @@ export default function ActivityPage() { ); const mapTaskActivity = useCallback( - (event: ActivityEventRead): FeedItem | null => { + ( + event: ActivityEventRead, + fallbackBoardId: string | null = null, + ): FeedItem | null => { if (!isTaskEventType(event.event_type)) return null; const meta = event.task_id ? taskMetaByIdRef.current.get(event.task_id) : null; - const boardId = meta?.boardId ?? null; + const routeName = event.route_name ?? null; + const routeParams = normalizeRouteParams(event.route_params); + const taskId = event.task_id ?? routeParams.taskId ?? null; + const boardId = + meta?.boardId ?? + event.board_id ?? + routeParams.boardId ?? + fallbackBoardId ?? + null; + const fallbackRouteParams: ActivityRouteParams = {}; + if (boardId) fallbackRouteParams.boardId = boardId; + if (taskId) fallbackRouteParams.taskId = taskId; + const effectiveRouteParams = + Object.keys(routeParams).length > 0 ? routeParams : fallbackRouteParams; + const effectiveRouteName = + routeName ?? (boardId ? "board" : "activity"); const author = resolveAuthor(event.agent_id, currentUserDisplayName); return { id: `activity:${event.id}`, created_at: event.created_at, event_type: event.event_type, message: event.message ?? null, + source_event_id: event.id, agent_id: author.id, actor_name: author.name, actor_role: author.role, board_id: boardId, board_name: boardNameForId(boardId), - task_id: event.task_id ?? null, + board_href: buildBoardHref(effectiveRouteParams, boardId), + task_id: taskId, task_title: meta?.title ?? null, title: - meta?.title ?? (event.task_id ? "Unknown task" : "Task activity"), + meta?.title ?? (taskId ? "Unknown task" : "Task activity"), + context_href: buildRouteHref(effectiveRouteName, effectiveRouteParams, { + eventId: event.id, + eventType: event.event_type, + createdAt: event.created_at, + taskId, + }), }; }, [boardNameForId, currentUserDisplayName, resolveAuthor], @@ -429,21 +550,34 @@ export default function ActivityPage() { ? taskMetaByIdRef.current.get(comment.task_id) : null; const boardId = meta?.boardId ?? fallbackBoardId; + const taskId = comment.task_id ?? null; + const routeParams: ActivityRouteParams = {}; + if (boardId) routeParams.boardId = boardId; + if (taskId) routeParams.taskId = taskId; + routeParams.commentId = comment.id; const author = resolveAuthor(comment.agent_id, currentUserDisplayName); return { id: `comment:${comment.id}`, created_at: comment.created_at, event_type: "task.comment", message: comment.message ?? null, + source_event_id: null, agent_id: author.id, actor_name: author.name, actor_role: author.role, board_id: boardId, board_name: boardNameForId(boardId), - task_id: comment.task_id ?? null, + board_href: buildBoardHref(routeParams, boardId), + task_id: taskId, task_title: meta?.title ?? null, title: - meta?.title ?? (comment.task_id ? "Unknown task" : "Task activity"), + meta?.title ?? (taskId ? "Unknown task" : "Task activity"), + context_href: buildRouteHref("board", routeParams, { + eventId: comment.id, + eventType: "task.comment", + createdAt: comment.created_at, + taskId, + }), }; }, [boardNameForId, currentUserDisplayName, resolveAuthor], @@ -496,20 +630,30 @@ export default function ActivityPage() { const taskMeta = approval.task_id ? taskMetaByIdRef.current.get(approval.task_id) : null; + const routeParams: ActivityRouteParams = { boardId }; + const taskId = approval.task_id ?? null; return { id: `approval:${approval.id}:${kind}:${stamp}`, created_at: stamp, event_type: kind, message, + source_event_id: null, agent_id: author.id, actor_name: author.name, actor_role: author.role, board_id: boardId, board_name: boardNameForId(boardId), - task_id: approval.task_id ?? null, + board_href: buildBoardHref(routeParams, boardId), + task_id: taskId, task_title: taskMeta?.title ?? null, title: `Approval · ${action}`, + context_href: buildRouteHref("board.approvals", routeParams, { + eventId: approval.id, + eventType: kind, + createdAt: stamp, + taskId, + }), }; }, [boardNameForId, currentUserDisplayName, resolveAuthor], @@ -523,19 +667,28 @@ export default function ActivityPage() { currentUserDisplayName, ); const command = content.startsWith("/"); + const routeParams: ActivityRouteParams = { boardId, panel: "chat" }; return { id: `chat:${memory.id}`, created_at: memory.created_at, event_type: command ? "board.command" : "board.chat", message: content || null, + source_event_id: null, agent_id: null, actor_name: actorName, actor_role: null, board_id: boardId, board_name: boardNameForId(boardId), + board_href: buildBoardHref(routeParams, boardId), task_id: null, task_title: null, title: command ? "Board command" : "Board chat", + context_href: buildRouteHref("board", routeParams, { + eventId: memory.id, + eventType: command ? "board.command" : "board.chat", + createdAt: memory.created_at, + taskId: null, + }), }; }, [boardNameForId, currentUserDisplayName], @@ -587,20 +740,35 @@ export default function ActivityPage() { : kind === "agent.offline" ? `${agent.name} is offline.` : `${agent.name} updated (${humanizeStatus(nextStatus)}).`; + const boardId = agent.board_id ?? null; + const routeParams: ActivityRouteParams = boardId + ? { boardId } + : {}; return { id: `agent:${agent.id}:${isSnapshot ? "snapshot" : kind}:${stamp}`, created_at: stamp, event_type: kind, message, + source_event_id: null, agent_id: agent.id, actor_name: agent.name, actor_role: roleFromAgent(agent), - board_id: agent.board_id ?? null, - board_name: boardNameForId(agent.board_id), + board_id: boardId, + board_name: boardNameForId(boardId), + board_href: buildBoardHref(routeParams, boardId), task_id: null, task_title: null, title: `Agent · ${agent.name}`, + context_href: + boardId === null + ? null + : buildRouteHref("board", routeParams, { + eventId: agent.id, + eventType: kind, + createdAt: stamp, + taskId: null, + }), }; }, [boardNameForId], @@ -839,12 +1007,8 @@ export default function ActivityPage() { updateTaskMeta(payload.task, boardId); } if (payload.activity) { - const mapped = mapTaskActivity(payload.activity); + const mapped = mapTaskActivity(payload.activity, boardId); if (mapped) { - if (!mapped.board_id) { - mapped.board_id = boardId; - mapped.board_name = boardNameForId(boardId); - } if (!mapped.task_title && payload.task?.title) { mapped.task_title = payload.task.title; mapped.title = payload.task.title; @@ -1285,6 +1449,48 @@ export default function ActivityPage() { }); }, [feedItems]); + const selectedFeedItemId = useMemo(() => { + if (!selectedEventId) return null; + const directMatch = orderedFeed.find( + (item) => item.source_event_id === selectedEventId, + ); + if (directMatch) return directMatch.id; + const fallbackMatch = orderedFeed.find( + (item) => + item.id === selectedEventId || item.id === `activity:${selectedEventId}`, + ); + return fallbackMatch?.id ?? null; + }, [orderedFeed, selectedEventId]); + + useEffect(() => { + if (!selectedFeedItemId) { + setHighlightedFeedItemId(null); + return; + } + + setHighlightedFeedItemId(selectedFeedItemId); + const scrollTimeout = window.setTimeout(() => { + const element = document.getElementById(feedItemElementId(selectedFeedItemId)); + if (!element) return; + element.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 50); + + const clearHighlightTimeout = window.setTimeout(() => { + setHighlightedFeedItemId((current) => + current === selectedFeedItemId ? null : current, + ); + }, 4_000); + + return () => { + window.clearTimeout(scrollTimeout); + window.clearTimeout(clearHighlightTimeout); + }; + }, [selectedFeedItemId]); + + const hasUnresolvedDeepLink = Boolean( + selectedEventId && !selectedFeedItemId && !isFeedLoading && !feedError, + ); + return ( {isMounted ? ( @@ -1321,11 +1527,22 @@ export default function ActivityPage() {
+ {hasUnresolvedDeepLink ? ( +
+ Requested activity item is not in the current feed window yet. +
+ ) : null} } + renderItem={(item) => ( + + )} />
diff --git a/frontend/src/app/agents/[agentId]/edit/page.tsx b/frontend/src/app/agents/[agentId]/edit/page.tsx index 2c1adfef..9fe11dda 100644 --- a/frontend/src/app/agents/[agentId]/edit/page.tsx +++ b/frontend/src/app/agents/[agentId]/edit/page.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { AGENT_EMOJI_OPTIONS } from "@/lib/agent-emoji"; import { DEFAULT_IDENTITY_PROFILE } from "@/lib/agent-templates"; type IdentityProfile = { @@ -39,19 +40,6 @@ type IdentityProfile = { emoji: string; }; -const EMOJI_OPTIONS = [ - { value: ":gear:", label: "Gear", glyph: "⚙️" }, - { value: ":sparkles:", label: "Sparkles", glyph: "✨" }, - { value: ":rocket:", label: "Rocket", glyph: "🚀" }, - { value: ":megaphone:", label: "Megaphone", glyph: "📣" }, - { value: ":chart_with_upwards_trend:", label: "Growth", glyph: "📈" }, - { value: ":bulb:", label: "Idea", glyph: "💡" }, - { value: ":wrench:", label: "Builder", glyph: "🔧" }, - { value: ":shield:", label: "Shield", glyph: "🛡️" }, - { value: ":memo:", label: "Notes", glyph: "📝" }, - { value: ":brain:", label: "Brain", glyph: "🧠" }, -]; - const getBoardOptions = (boards: BoardRead[]): SearchableSelectOption[] => boards.map((board) => ({ value: board.id, @@ -379,7 +367,7 @@ export default function EditAgentPage() { - {EMOJI_OPTIONS.map((option) => ( + {AGENT_EMOJI_OPTIONS.map((option) => ( {option.glyph} {option.label} diff --git a/frontend/src/app/agents/new/page.tsx b/frontend/src/app/agents/new/page.tsx index 271a8910..bec1fd4b 100644 --- a/frontend/src/app/agents/new/page.tsx +++ b/frontend/src/app/agents/new/page.tsx @@ -28,6 +28,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { AGENT_EMOJI_OPTIONS } from "@/lib/agent-emoji"; import { DEFAULT_IDENTITY_PROFILE } from "@/lib/agent-templates"; type IdentityProfile = { @@ -36,19 +37,6 @@ type IdentityProfile = { emoji: string; }; -const EMOJI_OPTIONS = [ - { value: ":gear:", label: "Gear", glyph: "⚙️" }, - { value: ":sparkles:", label: "Sparkles", glyph: "✨" }, - { value: ":rocket:", label: "Rocket", glyph: "🚀" }, - { value: ":megaphone:", label: "Megaphone", glyph: "📣" }, - { value: ":chart_with_upwards_trend:", label: "Growth", glyph: "📈" }, - { value: ":bulb:", label: "Idea", glyph: "💡" }, - { value: ":wrench:", label: "Builder", glyph: "🔧" }, - { value: ":shield:", label: "Shield", glyph: "🛡️" }, - { value: ":memo:", label: "Notes", glyph: "📝" }, - { value: ":brain:", label: "Brain", glyph: "🧠" }, -]; - const getBoardOptions = (boards: BoardRead[]): SearchableSelectOption[] => boards.map((board) => ({ value: board.id, @@ -232,7 +220,7 @@ export default function NewAgentPage() { - {EMOJI_OPTIONS.map((option) => ( + {AGENT_EMOJI_OPTIONS.map((option) => ( {option.glyph} {option.label} diff --git a/frontend/src/app/approvals/page.test.tsx b/frontend/src/app/approvals/page.test.tsx index f747b5bf..1438ba58 100644 --- a/frontend/src/app/approvals/page.test.tsx +++ b/frontend/src/app/approvals/page.test.tsx @@ -59,16 +59,31 @@ vi.mock("@clerk/nextjs", () => { describe("/approvals auth boundary", () => { it("renders without ClerkProvider runtime errors when publishable key is a placeholder", () => { + const previousAuthMode = process.env.NEXT_PUBLIC_AUTH_MODE; + const previousPublishableKey = + process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + + process.env.NEXT_PUBLIC_AUTH_MODE = "local"; process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = "placeholder"; + window.sessionStorage.clear(); - render( - - - - - , - ); + try { + render( + + + + + , + ); - expect(screen.getByText(/sign in to view approvals/i)).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: /local authentication/i }), + ).toBeInTheDocument(); + expect(screen.getByLabelText(/access token/i)).toBeInTheDocument(); + } finally { + process.env.NEXT_PUBLIC_AUTH_MODE = previousAuthMode; + process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = previousPublishableKey; + window.sessionStorage.clear(); + } }); }); diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index 966a0702..b68aa37f 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -291,6 +291,9 @@ export default function EditBoardPage() { const [requireReviewBeforeDone, setRequireReviewBeforeDone] = useState< boolean | undefined >(undefined); + const [commentRequiredForReview, setCommentRequiredForReview] = useState< + boolean | undefined + >(undefined); const [ blockStatusChangesWithPendingApproval, setBlockStatusChangesWithPendingApproval, @@ -504,6 +507,8 @@ export default function EditBoardPage() { requireApprovalForDone ?? baseBoard?.require_approval_for_done ?? true; const resolvedRequireReviewBeforeDone = requireReviewBeforeDone ?? baseBoard?.require_review_before_done ?? false; + const resolvedCommentRequiredForReview = + commentRequiredForReview ?? baseBoard?.comment_required_for_review ?? false; const resolvedBlockStatusChangesWithPendingApproval = blockStatusChangesWithPendingApproval ?? baseBoard?.block_status_changes_with_pending_approval ?? @@ -588,6 +593,7 @@ export default function EditBoardPage() { setObjective(updated.objective ?? ""); setRequireApprovalForDone(updated.require_approval_for_done ?? true); setRequireReviewBeforeDone(updated.require_review_before_done ?? false); + setCommentRequiredForReview(updated.comment_required_for_review ?? false); setBlockStatusChangesWithPendingApproval( updated.block_status_changes_with_pending_approval ?? false, ); @@ -656,6 +662,7 @@ export default function EditBoardPage() { : resolvedObjective.trim() || null, require_approval_for_done: resolvedRequireApprovalForDone, require_review_before_done: resolvedRequireReviewBeforeDone, + comment_required_for_review: resolvedCommentRequiredForReview, block_status_changes_with_pending_approval: resolvedBlockStatusChangesWithPendingApproval, only_lead_can_change_status: resolvedOnlyLeadCanChangeStatus, @@ -1016,6 +1023,42 @@ export default function EditBoardPage() {
+
+ + + + Require comment for review + + + Require a task comment when moving status to{" "} + review. + + +
{gatewayUrlError ? (

{gatewayUrlError}

- ) : gatewayCheckMessage ? ( -

- {gatewayCheckMessage} -

+ ) : gatewayCheckStatus === "error" && gatewayCheckMessage ? ( +

{gatewayCheckMessage}

) : null}
@@ -121,23 +101,80 @@ export function GatewayForm({ onGatewayTokenChange(event.target.value)} - onBlur={onRunGatewayCheck} placeholder="Bearer token" disabled={isLoading} />
+
+
+ + onWorkspaceRootChange(event.target.value)} + placeholder={workspaceRootPlaceholder} + disabled={isLoading} + /> +
+ +
+ + +
+
+
+ - onWorkspaceRootChange(event.target.value)} - placeholder={workspaceRootPlaceholder} - disabled={isLoading} - />
{errorMessage ? ( diff --git a/frontend/src/components/molecules/TaskCard.test.tsx b/frontend/src/components/molecules/TaskCard.test.tsx new file mode 100644 index 00000000..fde0daba --- /dev/null +++ b/frontend/src/components/molecules/TaskCard.test.tsx @@ -0,0 +1,67 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { TaskCard } from "./TaskCard"; + +describe("TaskCard", () => { + it("renders title, assignee, and due date", () => { + render( + , + ); + + expect(screen.getByText("Fix flaky test")).toBeInTheDocument(); + expect(screen.getByText("Zara")).toBeInTheDocument(); + expect(screen.getByText("Feb 11")).toBeInTheDocument(); + expect(screen.getByText("HIGH")).toBeInTheDocument(); + }); + + it("shows blocked state with count", () => { + render( + , + ); + + expect(screen.getByText(/Blocked · 2/i)).toBeInTheDocument(); + }); + + it("shows approvals pending indicator", () => { + render( + , + ); + + expect(screen.getByText(/Approval needed · 3/i)).toBeInTheDocument(); + }); + + it("shows lead review indicator when status is review with no approvals and not blocked", () => { + render(); + + expect(screen.getByText(/Waiting for lead review/i)).toBeInTheDocument(); + }); + + it("invokes onClick for mouse and keyboard activation", () => { + const onClick = vi.fn(); + + render(); + + fireEvent.click(screen.getByRole("button", { name: /Clickable/i })); + expect(onClick).toHaveBeenCalledTimes(1); + + fireEvent.keyDown(screen.getByRole("button", { name: /Clickable/i }), { + key: "Enter", + }); + expect(onClick).toHaveBeenCalledTimes(2); + + fireEvent.keyDown(screen.getByRole("button", { name: /Clickable/i }), { + key: " ", + }); + expect(onClick).toHaveBeenCalledTimes(3); + }); +}); diff --git a/frontend/src/components/organisms/LocalAuthLogin.tsx b/frontend/src/components/organisms/LocalAuthLogin.tsx index fd7ab8c3..a2e7ef36 100644 --- a/frontend/src/components/organisms/LocalAuthLogin.tsx +++ b/frontend/src/components/organisms/LocalAuthLogin.tsx @@ -7,17 +7,18 @@ import { setLocalAuthToken } from "@/auth/localAuth"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import { getApiBaseUrl } from "@/lib/api-base"; const LOCAL_AUTH_TOKEN_MIN_LENGTH = 50; async function validateLocalToken(token: string): Promise { - const rawBaseUrl = process.env.NEXT_PUBLIC_API_URL; - if (!rawBaseUrl) { - return "NEXT_PUBLIC_API_URL is not set."; + let baseUrl: string; + try { + baseUrl = getApiBaseUrl(); + } catch { + return "Unable to resolve backend URL."; } - const baseUrl = rawBaseUrl.replace(/\/+$/, ""); - let response: Response; try { response = await fetch(`${baseUrl}/api/v1/users/me`, { diff --git a/frontend/src/components/organisms/TaskBoard.test.tsx b/frontend/src/components/organisms/TaskBoard.test.tsx index 35fb111f..d4ff977d 100644 --- a/frontend/src/components/organisms/TaskBoard.test.tsx +++ b/frontend/src/components/organisms/TaskBoard.test.tsx @@ -1,8 +1,22 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { fireEvent, render, screen, within } from "@testing-library/react"; +import type { ComponentProps } from "react"; +import { describe, expect, it, vi } from "vitest"; import { TaskBoard } from "./TaskBoard"; +type Task = ComponentProps["tasks"][number]; + +const buildTask = (overrides: Partial = {}): Task => ({ + id: `task-${Math.random().toString(16).slice(2)}`, + title: "Task", + status: "inbox", + priority: "medium", + approvals_pending_count: 0, + blocked_by_task_ids: [], + is_blocked: false, + ...overrides, +}); + describe("TaskBoard", () => { it("uses a mobile-first stacked layout (no horizontal scroll) with responsive kanban columns on larger screens", () => { render( @@ -48,4 +62,160 @@ describe("TaskBoard", () => { // Ensure we didn't accidentally keep unscoped sticky behavior. expect(header?.className).not.toContain("sticky top-0"); }); + + it("renders the 4 columns and shows per-column counts", () => { + const tasks: Task[] = [ + buildTask({ id: "t1", title: "Inbox A", status: "inbox" }), + buildTask({ id: "t2", title: "Doing A", status: "in_progress" }), + buildTask({ id: "t3", title: "Review A", status: "review" }), + buildTask({ id: "t4", title: "Done A", status: "done" }), + buildTask({ id: "t5", title: "Inbox B", status: "inbox" }), + ]; + + render(); + + const inboxHeading = screen.getByRole("heading", { name: "Inbox" }); + const inProgressHeading = screen.getByRole("heading", { + name: "In Progress", + }); + const reviewHeading = screen.getByRole("heading", { name: "Review" }); + const doneHeading = screen.getByRole("heading", { name: "Done" }); + + expect(inboxHeading).toBeInTheDocument(); + expect(inProgressHeading).toBeInTheDocument(); + expect(reviewHeading).toBeInTheDocument(); + expect(doneHeading).toBeInTheDocument(); + + const inboxColumn = inboxHeading.closest(".kanban-column") as HTMLElement | null; + const inProgressColumn = inProgressHeading.closest( + ".kanban-column", + ) as HTMLElement | null; + const reviewColumn = reviewHeading.closest(".kanban-column") as HTMLElement | null; + const doneColumn = doneHeading.closest(".kanban-column") as HTMLElement | null; + expect(inboxColumn).toBeTruthy(); + expect(inProgressColumn).toBeTruthy(); + expect(reviewColumn).toBeTruthy(); + expect(doneColumn).toBeTruthy(); + if (!inboxColumn || !inProgressColumn || !reviewColumn || !doneColumn) return; + + const getColumnCountBadge = (column: HTMLElement) => + column.querySelector( + ".column-header span.h-6.w-6.rounded-full", + ) as HTMLElement | null; + + const inboxCountBadge = getColumnCountBadge(inboxColumn); + const inProgressCountBadge = getColumnCountBadge(inProgressColumn); + const reviewCountBadge = getColumnCountBadge(reviewColumn); + const doneCountBadge = getColumnCountBadge(doneColumn); + + expect(inboxCountBadge).toHaveTextContent("2"); + expect(inProgressCountBadge).toHaveTextContent("1"); + expect(reviewCountBadge).toHaveTextContent("1"); + expect(doneCountBadge).toHaveTextContent("1"); + + expect(screen.getByText("Inbox A")).toBeInTheDocument(); + expect(screen.getByText("Inbox B")).toBeInTheDocument(); + }); + + it("filters the review column by bucket", () => { + const tasks: Task[] = [ + buildTask({ + id: "blocked", + title: "Blocked Review", + status: "review", + is_blocked: true, + blocked_by_task_ids: ["dep-1"], + }), + buildTask({ + id: "approval", + title: "Needs Approval", + status: "review", + approvals_pending_count: 2, + }), + buildTask({ + id: "lead", + title: "Lead Review", + status: "review", + }), + ]; + + render(); + + const reviewHeading = screen.getByRole("heading", { name: "Review" }); + const reviewColumn = reviewHeading.closest(".kanban-column") as HTMLElement | null; + expect(reviewColumn).toBeTruthy(); + if (!reviewColumn) return; + + const header = reviewColumn.querySelector( + ".column-header", + ) as HTMLElement | null; + expect(header).toBeTruthy(); + if (!header) return; + + const headerQueries = within(header); + + expect(headerQueries.getByRole("button", { name: /All · 3/i })).toBeInTheDocument(); + expect( + headerQueries.getByRole("button", { name: /Approval needed · 1/i }), + ).toBeInTheDocument(); + expect( + headerQueries.getByRole("button", { name: /Lead review · 1/i }), + ).toBeInTheDocument(); + expect( + headerQueries.getByRole("button", { name: /Blocked · 1/i }), + ).toBeInTheDocument(); + + fireEvent.click(headerQueries.getByRole("button", { name: /Blocked · 1/i })); + expect(screen.getByText("Blocked Review")).toBeInTheDocument(); + expect(screen.queryByText("Needs Approval")).not.toBeInTheDocument(); + expect(screen.queryByText("Lead Review")).not.toBeInTheDocument(); + + fireEvent.click( + headerQueries.getByRole("button", { name: /Approval needed · 1/i }), + ); + expect(screen.getByText("Needs Approval")).toBeInTheDocument(); + expect(screen.queryByText("Blocked Review")).not.toBeInTheDocument(); + expect(screen.queryByText("Lead Review")).not.toBeInTheDocument(); + + fireEvent.click( + headerQueries.getByRole("button", { name: /Lead review · 1/i }), + ); + expect(screen.getByText("Lead Review")).toBeInTheDocument(); + expect(screen.queryByText("Blocked Review")).not.toBeInTheDocument(); + expect(screen.queryByText("Needs Approval")).not.toBeInTheDocument(); + }); + + it("invokes onTaskMove when a task is dropped onto a different column", () => { + const onTaskMove = vi.fn(); + const tasks: Task[] = [ + buildTask({ id: "t1", title: "Inbox A", status: "inbox" }), + ]; + + render(); + + const dropTarget = screen + .getByRole("heading", { name: "Done" }) + .closest(".kanban-column") as HTMLElement | null; + expect(dropTarget).toBeTruthy(); + if (!dropTarget) return; + + fireEvent.drop(dropTarget, { + dataTransfer: { + getData: () => JSON.stringify({ taskId: "t1", status: "inbox" }), + }, + }); + + expect(onTaskMove).toHaveBeenCalledWith("t1", "done"); + }); + + it("does not allow dragging when readOnly is true", () => { + const tasks: Task[] = [buildTask({ id: "t1", title: "Inbox A" })]; + + render(); + + expect(screen.getByRole("button", { name: /Inbox A/i })).toHaveAttribute( + "draggable", + "false", + ); + }); }); diff --git a/frontend/src/components/organisms/UserMenu.test.tsx b/frontend/src/components/organisms/UserMenu.test.tsx new file mode 100644 index 00000000..10553e38 --- /dev/null +++ b/frontend/src/components/organisms/UserMenu.test.tsx @@ -0,0 +1,98 @@ +import type { + AnchorHTMLAttributes, + ImgHTMLAttributes, + PropsWithChildren, + ReactNode, +} from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { UserMenu } from "./UserMenu"; + +const useUserMock = vi.hoisted(() => vi.fn()); +const clearLocalAuthTokenMock = vi.hoisted(() => vi.fn()); +const isLocalAuthModeMock = vi.hoisted(() => vi.fn()); +type LinkProps = PropsWithChildren<{ + href: string | { pathname?: string }; +}> & + Omit, "href">; + +vi.mock("next/image", () => ({ + default: (props: ImgHTMLAttributes) => ( + // eslint-disable-next-line @next/next/no-img-element + {props.alt + ), +})); + +vi.mock("next/link", () => ({ + default: ({ + children, + href, + ...rest + }: LinkProps) => ( + + {children} + + ), +})); + +vi.mock("@/auth/clerk", () => ({ + useUser: useUserMock, + SignOutButton: ({ children }: { children: ReactNode }) => children, +})); + +vi.mock("@/auth/localAuth", () => ({ + clearLocalAuthToken: clearLocalAuthTokenMock, + isLocalAuthMode: isLocalAuthModeMock, +})); + +describe("UserMenu", () => { + beforeEach(() => { + useUserMock.mockReset(); + clearLocalAuthTokenMock.mockReset(); + isLocalAuthModeMock.mockReset(); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("renders and opens local-mode menu actions", async () => { + const user = userEvent.setup(); + useUserMock.mockReturnValue({ user: null }); + isLocalAuthModeMock.mockReturnValue(true); + + render(); + + await user.click(screen.getByRole("button", { name: /open user menu/i })); + + expect( + screen.getByRole("link", { name: /open boards/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /create board/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /sign out/i }), + ).toBeInTheDocument(); + }); + + it("clears local auth token and reloads on local sign out", async () => { + const user = userEvent.setup(); + useUserMock.mockReturnValue({ user: null }); + isLocalAuthModeMock.mockReturnValue(true); + const reloadSpy = vi.fn(); + vi.stubGlobal("location", { + ...window.location, + reload: reloadSpy, + } as Location); + + render(); + + await user.click(screen.getByRole("button", { name: /open user menu/i })); + await user.click(screen.getByRole("button", { name: /sign out/i })); + + expect(clearLocalAuthTokenMock).toHaveBeenCalledTimes(1); + expect(reloadSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/frontend/src/lib/agent-emoji.ts b/frontend/src/lib/agent-emoji.ts new file mode 100644 index 00000000..9c09a54b --- /dev/null +++ b/frontend/src/lib/agent-emoji.ts @@ -0,0 +1,32 @@ +export type AgentEmojiOption = { + value: string; + label: string; + glyph: string; +}; + +export const AGENT_EMOJI_OPTIONS: readonly AgentEmojiOption[] = [ + { value: ":gear:", label: "Gear", glyph: "⚙️" }, + { value: ":alarm_clock:", label: "Alarm Clock", glyph: "⏰" }, + { value: ":art:", label: "Art", glyph: "🎨" }, + { value: ":brain:", label: "Brain", glyph: "🧠" }, + { value: ":wrench:", label: "Builder", glyph: "🔧" }, + { value: ":dart:", label: "Bullseye", glyph: "🎯" }, + { value: ":computer:", label: "Computer", glyph: "💻" }, + { value: ":chart_with_upwards_trend:", label: "Growth", glyph: "📈" }, + { value: ":bulb:", label: "Idea", glyph: "💡" }, + { value: ":zap:", label: "Lightning", glyph: "⚡" }, + { value: ":lock:", label: "Lock", glyph: "🔒" }, + { value: ":mailbox:", label: "Mailbox", glyph: "📬" }, + { value: ":megaphone:", label: "Megaphone", glyph: "📣" }, + { value: ":memo:", label: "Notes", glyph: "📝" }, + { value: ":owl:", label: "Owl", glyph: "🦉" }, + { value: ":robot:", label: "Robot", glyph: "🤖" }, + { value: ":rocket:", label: "Rocket", glyph: "🚀" }, + { value: ":mag:", label: "Search", glyph: "🔍" }, + { value: ":shield:", label: "Shield", glyph: "🛡️" }, + { value: ":sparkles:", label: "Sparkles", glyph: "✨" }, +]; + +export const AGENT_EMOJI_GLYPHS: Record = Object.fromEntries( + AGENT_EMOJI_OPTIONS.map(({ value, glyph }) => [value, glyph]), +); diff --git a/frontend/src/lib/api-base.test.ts b/frontend/src/lib/api-base.test.ts new file mode 100644 index 00000000..3d6176c0 --- /dev/null +++ b/frontend/src/lib/api-base.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { getApiBaseUrl } from "./api-base"; + +describe("getApiBaseUrl", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("returns normalized explicit URL", () => { + vi.stubEnv("NEXT_PUBLIC_API_URL", "https://api.example.com///"); + + expect(getApiBaseUrl()).toBe("https://api.example.com"); + }); + + it("auto-resolves from browser host when set to auto", () => { + vi.stubEnv("NEXT_PUBLIC_API_URL", "auto"); + + expect(getApiBaseUrl()).toBe("http://localhost:8000"); + }); + + it("auto-resolves from browser host when unset", () => { + vi.stubEnv("NEXT_PUBLIC_API_URL", ""); + + expect(getApiBaseUrl()).toBe("http://localhost:8000"); + }); +}); diff --git a/frontend/src/lib/api-base.ts b/frontend/src/lib/api-base.ts index a0fbea5b..3ba57223 100644 --- a/frontend/src/lib/api-base.ts +++ b/frontend/src/lib/api-base.ts @@ -1,11 +1,22 @@ export function getApiBaseUrl(): string { - const raw = process.env.NEXT_PUBLIC_API_URL; - if (!raw) { - throw new Error("NEXT_PUBLIC_API_URL is not set."); + const raw = process.env.NEXT_PUBLIC_API_URL?.trim(); + if (raw && raw.toLowerCase() !== "auto") { + const normalized = raw.replace(/\/+$/, ""); + if (!normalized) { + throw new Error("NEXT_PUBLIC_API_URL is invalid."); + } + return normalized; } - const normalized = raw.replace(/\/+$/, ""); - if (!normalized) { - throw new Error("NEXT_PUBLIC_API_URL is invalid."); + + if (typeof window !== "undefined") { + const protocol = window.location.protocol === "https:" ? "https" : "http"; + const host = window.location.hostname; + if (host) { + return `${protocol}://${host}:8000`; + } } - return normalized; + + throw new Error( + "NEXT_PUBLIC_API_URL is not set and cannot be auto-resolved outside the browser.", + ); } diff --git a/frontend/src/lib/gateway-form.test.ts b/frontend/src/lib/gateway-form.test.ts new file mode 100644 index 00000000..bca3c16a --- /dev/null +++ b/frontend/src/lib/gateway-form.test.ts @@ -0,0 +1,154 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { gatewaysStatusApiV1GatewaysStatusGet } from "@/api/generated/gateways/gateways"; + +import { checkGatewayConnection, validateGatewayUrl } from "./gateway-form"; + +vi.mock("@/api/generated/gateways/gateways", () => ({ + gatewaysStatusApiV1GatewaysStatusGet: vi.fn(), +})); + +const mockedGatewaysStatusApiV1GatewaysStatusGet = vi.mocked( + gatewaysStatusApiV1GatewaysStatusGet, +); + +describe("validateGatewayUrl", () => { + it("accepts ws:// with explicit non-default port", () => { + expect(validateGatewayUrl("ws://localhost:18789")).toBeNull(); + }); + + it("accepts wss:// with explicit non-default port", () => { + expect(validateGatewayUrl("wss://gateway.example.com:8443")).toBeNull(); + }); + + it("accepts wss:// with explicit default port 443", () => { + expect(validateGatewayUrl("wss://devbot.tailcc2080.ts.net:443")).toBeNull(); + }); + + it("accepts ws:// with explicit default port 80", () => { + expect(validateGatewayUrl("ws://localhost:80")).toBeNull(); + }); + + it("accepts URLs with a path after the port", () => { + expect(validateGatewayUrl("wss://host.example.com:443/gateway")).toBeNull(); + }); + + it("trims surrounding whitespace before validating", () => { + expect(validateGatewayUrl(" wss://host:443 ")).toBeNull(); + }); + + it("accepts IPv6 URLs with explicit non-default port", () => { + expect(validateGatewayUrl("wss://[::1]:8080")).toBeNull(); + }); + + it("accepts IPv6 URLs with explicit default port", () => { + expect(validateGatewayUrl("wss://[2001:db8::1]:443")).toBeNull(); + }); + + it("accepts userinfo URLs with explicit port", () => { + expect( + validateGatewayUrl("ws://user:pass@gateway.example.com:8080"), + ).toBeNull(); + }); + + it("accepts userinfo URLs with IPv6 host and explicit port", () => { + expect(validateGatewayUrl("wss://user@[::1]:443")).toBeNull(); + }); + + it("rejects empty string", () => { + expect(validateGatewayUrl("")).toBe("Gateway URL is required."); + }); + + it("rejects wss:// with no port at all", () => { + expect(validateGatewayUrl("wss://gateway.example.com")).toBe( + "Gateway URL must include an explicit port.", + ); + }); + + it("rejects ws:// with no port at all", () => { + expect(validateGatewayUrl("ws://localhost")).toBe( + "Gateway URL must include an explicit port.", + ); + }); + + it("rejects https:// scheme", () => { + expect(validateGatewayUrl("https://gateway.example.com:443")).toBe( + "Gateway URL must start with ws:// or wss://.", + ); + }); + + it("rejects http:// scheme", () => { + expect(validateGatewayUrl("http://localhost:8080")).toBe( + "Gateway URL must start with ws:// or wss://.", + ); + }); + + it("rejects completely invalid URL", () => { + expect(validateGatewayUrl("not-a-url")).toBe( + "Enter a valid gateway URL including port.", + ); + }); + + it("rejects out-of-range ports", () => { + expect(validateGatewayUrl("wss://gateway.example.com:65536")).toBe( + "Enter a valid gateway URL including port.", + ); + }); + + it("rejects userinfo URLs with no explicit port", () => { + expect(validateGatewayUrl("ws://user:pass@gateway.example.com")).toBe( + "Gateway URL must include an explicit port.", + ); + }); + + it("rejects URL with only whitespace", () => { + expect(validateGatewayUrl(" ")).toBe("Gateway URL is required."); + }); +}); + +describe("checkGatewayConnection", () => { + beforeEach(() => { + mockedGatewaysStatusApiV1GatewaysStatusGet.mockReset(); + }); + + it("passes pairing and TLS toggles to gateway status API", async () => { + mockedGatewaysStatusApiV1GatewaysStatusGet.mockResolvedValue({ + status: 200, + data: { connected: true }, + } as never); + + const result = await checkGatewayConnection({ + gatewayUrl: "ws://gateway.example:18789", + gatewayToken: "secret-token", + gatewayDisableDevicePairing: true, + gatewayAllowInsecureTls: true, + }); + + expect(mockedGatewaysStatusApiV1GatewaysStatusGet).toHaveBeenCalledWith({ + gateway_url: "ws://gateway.example:18789", + gateway_token: "secret-token", + gateway_disable_device_pairing: true, + gateway_allow_insecure_tls: true, + }); + expect(result).toEqual({ ok: true, message: "Gateway reachable." }); + }); + + it("returns gateway-provided error message when offline", async () => { + mockedGatewaysStatusApiV1GatewaysStatusGet.mockResolvedValue({ + status: 200, + data: { + connected: false, + error: "missing required scope", + }, + } as never); + + const result = await checkGatewayConnection({ + gatewayUrl: "ws://gateway.example:18789", + gatewayToken: "", + gatewayDisableDevicePairing: false, + gatewayAllowInsecureTls: false, + }); + + expect(result).toEqual({ ok: false, message: "missing required scope" }); + }); +}); diff --git a/frontend/src/lib/gateway-form.ts b/frontend/src/lib/gateway-form.ts index 5dec8c54..d63f37ea 100644 --- a/frontend/src/lib/gateway-form.ts +++ b/frontend/src/lib/gateway-form.ts @@ -4,6 +4,56 @@ export const DEFAULT_WORKSPACE_ROOT = "~/.openclaw"; export type GatewayCheckStatus = "idle" | "checking" | "success" | "error"; +/** + * Returns true only when the URL string contains an explicit ":port" segment. + * + * JavaScript's URL API sets `.port` to "" for *both* an omitted port and a + * port that equals the scheme's default (e.g. 443 for wss:). We therefore + * inspect the raw host+port token from the URL string instead. + */ +function hasExplicitPort(urlString: string): boolean { + try { + // Extract the authority portion (between // and the first / ? or #) + const withoutScheme = urlString.slice(urlString.indexOf("//") + 2); + const authority = withoutScheme.split(/[/?#]/)[0]; + if (!authority) { + return false; + } + + // authority may be: + // - host[:port] + // - [ipv6][:port] + // - userinfo@host[:port] + // - userinfo@[ipv6][:port] + const atIndex = authority.lastIndexOf("@"); + const hostPort = atIndex === -1 ? authority : authority.slice(atIndex + 1); + + let portSegment = ""; + if (hostPort.startsWith("[")) { + const closingBracketIndex = hostPort.indexOf("]"); + if (closingBracketIndex === -1) { + return false; + } + portSegment = hostPort.slice(closingBracketIndex + 1); + } else { + const lastColonIndex = hostPort.lastIndexOf(":"); + if (lastColonIndex === -1) { + return false; + } + portSegment = hostPort.slice(lastColonIndex); + } + + if (!portSegment.startsWith(":") || !/^:\d+$/.test(portSegment)) { + return false; + } + + const port = Number.parseInt(portSegment.slice(1), 10); + return Number.isInteger(port) && port >= 0 && port <= 65535; + } catch { + return false; + } +} + export const validateGatewayUrl = (value: string) => { const trimmed = value.trim(); if (!trimmed) return "Gateway URL is required."; @@ -12,7 +62,7 @@ export const validateGatewayUrl = (value: string) => { if (url.protocol !== "ws:" && url.protocol !== "wss:") { return "Gateway URL must start with ws:// or wss://."; } - if (!url.port) { + if (!hasExplicitPort(trimmed)) { return "Gateway URL must include an explicit port."; } return null; @@ -24,10 +74,19 @@ export const validateGatewayUrl = (value: string) => { export async function checkGatewayConnection(params: { gatewayUrl: string; gatewayToken: string; + gatewayDisableDevicePairing: boolean; + gatewayAllowInsecureTls: boolean; }): Promise<{ ok: boolean; message: string }> { try { - const requestParams: Record = { + const requestParams: { + gateway_url: string; + gateway_token?: string; + gateway_disable_device_pairing: boolean; + gateway_allow_insecure_tls: boolean; + } = { gateway_url: params.gatewayUrl.trim(), + gateway_disable_device_pairing: params.gatewayDisableDevicePairing, + gateway_allow_insecure_tls: params.gatewayAllowInsecureTls, }; if (params.gatewayToken.trim()) { requestParams.gateway_token = params.gatewayToken.trim(); diff --git a/frontend/vitest.full-coverage.config.ts b/frontend/vitest.full-coverage.config.ts new file mode 100644 index 00000000..59eea1d4 --- /dev/null +++ b/frontend/vitest.full-coverage.config.ts @@ -0,0 +1,37 @@ +import path from "node:path"; + +import { defineConfig } from "vitest/config"; + +// Broader coverage config used for gap analysis. +// - Does NOT enforce 100% thresholds. +// - Includes most source files (excluding generated artifacts and types). +// Keep the default vitest.config.ts as the scoped coverage gate. +export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + test: { + environment: "jsdom", + setupFiles: ["./src/setupTests.ts"], + globals: true, + coverage: { + provider: "v8", + reporter: ["text", "lcov"], + reportsDirectory: "./coverage-full", + include: ["src/**/*.{ts,tsx}"], + exclude: [ + "**/*.d.ts", + "src/**/__generated__/**", + "src/**/generated/**", + ], + thresholds: { + lines: 0, + statements: 0, + functions: 0, + branches: 0, + }, + }, + }, +}); diff --git a/install.sh b/install.sh index 25f93b7d..7291a3ac 100755 --- a/install.sh +++ b/install.sh @@ -3,10 +3,17 @@ set -euo pipefail SCRIPT_NAME="$(basename "$0")" -REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +if [[ "$SCRIPT_NAME" == "bash" || "$SCRIPT_NAME" == "-bash" ]]; then + SCRIPT_NAME="install.sh" +fi +REPO_ROOT="" +REPO_GIT_URL="${OPENCLAW_REPO_URL:-https://github.com/abhi1693/openclaw-mission-control.git}" +REPO_CLONE_REF="${OPENCLAW_REPO_REF:-}" +REPO_DIR_NAME="openclaw-mission-control" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}" LOG_DIR="$STATE_DIR/openclaw-mission-control-install" +PLATFORM="" LINUX_DISTRO="" PKG_MANAGER="" PKG_UPDATED=0 @@ -49,6 +56,66 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } +repo_has_layout() { + local dir="$1" + [[ -f "$dir/Makefile" && -f "$dir/compose.yml" ]] +} + +resolve_script_directory() { + local script_source="" + local script_dir="" + + if [[ -n "${BASH_SOURCE:-}" && -n "${BASH_SOURCE[0]:-}" ]]; then + script_source="${BASH_SOURCE[0]}" + elif [[ -n "${0:-}" && "${0:-}" != "bash" ]]; then + script_source="$0" + fi + + [[ -n "$script_source" ]] || return 1 + + script_dir="$(cd -- "$(dirname -- "$script_source")" 2>/dev/null && pwd -P)" || return 1 + printf '%s\n' "$script_dir" +} + +bootstrap_repo_checkout() { + local target_dir="$PWD/$REPO_DIR_NAME" + + if ! command_exists git; then + die "Git is required for one-line bootstrap installs. Install git and re-run." + fi + if [[ -e "$target_dir" ]]; then + die "Cannot auto-clone into $target_dir because it already exists. Run ./install.sh from that repository or remove the directory." + fi + + info "Repository checkout not found. Cloning into $target_dir ..." + if [[ -n "$REPO_CLONE_REF" ]]; then + git clone --depth 1 --branch "$REPO_CLONE_REF" "$REPO_GIT_URL" "$target_dir" + else + git clone --depth 1 "$REPO_GIT_URL" "$target_dir" + fi + + REPO_ROOT="$target_dir" + SCRIPT_NAME="install.sh" +} + +resolve_repo_root() { + local script_dir="" + + if script_dir="$(resolve_script_directory)"; then + if repo_has_layout "$script_dir"; then + REPO_ROOT="$script_dir" + return + fi + fi + + if repo_has_layout "$PWD"; then + REPO_ROOT="$PWD" + return + fi + + bootstrap_repo_checkout +} + usage() { cat </dev/null || printf '%s' "$target_file")" + if command_exists realpath && realpath --relative-to="$REPO_ROOT" "$target_file" >/dev/null 2>&1; then + display_path="$(realpath --relative-to="$REPO_ROOT" "$target_file" 2>/dev/null)" + else + display_path="$(relative_to "$(cd -- "$(dirname -- "$target_file")" && pwd -P)/$(basename "$target_file")" "$REPO_ROOT")" + fi + info "Created $display_path" } upsert_env_value() { @@ -474,8 +587,26 @@ ensure_nodejs() { die "Cannot continue without Node.js >= 22." fi + if [[ "$PLATFORM" == "darwin" ]]; then + brew upgrade node@22 2>/dev/null || brew install node@22 + if [[ -d "$(brew --prefix node@22 2>/dev/null)/bin" ]]; then + export PATH="$(brew --prefix node@22)/bin:$PATH" + fi + if ! command_exists node || ! command_exists npm; then + die 'Node.js/npm installation failed. Ensure Homebrew bin is in PATH (e.g. eval "$(brew shellenv)").' + fi + hash -r || true + node_version="$(node -v || true)" + node_major="${node_version#v}" + node_major="${node_major%%.*}" + if [[ ! "$node_major" =~ ^[0-9]+$ ]] || ((node_major < 22)); then + die "Detected Node.js $node_version. Node.js >= 22 is required. Install with: brew install node@22 and ensure $(brew --prefix node@22 2>/dev/null || echo '/opt/homebrew/opt/node@22')/bin is in PATH." + fi + return + fi + if [[ "$PKG_MANAGER" != "apt" ]]; then - die "Node.js auto-install is currently implemented for apt-based distros only. Install Node.js >= 22 manually, then rerun installer. Suggested command: $(install_command_hint "$PKG_MANAGER" nodejs npm)" + die "Node.js auto-install is currently implemented for apt-based distros and macOS only. Install Node.js >= 22 manually, then rerun installer. Suggested command: $(install_command_hint "$PKG_MANAGER" nodejs npm)" fi install_packages ca-certificates curl gnupg @@ -505,6 +636,10 @@ ensure_docker() { return fi + if [[ "$PLATFORM" == "darwin" ]]; then + die "Docker and Docker Compose v2 are required on macOS. Install Docker Desktop from https://www.docker.com/products/docker-desktop/, start it, then re-run this script." + fi + info "Docker and Docker Compose v2 are required." if ! confirm "Install Docker tooling now?" "y"; then die "Cannot continue without Docker." @@ -599,8 +734,8 @@ start_local_services() { } ensure_repo_layout() { - [[ -f "$REPO_ROOT/Makefile" ]] || die "Run $SCRIPT_NAME from repository root." - [[ -f "$REPO_ROOT/compose.yml" ]] || die "Missing compose.yml in repository root." + [[ -f "$REPO_ROOT/Makefile" ]] || die "Missing Makefile in expected repository root: $REPO_ROOT" + [[ -f "$REPO_ROOT/compose.yml" ]] || die "Missing compose.yml in expected repository root: $REPO_ROOT" } main() { @@ -615,12 +750,17 @@ main() { local database_url="" local start_services="yes" + resolve_repo_root cd "$REPO_ROOT" ensure_repo_layout parse_args "$@" detect_platform - info "Platform detected: linux ($LINUX_DISTRO)" + if [[ "$PLATFORM" == "darwin" ]]; then + info "Platform detected: darwin (macOS)" + else + info "Platform detected: linux ($LINUX_DISTRO)" + fi if [[ -n "$FORCE_MODE" ]]; then deployment_mode="$FORCE_MODE" @@ -737,6 +877,16 @@ main() { upsert_env_value "$REPO_ROOT/.env" "CORS_ORIGINS" "http://$public_host:$frontend_port" if [[ "$deployment_mode" == "docker" ]]; then + ensure_file_from_example "$REPO_ROOT/backend/.env" "$REPO_ROOT/backend/.env.example" + + # Docker services load backend/.env; ensure required runtime values are populated. + upsert_env_value "$REPO_ROOT/backend/.env" "ENVIRONMENT" "prod" + upsert_env_value "$REPO_ROOT/backend/.env" "AUTH_MODE" "local" + upsert_env_value "$REPO_ROOT/backend/.env" "LOCAL_AUTH_TOKEN" "$local_auth_token" + upsert_env_value "$REPO_ROOT/backend/.env" "CORS_ORIGINS" "http://$public_host:$frontend_port" + upsert_env_value "$REPO_ROOT/backend/.env" "BASE_URL" "http://$public_host:$backend_port" + upsert_env_value "$REPO_ROOT/backend/.env" "DB_AUTO_MIGRATE" "true" + upsert_env_value "$REPO_ROOT/.env" "DB_AUTO_MIGRATE" "true" info "Starting production-like Docker stack..." @@ -825,4 +975,4 @@ Stop local background services: SUMMARY } -main "$@" \ No newline at end of file +main "$@" diff --git a/scripts/rq-docker b/scripts/rq-docker new file mode 100644 index 00000000..05a5b3f4 --- /dev/null +++ b/scripts/rq-docker @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""RQ worker entrypoint for Docker containers.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +# In Docker, the working directory is /app and app code is at /app/app/ +# Add /app to sys.path so we can import app.services.queue_worker +WORKDIR = Path.cwd() +sys.path.insert(0, str(WORKDIR)) + +from app.services.queue_worker import run_worker + + +def cmd_worker(args: argparse.Namespace) -> int: + try: + run_worker() + except KeyboardInterrupt: + return 0 + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="RQ background worker helpers.") + subparsers = parser.add_subparsers(dest="command", required=True) + + worker_parser = subparsers.add_parser( + "worker", + help="Continuously process queued background work.", + ) + worker_parser.set_defaults(func=cmd_worker) + + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + try: + sys.exit(args.func(args)) + except Exception: + # Log unexpected errors before exiting + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main()