diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c7c0e25 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,139 @@ +name: CI + +on: + push: + branches: [main, agentic/*] + pull_request: + branches: [main] + +env: + PYTHON_VERSION: "3.11" + NODE_VERSION: "20" + +# Set minimal permissions for the workflow +permissions: + contents: read + +jobs: + backend-lint-test: + name: Backend - Lint & Test + runs-on: ubuntu-latest + permissions: + contents: read + + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: agentic_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: backend/requirements.txt + + - name: Install dependencies + working-directory: backend + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + working-directory: backend + env: + DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/agentic_test + REDIS_URL: redis://localhost:6379/0 + run: | + pytest tests/ -v --cov=app --cov-report=xml + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + files: backend/coverage.xml + flags: backend + + frontend-lint-build: + name: Frontend - Lint & Build + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Build + working-directory: frontend + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: frontend-build + path: frontend/dist + + docker-build: + name: Docker Build + runs-on: ubuntu-latest + needs: [backend-lint-test, frontend-lint-build] + if: github.event_name == 'push' + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build backend image + uses: docker/build-push-action@v5 + with: + context: ./backend + push: false + tags: agentic-backend:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build frontend image + uses: docker/build-push-action@v5 + with: + context: ./frontend + push: false + tags: agentic-frontend:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e17a8d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,73 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Testing +.coverage +.pytest_cache/ +htmlcov/ +coverage.xml + +# Node.js +node_modules/ +npm-debug.log +yarn-error.log + +# Build outputs +frontend/dist/ +*.tgz + +# Environment +.env +.env.local +.env.*.local + +# Docker +.docker/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Database +*.db +*.sqlite3 + +# Temporary files +tmp/ +temp/ diff --git a/README.md b/README.md index f5a8ce3..ab8c6f3 100644 --- a/README.md +++ b/README.md @@ -1 +1,208 @@ -# rag7 \ No newline at end of file +# Agentic Agent Platform + +A production-ready scaffold for building autonomous agent systems with human oversight capabilities. + +## Features + +- **Modular FastAPI Backend** - Async Python backend with SQLAlchemy 2.0 +- **React Oversight UI** - Real-time dashboard for monitoring and controlling agents +- **Reliable Orchestration** - Ack/retry flow with exponential backoff and escalation +- **Persistent Event Store** - PostgreSQL with Alembic migrations +- **OIDC Authentication & RBAC** - Secure authentication with role-based access control +- **ML Pipeline Integration** - Training job runner and model registry + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Frontend (React) │ +│ ┌─────────────────┐ ┌──────────────────┐ ┌───────────────┐ │ +│ │ Login/Auth Flow │ │ Oversight │ │ RBAC-aware │ │ +│ │ (OIDC support) │ │ Dashboard │ │ UI Controls │ │ +│ └─────────────────┘ └──────────────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ REST API + WebSocket +┌─────────────────────────────────────────────────────────────────┐ +│ Backend (FastAPI) │ +│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐│ +│ │ Auth API │ │ Decisions API│ │ Admin API ││ +│ │ (JWT/OIDC) │ │ (Tasks,Events│ │ (Agents, Models, Jobs) ││ +│ └──────────────┘ └──────────────┘ └────────────────────────┘│ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Agent Systems │ │ +│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────┐ │ │ +│ │ │Communication│ │ Decision │ │ Delegation │ │Learning│ │ │ +│ │ │ (Ack/Retry) │ │ System │ │ System │ │ System │ │ │ +│ │ └────────────┘ └────────────┘ └────────────┘ └────────┘ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ │ │ + ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ + │PostgreSQL│ │ Redis │ │ Celery │ + │(Events, │ │(Pub/Sub │ │(Training│ + │ Tasks) │ │ Acks) │ │ Jobs) │ + └─────────┘ └─────────┘ └─────────┘ +``` + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose +- Python 3.11+ (for local development) +- Node.js 20+ (for local development) + +### Running with Docker Compose + +```bash +# Clone the repository +git clone https://github.com/Stacey77/rag7.git +cd rag7 + +# Start all services +docker-compose up --build + +# Access the UI at http://localhost:3000 +# API available at http://localhost:8000 +``` + +### Local Development + +#### Backend + +```bash +cd backend + +# Create virtual environment +python -m venv venv +source venv/bin/activate # or `venv\Scripts\activate` on Windows + +# Install dependencies +pip install -r requirements.txt + +# Set environment variables +export DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/agentic +export REDIS_URL=redis://localhost:6379/0 + +# Run migrations +alembic upgrade head + +# Start the server +uvicorn app.main:app --reload +``` + +#### Frontend + +```bash +cd frontend + +# Install dependencies +npm install + +# Start development server +npm run dev +``` + +## Environment Variables + +### Backend + +| Variable | Description | Default | +|----------|-------------|---------| +| `DATABASE_URL` | PostgreSQL connection URL | `postgresql+asyncpg://postgres:postgres@localhost:5432/agentic` | +| `REDIS_URL` | Redis connection URL | `redis://localhost:6379/0` | +| `JWT_SECRET_KEY` | Secret key for JWT signing | `dev-secret-key-change-in-production` | +| `OIDC_ISSUER` | OIDC provider issuer URL | - | +| `OIDC_CLIENT_ID` | OIDC client ID | - | +| `OIDC_CLIENT_SECRET` | OIDC client secret | - | +| `TASK_ACK_TIMEOUT_SECONDS` | Task acknowledgement timeout | `30` | +| `TASK_MAX_RETRIES` | Maximum retry attempts | `3` | + +## API Endpoints + +### Authentication + +- `POST /api/v1/auth/token` - Login with username/password +- `GET /api/v1/auth/me` - Get current user profile +- `GET /api/v1/auth/oidc/config` - Get OIDC configuration +- `POST /api/v1/auth/oidc/callback` - OIDC callback handler + +### Decisions + +- `POST /api/v1/decisions/task` - Create a new task +- `GET /api/v1/decisions/task/{id}` - Get task by ID +- `GET /api/v1/decisions/tasks` - List tasks +- `PATCH /api/v1/decisions/task/{id}/state` - Update task state +- `POST /api/v1/decisions/task/{id}/override` - Override decision (reviewer+) +- `POST /api/v1/decisions/task/{id}/escalate` - Escalate task +- `GET /api/v1/decisions/events` - Get events +- `GET /api/v1/decisions/escalations` - Get escalated tasks + +### Admin + +- `POST /api/v1/admin/agents` - Create agent +- `GET /api/v1/admin/agents` - List agents +- `POST /api/v1/admin/models` - Register model +- `GET /api/v1/admin/models` - List models +- `POST /api/v1/admin/training-jobs` - Create training job +- `GET /api/v1/admin/audits` - Get audit log + +### WebSocket + +- `WS /api/v1/oversight/ws?token={jwt}` - Real-time oversight stream + +## RBAC Roles + +| Role | Permissions | +|------|-------------| +| `admin` | Full access to all endpoints | +| `reviewer` | Override decisions, view escalations | +| `agent_manager` | Manage agents, create tasks, view stats | +| `viewer` | Read-only access to tasks and events | + +## Task State Machine + +``` +queued → assigned → acked → in_progress → completed → verified + ↓ ↓ + (timeout) (failure) + ↓ ↓ + retry (N times) → escalated +``` + +## Database Schema + +The platform uses the following tables: + +- `events` - Event store for all system events +- `tasks` - Task definitions and state +- `agents` - Agent configurations +- `audits` - Audit log for administrative actions +- `feedback` - Feedback for agent learning +- `models` - Model registry +- `training_jobs` - Training job tracking + +## Testing + +```bash +cd backend + +# Run tests +pytest tests/ -v + +# Run with coverage +pytest tests/ --cov=app --cov-report=html +``` + +## Next Steps + +- [ ] Harden authentication (refresh tokens, session management) +- [ ] Add OpenTelemetry for observability +- [ ] Kubernetes manifests for production deployment +- [ ] Implement actual ML training pipeline with Celery +- [ ] Add rate limiting and API throttling +- [ ] Implement agent health monitoring + +## License + +MIT \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..335a072 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,33 @@ +# Agentic Agent Platform Backend + +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create non-root user +RUN adduser --disabled-password --gecos '' appuser && \ + chown -R appuser:appuser /app +USER appuser + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD python -c "import httpx; httpx.get('http://localhost:8000/health')" || exit 1 + +# Run application +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..f3b4643 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,87 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version path separator; As of alembic 1.10, this is the default. +version_path_separator = os + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/agentic + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -q + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..a4602f0 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,95 @@ +"""Alembic environment configuration for async migrations.""" + +import asyncio +from logging.config import fileConfig +import os +import sys + +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from alembic import context + +# Add the backend directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Import models and base +from app.db.base import Base +from app.db import models # noqa: F401 - import for side effects + +# this is the Alembic Config object +config = context.config + +# Interpret the config file for Python logging. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here for 'autogenerate' support +target_metadata = Base.metadata + +# Get database URL from environment or config +def get_url(): + return os.getenv( + "DATABASE_URL", + config.get_main_option("sqlalchemy.url") + ) + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + """ + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + """Run migrations with connection.""" + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """Run migrations in 'online' mode with async engine.""" + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = get_url() + + connectable = async_engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/0001_initial.py b/backend/alembic/versions/0001_initial.py new file mode 100644 index 0000000..05e03de --- /dev/null +++ b/backend/alembic/versions/0001_initial.py @@ -0,0 +1,123 @@ +"""Initial migration - create all tables + +Revision ID: 0001_initial +Revises: +Create Date: 2024-01-01 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0001_initial' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create agents table first (referenced by other tables) + op.create_table( + 'agents', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('name', sa.String(100), nullable=False, unique=True), + sa.Column('agent_type', sa.String(50), nullable=False), + sa.Column('status', sa.String(20), nullable=False, server_default='available'), + sa.Column('capabilities', sa.JSON(), server_default='[]'), + sa.Column('config', sa.JSON(), server_default='{}'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + # Create tasks table + op.create_table( + 'tasks', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('title', sa.String(255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('task_type', sa.String(50), nullable=False, server_default='default'), + sa.Column('priority', sa.String(20), nullable=False, server_default='medium'), + sa.Column('state', sa.String(20), nullable=False, server_default='queued', index=True), + sa.Column('payload', sa.JSON(), server_default='{}'), + sa.Column('assigned_agent_id', sa.String(36), sa.ForeignKey('agents.id'), nullable=True), + sa.Column('retry_count', sa.Integer(), server_default='0'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + # Create events table + op.create_table( + 'events', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('event_type', sa.String(100), nullable=False, index=True), + sa.Column('entity_type', sa.String(50), nullable=False, index=True), + sa.Column('entity_id', sa.String(36), nullable=False, index=True), + sa.Column('data', sa.JSON(), server_default='{}'), + sa.Column('user_id', sa.String(100), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + # Create audits table + op.create_table( + 'audits', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('action', sa.String(100), nullable=False, index=True), + sa.Column('entity_type', sa.String(50), nullable=False, index=True), + sa.Column('entity_id', sa.String(36), nullable=False), + sa.Column('user_id', sa.String(100), nullable=False, index=True), + sa.Column('details', sa.JSON(), server_default='{}'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + # Create feedback table + op.create_table( + 'feedback', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('task_id', sa.String(36), sa.ForeignKey('tasks.id'), nullable=False), + sa.Column('agent_id', sa.String(36), sa.ForeignKey('agents.id'), nullable=False), + sa.Column('feedback_type', sa.String(50), nullable=False), + sa.Column('content', sa.JSON(), server_default='{}'), + sa.Column('source_user_id', sa.String(100), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + # Create training_jobs table + op.create_table( + 'training_jobs', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('model_name', sa.String(100), nullable=False), + sa.Column('agent_id', sa.String(36), sa.ForeignKey('agents.id'), nullable=False), + sa.Column('status', sa.String(20), nullable=False, server_default='pending', index=True), + sa.Column('config', sa.JSON(), server_default='{}'), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + ) + + # Create models table (model registry) + op.create_table( + 'models', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('name', sa.String(100), nullable=False, index=True), + sa.Column('version', sa.String(50), nullable=False), + sa.Column('agent_id', sa.String(36), sa.ForeignKey('agents.id'), nullable=False), + sa.Column('training_job_id', sa.String(36), sa.ForeignKey('training_jobs.id'), nullable=True), + sa.Column('metrics', sa.JSON(), server_default='{}'), + sa.Column('artifact_path', sa.String(500), nullable=True), + sa.Column('is_active', sa.Boolean(), server_default='false'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table('models') + op.drop_table('training_jobs') + op.drop_table('feedback') + op.drop_table('audits') + op.drop_table('events') + op.drop_table('tasks') + op.drop_table('agents') diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..dd75d0e --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +# Agentic Agent Platform Backend diff --git a/backend/app/agents/__init__.py b/backend/app/agents/__init__.py new file mode 100644 index 0000000..cb959ec --- /dev/null +++ b/backend/app/agents/__init__.py @@ -0,0 +1 @@ +# Agents module diff --git a/backend/app/agents/communication.py b/backend/app/agents/communication.py new file mode 100644 index 0000000..f8ba77a --- /dev/null +++ b/backend/app/agents/communication.py @@ -0,0 +1,292 @@ +"""Agent Communication System with ack flow, timeouts, and retries.""" + +import asyncio +import json +import uuid +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Callable, Optional +import redis.asyncio as redis + +from app.core import get_settings + + +settings = get_settings() + + +class TaskState(str, Enum): + """Task state machine states.""" + QUEUED = "queued" + ASSIGNED = "assigned" + ACKED = "acked" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + VERIFIED = "verified" + FAILED = "failed" + ESCALATED = "escalated" + + +class AgentMessage: + """Message structure for agent communication.""" + + def __init__( + self, + message_id: str, + task_id: str, + message_type: str, + payload: dict, + sender_id: str, + recipient_id: Optional[str] = None, + correlation_id: Optional[str] = None, + timestamp: Optional[datetime] = None + ): + self.message_id = message_id + self.task_id = task_id + self.message_type = message_type + self.payload = payload + self.sender_id = sender_id + self.recipient_id = recipient_id + self.correlation_id = correlation_id or message_id + self.timestamp = timestamp or datetime.now(timezone.utc) + + def to_dict(self) -> dict: + return { + "message_id": self.message_id, + "task_id": self.task_id, + "message_type": self.message_type, + "payload": self.payload, + "sender_id": self.sender_id, + "recipient_id": self.recipient_id, + "correlation_id": self.correlation_id, + "timestamp": self.timestamp.isoformat() + } + + @classmethod + def from_dict(cls, data: dict) -> "AgentMessage": + return cls( + message_id=data["message_id"], + task_id=data["task_id"], + message_type=data["message_type"], + payload=data["payload"], + sender_id=data["sender_id"], + recipient_id=data.get("recipient_id"), + correlation_id=data.get("correlation_id"), + timestamp=datetime.fromisoformat(data["timestamp"]) if data.get("timestamp") else None + ) + + +class AgentCommunicationSystem: + """ + Agent communication system with ack protocol over Redis pub/sub. + + Features: + - Publish tasks with configurable timeout + - Wait for acknowledgement with retry logic + - Exponential backoff for retries + - Escalation after threshold failures + """ + + TASK_CHANNEL = "agentic:tasks" + ACK_CHANNEL = "agentic:acks" + ESCALATION_CHANNEL = "agentic:escalations" + + def __init__(self, redis_client: redis.Redis): + self.redis = redis_client + self.pending_acks: dict[str, asyncio.Event] = {} + self.ack_results: dict[str, dict] = {} + self._subscriber_task: Optional[asyncio.Task] = None + + async def start(self): + """Start the ack listener.""" + self._subscriber_task = asyncio.create_task(self._ack_listener()) + + async def stop(self): + """Stop the ack listener.""" + if self._subscriber_task: + self._subscriber_task.cancel() + try: + await self._subscriber_task + except asyncio.CancelledError: + pass + + async def _ack_listener(self): + """Listen for acknowledgements on the ack channel.""" + pubsub = self.redis.pubsub() + await pubsub.subscribe(self.ACK_CHANNEL) + + try: + async for message in pubsub.listen(): + if message["type"] == "message": + data = json.loads(message["data"]) + correlation_id = data.get("correlation_id") + + if correlation_id and correlation_id in self.pending_acks: + self.ack_results[correlation_id] = data + self.pending_acks[correlation_id].set() + except asyncio.CancelledError: + pass + finally: + await pubsub.unsubscribe(self.ACK_CHANNEL) + await pubsub.close() + + async def publish_task( + self, + task_id: str, + payload: dict, + sender_id: str = "orchestrator", + recipient_id: Optional[str] = None, + timeout: Optional[int] = None, + max_retries: Optional[int] = None, + on_state_change: Optional[Callable[[str, TaskState], Any]] = None + ) -> tuple[bool, Optional[dict], int]: + """ + Publish a task and wait for acknowledgement. + + Returns: + Tuple of (success, ack_data, retry_count) + """ + timeout = timeout or settings.TASK_ACK_TIMEOUT_SECONDS + max_retries = max_retries or settings.TASK_MAX_RETRIES + + message = AgentMessage( + message_id=str(uuid.uuid4()), + task_id=task_id, + message_type="task_assignment", + payload=payload, + sender_id=sender_id, + recipient_id=recipient_id + ) + + retry_count = 0 + + while retry_count <= max_retries: + # Update state + if on_state_change: + state = TaskState.QUEUED if retry_count == 0 else TaskState.ASSIGNED + await on_state_change(task_id, state) + + # Create ack event + correlation_id = message.correlation_id + self.pending_acks[correlation_id] = asyncio.Event() + + # Publish task + await self.redis.publish(self.TASK_CHANNEL, json.dumps(message.to_dict())) + + if on_state_change: + await on_state_change(task_id, TaskState.ASSIGNED) + + # Wait for ack with timeout + try: + backoff = settings.TASK_BACKOFF_BASE ** retry_count + actual_timeout = timeout * backoff + + await asyncio.wait_for( + self.pending_acks[correlation_id].wait(), + timeout=actual_timeout + ) + + # Ack received + ack_data = self.ack_results.pop(correlation_id, None) + del self.pending_acks[correlation_id] + + if on_state_change: + await on_state_change(task_id, TaskState.ACKED) + + return True, ack_data, retry_count + + except asyncio.TimeoutError: + # Timeout - retry with backoff + del self.pending_acks[correlation_id] + retry_count += 1 + + if retry_count <= max_retries: + # Generate new correlation ID for retry + message.correlation_id = str(uuid.uuid4()) + + # All retries exhausted - escalate + if on_state_change: + await on_state_change(task_id, TaskState.ESCALATED) + + await self._escalate_task(task_id, message, retry_count) + + return False, None, retry_count + + async def send_ack( + self, + correlation_id: str, + task_id: str, + agent_id: str, + status: str = "acked", + metadata: Optional[dict] = None + ): + """Send acknowledgement for a task.""" + ack_message = { + "correlation_id": correlation_id, + "task_id": task_id, + "agent_id": agent_id, + "status": status, + "metadata": metadata or {}, + "timestamp": datetime.now(timezone.utc).isoformat() + } + await self.redis.publish(self.ACK_CHANNEL, json.dumps(ack_message)) + + async def _escalate_task( + self, + task_id: str, + original_message: AgentMessage, + retry_count: int + ): + """Escalate task to human review queue.""" + escalation_message = { + "task_id": task_id, + "original_message": original_message.to_dict(), + "retry_count": retry_count, + "escalation_reason": "max_retries_exceeded", + "timestamp": datetime.now(timezone.utc).isoformat() + } + + # Publish to escalation channel + await self.redis.publish( + self.ESCALATION_CHANNEL, + json.dumps(escalation_message) + ) + + # Also add to a persistent escalation queue + await self.redis.lpush( + "agentic:escalation_queue", + json.dumps(escalation_message) + ) + + async def subscribe_to_tasks( + self, + handler: Callable[[AgentMessage], Any] + ) -> asyncio.Task: + """Subscribe to task channel and handle incoming tasks.""" + async def _listener(): + pubsub = self.redis.pubsub() + await pubsub.subscribe(self.TASK_CHANNEL) + + try: + async for message in pubsub.listen(): + if message["type"] == "message": + data = json.loads(message["data"]) + agent_message = AgentMessage.from_dict(data) + await handler(agent_message) + except asyncio.CancelledError: + pass + finally: + await pubsub.unsubscribe(self.TASK_CHANNEL) + await pubsub.close() + + return asyncio.create_task(_listener()) + + async def get_escalated_tasks(self, count: int = 10) -> list[dict]: + """Get escalated tasks from the queue.""" + tasks = [] + for _ in range(count): + task = await self.redis.rpop("agentic:escalation_queue") + if task: + tasks.append(json.loads(task)) + else: + break + return tasks diff --git a/backend/app/agents/decision.py b/backend/app/agents/decision.py new file mode 100644 index 0000000..6468796 --- /dev/null +++ b/backend/app/agents/decision.py @@ -0,0 +1,199 @@ +"""Agent Decision System for task processing and decision making.""" + +import uuid +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Optional +import redis.asyncio as redis + +from app.agents.communication import TaskState, AgentCommunicationSystem + + +class DecisionType(str, Enum): + """Types of agent decisions.""" + TASK_ACCEPT = "task_accept" + TASK_REJECT = "task_reject" + TASK_DELEGATE = "task_delegate" + TASK_COMPLETE = "task_complete" + TASK_FAIL = "task_fail" + HUMAN_OVERRIDE = "human_override" + AUTO_ESCALATE = "auto_escalate" + + +class ConfidenceLevel(str, Enum): + """Confidence levels for decisions.""" + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + UNCERTAIN = "uncertain" + + +class AgentDecision: + """Represents a decision made by an agent.""" + + def __init__( + self, + decision_id: str, + task_id: str, + agent_id: str, + decision_type: DecisionType, + confidence: ConfidenceLevel, + rationale: str, + metadata: Optional[dict] = None, + timestamp: Optional[datetime] = None + ): + self.decision_id = decision_id + self.task_id = task_id + self.agent_id = agent_id + self.decision_type = decision_type + self.confidence = confidence + self.rationale = rationale + self.metadata = metadata or {} + self.timestamp = timestamp or datetime.now(timezone.utc) + + def to_dict(self) -> dict: + return { + "decision_id": self.decision_id, + "task_id": self.task_id, + "agent_id": self.agent_id, + "decision_type": self.decision_type.value, + "confidence": self.confidence.value, + "rationale": self.rationale, + "metadata": self.metadata, + "timestamp": self.timestamp.isoformat() + } + + +class AgentDecisionSystem: + """ + Decision system for autonomous agents. + + Features: + - Process incoming tasks and make decisions + - Track decision history + - Support human override and escalation + - Integrate with confidence thresholds + """ + + CONFIDENCE_THRESHOLD = ConfidenceLevel.MEDIUM + + def __init__( + self, + agent_id: str, + communication_system: AgentCommunicationSystem + ): + self.agent_id = agent_id + self.comm_system = communication_system + self.decision_history: list[AgentDecision] = [] + + async def process_task( + self, + task_id: str, + payload: dict, + correlation_id: str + ) -> AgentDecision: + """ + Process an incoming task and make a decision. + + This is a placeholder implementation that should be overridden + with actual task processing logic. + """ + # Evaluate task and determine confidence + confidence = await self._evaluate_confidence(payload) + + # Make decision based on confidence + if confidence in [ConfidenceLevel.HIGH, ConfidenceLevel.MEDIUM]: + decision_type = DecisionType.TASK_ACCEPT + rationale = "Task accepted based on confidence evaluation" + else: + decision_type = DecisionType.AUTO_ESCALATE + rationale = "Low confidence - escalating for review" + + decision = AgentDecision( + decision_id=str(uuid.uuid4()), + task_id=task_id, + agent_id=self.agent_id, + decision_type=decision_type, + confidence=confidence, + rationale=rationale, + metadata={"correlation_id": correlation_id} + ) + + self.decision_history.append(decision) + + # Send ack if accepting + if decision_type == DecisionType.TASK_ACCEPT: + await self.comm_system.send_ack( + correlation_id=correlation_id, + task_id=task_id, + agent_id=self.agent_id, + status="accepted", + metadata=decision.to_dict() + ) + + return decision + + async def _evaluate_confidence(self, payload: dict) -> ConfidenceLevel: + """ + Evaluate confidence level for processing a task. + + This is a placeholder - real implementation would use ML models. + """ + # Simple heuristic for demo purposes + if payload.get("priority") == "high": + return ConfidenceLevel.MEDIUM + elif payload.get("type") in ["simple", "routine"]: + return ConfidenceLevel.HIGH + else: + return ConfidenceLevel.LOW + + async def complete_task( + self, + task_id: str, + result: Any, + success: bool = True + ) -> AgentDecision: + """Mark a task as completed.""" + decision = AgentDecision( + decision_id=str(uuid.uuid4()), + task_id=task_id, + agent_id=self.agent_id, + decision_type=DecisionType.TASK_COMPLETE if success else DecisionType.TASK_FAIL, + confidence=ConfidenceLevel.HIGH, + rationale="Task processing completed", + metadata={"result": result, "success": success} + ) + + self.decision_history.append(decision) + return decision + + async def request_human_override( + self, + task_id: str, + reason: str, + options: Optional[list[dict]] = None + ) -> AgentDecision: + """Request human override for a decision.""" + decision = AgentDecision( + decision_id=str(uuid.uuid4()), + task_id=task_id, + agent_id=self.agent_id, + decision_type=DecisionType.HUMAN_OVERRIDE, + confidence=ConfidenceLevel.UNCERTAIN, + rationale=reason, + metadata={"options": options or [], "requires_human": True} + ) + + self.decision_history.append(decision) + return decision + + def get_decision_history( + self, + task_id: Optional[str] = None, + limit: int = 100 + ) -> list[AgentDecision]: + """Get decision history, optionally filtered by task.""" + history = self.decision_history + if task_id: + history = [d for d in history if d.task_id == task_id] + return history[-limit:] diff --git a/backend/app/agents/delegation.py b/backend/app/agents/delegation.py new file mode 100644 index 0000000..9968817 --- /dev/null +++ b/backend/app/agents/delegation.py @@ -0,0 +1,227 @@ +"""Agent Delegation System for task routing and load balancing.""" + +import uuid +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Dict, List, Optional +import redis.asyncio as redis + +from app.agents.communication import AgentCommunicationSystem, TaskState + + +class AgentCapability(str, Enum): + """Agent capabilities for task routing.""" + TEXT_PROCESSING = "text_processing" + CODE_ANALYSIS = "code_analysis" + DATA_EXTRACTION = "data_extraction" + DECISION_MAKING = "decision_making" + HUMAN_INTERACTION = "human_interaction" + ML_INFERENCE = "ml_inference" + + +class AgentStatus(str, Enum): + """Agent operational status.""" + AVAILABLE = "available" + BUSY = "busy" + OFFLINE = "offline" + MAINTENANCE = "maintenance" + + +class AgentProfile: + """Profile describing an agent's capabilities and status.""" + + def __init__( + self, + agent_id: str, + name: str, + capabilities: List[AgentCapability], + max_concurrent_tasks: int = 5, + priority_level: int = 1, + status: AgentStatus = AgentStatus.AVAILABLE + ): + self.agent_id = agent_id + self.name = name + self.capabilities = capabilities + self.max_concurrent_tasks = max_concurrent_tasks + self.priority_level = priority_level + self.status = status + self.current_tasks: List[str] = [] + self.metrics = { + "tasks_completed": 0, + "tasks_failed": 0, + "avg_completion_time": 0.0 + } + + @property + def is_available(self) -> bool: + return ( + self.status == AgentStatus.AVAILABLE and + len(self.current_tasks) < self.max_concurrent_tasks + ) + + def to_dict(self) -> dict: + return { + "agent_id": self.agent_id, + "name": self.name, + "capabilities": [c.value for c in self.capabilities], + "max_concurrent_tasks": self.max_concurrent_tasks, + "priority_level": self.priority_level, + "status": self.status.value, + "current_tasks": self.current_tasks, + "metrics": self.metrics + } + + +class DelegationStrategy(str, Enum): + """Task delegation strategies.""" + ROUND_ROBIN = "round_robin" + LEAST_LOADED = "least_loaded" + CAPABILITY_MATCH = "capability_match" + PRIORITY_BASED = "priority_based" + + +class AgentDelegationSystem: + """ + Delegation system for routing tasks to appropriate agents. + + Features: + - Register and track agent profiles + - Route tasks based on capabilities and load + - Support multiple delegation strategies + - Track task assignments + """ + + def __init__( + self, + communication_system: AgentCommunicationSystem, + strategy: DelegationStrategy = DelegationStrategy.CAPABILITY_MATCH + ): + self.comm_system = communication_system + self.strategy = strategy + self.agents: Dict[str, AgentProfile] = {} + self._round_robin_index = 0 + + def register_agent(self, profile: AgentProfile) -> None: + """Register an agent with the delegation system.""" + self.agents[profile.agent_id] = profile + + def unregister_agent(self, agent_id: str) -> None: + """Unregister an agent from the delegation system.""" + if agent_id in self.agents: + del self.agents[agent_id] + + def update_agent_status(self, agent_id: str, status: AgentStatus) -> None: + """Update an agent's status.""" + if agent_id in self.agents: + self.agents[agent_id].status = status + + async def delegate_task( + self, + task_id: str, + payload: dict, + required_capabilities: Optional[List[AgentCapability]] = None + ) -> Optional[str]: + """ + Delegate a task to an appropriate agent. + + Returns: + Agent ID if delegation successful, None otherwise + """ + # Find suitable agents + candidates = self._find_suitable_agents(required_capabilities) + + if not candidates: + return None + + # Select agent based on strategy + selected_agent = self._select_agent(candidates) + + if not selected_agent: + return None + + # Assign task to agent + selected_agent.current_tasks.append(task_id) + + # Publish task with recipient + success, ack_data, retry_count = await self.comm_system.publish_task( + task_id=task_id, + payload=payload, + recipient_id=selected_agent.agent_id + ) + + if success: + return selected_agent.agent_id + else: + # Remove task from agent if not acked + if task_id in selected_agent.current_tasks: + selected_agent.current_tasks.remove(task_id) + return None + + def _find_suitable_agents( + self, + required_capabilities: Optional[List[AgentCapability]] + ) -> List[AgentProfile]: + """Find agents that can handle the task.""" + candidates = [] + + for agent in self.agents.values(): + if not agent.is_available: + continue + + if required_capabilities: + # Check if agent has all required capabilities + if all(cap in agent.capabilities for cap in required_capabilities): + candidates.append(agent) + else: + candidates.append(agent) + + return candidates + + def _select_agent( + self, + candidates: List[AgentProfile] + ) -> Optional[AgentProfile]: + """Select an agent based on the delegation strategy.""" + if not candidates: + return None + + if self.strategy == DelegationStrategy.ROUND_ROBIN: + self._round_robin_index = (self._round_robin_index + 1) % len(candidates) + return candidates[self._round_robin_index] + + elif self.strategy == DelegationStrategy.LEAST_LOADED: + return min(candidates, key=lambda a: len(a.current_tasks)) + + elif self.strategy == DelegationStrategy.PRIORITY_BASED: + return max(candidates, key=lambda a: a.priority_level) + + else: # CAPABILITY_MATCH - default to first match + return candidates[0] + + def complete_task(self, agent_id: str, task_id: str, success: bool = True) -> None: + """Mark a task as completed by an agent.""" + if agent_id in self.agents: + agent = self.agents[agent_id] + if task_id in agent.current_tasks: + agent.current_tasks.remove(task_id) + + if success: + agent.metrics["tasks_completed"] += 1 + else: + agent.metrics["tasks_failed"] += 1 + + def get_agent_workload(self) -> Dict[str, dict]: + """Get current workload for all agents.""" + return { + agent_id: { + "current_tasks": len(profile.current_tasks), + "max_tasks": profile.max_concurrent_tasks, + "status": profile.status.value, + "utilization": len(profile.current_tasks) / profile.max_concurrent_tasks + } + for agent_id, profile in self.agents.items() + } + + def get_all_agents(self) -> List[dict]: + """Get all registered agents.""" + return [agent.to_dict() for agent in self.agents.values()] diff --git a/backend/app/agents/learning.py b/backend/app/agents/learning.py new file mode 100644 index 0000000..e2716ac --- /dev/null +++ b/backend/app/agents/learning.py @@ -0,0 +1,356 @@ +"""Agent Learning System with ML pipeline integration.""" + +import uuid +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Dict, List, Optional + +from app.core import get_settings + + +settings = get_settings() + + +class FeedbackType(str, Enum): + """Types of feedback for learning.""" + HUMAN_CORRECTION = "human_correction" + OUTCOME_RESULT = "outcome_result" + PERFORMANCE_METRIC = "performance_metric" + USER_RATING = "user_rating" + + +class TrainingJobStatus(str, Enum): + """Training job status.""" + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class Feedback: + """Feedback data for agent learning.""" + + def __init__( + self, + feedback_id: str, + task_id: str, + agent_id: str, + feedback_type: FeedbackType, + content: dict, + source_user_id: Optional[str] = None, + timestamp: Optional[datetime] = None + ): + self.feedback_id = feedback_id + self.task_id = task_id + self.agent_id = agent_id + self.feedback_type = feedback_type + self.content = content + self.source_user_id = source_user_id + self.timestamp = timestamp or datetime.now(timezone.utc) + + def to_dict(self) -> dict: + return { + "feedback_id": self.feedback_id, + "task_id": self.task_id, + "agent_id": self.agent_id, + "feedback_type": self.feedback_type.value, + "content": self.content, + "source_user_id": self.source_user_id, + "timestamp": self.timestamp.isoformat() + } + + +class ModelMetadata: + """Metadata for trained models in the registry.""" + + def __init__( + self, + model_id: str, + model_name: str, + version: str, + agent_id: str, + training_job_id: str, + metrics: dict, + artifact_path: Optional[str] = None, + is_active: bool = False, + created_at: Optional[datetime] = None + ): + self.model_id = model_id + self.model_name = model_name + self.version = version + self.agent_id = agent_id + self.training_job_id = training_job_id + self.metrics = metrics + self.artifact_path = artifact_path + self.is_active = is_active + self.created_at = created_at or datetime.now(timezone.utc) + + def to_dict(self) -> dict: + return { + "model_id": self.model_id, + "model_name": self.model_name, + "version": self.version, + "agent_id": self.agent_id, + "training_job_id": self.training_job_id, + "metrics": self.metrics, + "artifact_path": self.artifact_path, + "is_active": self.is_active, + "created_at": self.created_at.isoformat() + } + + +class TrainingJob: + """Training job definition.""" + + def __init__( + self, + job_id: str, + agent_id: str, + model_name: str, + training_config: dict, + status: TrainingJobStatus = TrainingJobStatus.PENDING, + started_at: Optional[datetime] = None, + completed_at: Optional[datetime] = None, + result_model_id: Optional[str] = None, + error_message: Optional[str] = None + ): + self.job_id = job_id + self.agent_id = agent_id + self.model_name = model_name + self.training_config = training_config + self.status = status + self.started_at = started_at + self.completed_at = completed_at + self.result_model_id = result_model_id + self.error_message = error_message + self.created_at = datetime.now(timezone.utc) + + def to_dict(self) -> dict: + return { + "job_id": self.job_id, + "agent_id": self.agent_id, + "model_name": self.model_name, + "training_config": self.training_config, + "status": self.status.value, + "started_at": self.started_at.isoformat() if self.started_at else None, + "completed_at": self.completed_at.isoformat() if self.completed_at else None, + "result_model_id": self.result_model_id, + "error_message": self.error_message, + "created_at": self.created_at.isoformat() + } + + +class AgentLearningSystem: + """ + Learning system for continuous agent improvement. + + Features: + - Collect feedback from human oversight and outcomes + - Trigger training jobs via Celery + - Track model versions in registry + - Fetch latest model for inference + """ + + def __init__(self, agent_id: str): + self.agent_id = agent_id + self.feedback_buffer: List[Feedback] = [] + self.training_jobs: Dict[str, TrainingJob] = {} + self.model_registry: Dict[str, ModelMetadata] = {} + + def collect_feedback( + self, + task_id: str, + feedback_type: FeedbackType, + content: dict, + source_user_id: Optional[str] = None + ) -> Feedback: + """ + Collect feedback for learning. + + This triggers model training when feedback buffer reaches threshold. + """ + feedback = Feedback( + feedback_id=str(uuid.uuid4()), + task_id=task_id, + agent_id=self.agent_id, + feedback_type=feedback_type, + content=content, + source_user_id=source_user_id + ) + + self.feedback_buffer.append(feedback) + + # Check if we should trigger training + if self._should_trigger_training(): + self._enqueue_training_job() + + return feedback + + def _should_trigger_training(self, threshold: int = 100) -> bool: + """Check if training should be triggered based on feedback count.""" + return len(self.feedback_buffer) >= threshold + + def _enqueue_training_job(self) -> Optional[TrainingJob]: + """ + Enqueue a training job via Celery. + + This is a placeholder - actual implementation would use Celery tasks. + """ + job = TrainingJob( + job_id=str(uuid.uuid4()), + agent_id=self.agent_id, + model_name=f"agent_{self.agent_id}_model", + training_config={ + "feedback_count": len(self.feedback_buffer), + "epochs": 10, + "batch_size": 32, + "learning_rate": 0.001 + } + ) + + self.training_jobs[job.job_id] = job + + # In real implementation, this would call Celery task + # from app.tasks.training import run_training_job + # run_training_job.delay(job.job_id, [f.to_dict() for f in self.feedback_buffer]) + + # Clear feedback buffer after enqueuing + self.feedback_buffer = [] + + return job + + def enqueue_training_job_manual( + self, + model_name: str, + training_config: dict, + feedback_ids: Optional[List[str]] = None + ) -> TrainingJob: + """Manually enqueue a training job with custom configuration.""" + job = TrainingJob( + job_id=str(uuid.uuid4()), + agent_id=self.agent_id, + model_name=model_name, + training_config={ + **training_config, + "feedback_ids": feedback_ids or [] + } + ) + + self.training_jobs[job.job_id] = job + + # Placeholder for Celery task + return job + + def update_training_job_status( + self, + job_id: str, + status: TrainingJobStatus, + result_model_id: Optional[str] = None, + error_message: Optional[str] = None + ) -> Optional[TrainingJob]: + """Update status of a training job.""" + if job_id not in self.training_jobs: + return None + + job = self.training_jobs[job_id] + job.status = status + + if status == TrainingJobStatus.RUNNING: + job.started_at = datetime.now(timezone.utc) + elif status in [TrainingJobStatus.COMPLETED, TrainingJobStatus.FAILED]: + job.completed_at = datetime.now(timezone.utc) + job.result_model_id = result_model_id + job.error_message = error_message + + return job + + def register_model( + self, + model_name: str, + version: str, + training_job_id: str, + metrics: dict, + artifact_path: Optional[str] = None, + activate: bool = False + ) -> ModelMetadata: + """Register a trained model in the registry.""" + model = ModelMetadata( + model_id=str(uuid.uuid4()), + model_name=model_name, + version=version, + agent_id=self.agent_id, + training_job_id=training_job_id, + metrics=metrics, + artifact_path=artifact_path, + is_active=activate + ) + + self.model_registry[model.model_id] = model + + # Deactivate other versions if activating this one + if activate: + for m in self.model_registry.values(): + if m.model_name == model_name and m.model_id != model.model_id: + m.is_active = False + + return model + + def get_active_model(self, model_name: str) -> Optional[ModelMetadata]: + """Get the currently active model for inference.""" + for model in self.model_registry.values(): + if model.model_name == model_name and model.is_active: + return model + return None + + def get_latest_model(self, model_name: str) -> Optional[ModelMetadata]: + """Get the latest model version by creation time.""" + models = [ + m for m in self.model_registry.values() + if m.model_name == model_name + ] + if not models: + return None + return max(models, key=lambda m: m.created_at) + + def activate_model(self, model_id: str) -> bool: + """Activate a specific model version.""" + if model_id not in self.model_registry: + return False + + model = self.model_registry[model_id] + + # Deactivate other versions + for m in self.model_registry.values(): + if m.model_name == model.model_name: + m.is_active = False + + model.is_active = True + return True + + def get_training_jobs( + self, + status: Optional[TrainingJobStatus] = None + ) -> List[TrainingJob]: + """Get training jobs, optionally filtered by status.""" + jobs = list(self.training_jobs.values()) + if status: + jobs = [j for j in jobs if j.status == status] + return sorted(jobs, key=lambda j: j.created_at, reverse=True) + + def get_feedback_summary(self) -> dict: + """Get summary of collected feedback.""" + by_type = {} + for f in self.feedback_buffer: + t = f.feedback_type.value + by_type[t] = by_type.get(t, 0) + 1 + + return { + "total_pending": len(self.feedback_buffer), + "by_type": by_type, + "models_registered": len(self.model_registry), + "active_training_jobs": len([ + j for j in self.training_jobs.values() + if j.status == TrainingJobStatus.RUNNING + ]) + } diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..9c7f58e --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ +# API module diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py new file mode 100644 index 0000000..2f15878 --- /dev/null +++ b/backend/app/api/admin.py @@ -0,0 +1,338 @@ +"""Admin API endpoints for system management.""" + +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.auth import get_current_active_user, require_role, Role, User +from app.db.session import get_db +from app.db import crud + + +router = APIRouter() + + +class AgentCreate(BaseModel): + """Agent creation request.""" + name: str + agent_type: str + capabilities: list[str] = [] + config: dict = {} + + +class AgentResponse(BaseModel): + """Agent response model.""" + id: str + name: str + agent_type: str + status: str + capabilities: list[str] + config: dict + + +class ModelCreate(BaseModel): + """Model registration request.""" + name: str + version: str + agent_id: str + training_job_id: Optional[str] = None + metrics: dict = {} + artifact_path: Optional[str] = None + is_active: bool = False + + +class ModelResponse(BaseModel): + """Model response model.""" + id: str + name: str + version: str + agent_id: str + metrics: dict + artifact_path: Optional[str] + is_active: bool + + +class TrainingJobCreate(BaseModel): + """Training job creation request.""" + model_name: str + agent_id: str + config: dict = {} + feedback_ids: list[str] = [] + + +# Agent Management + +@router.post("/agents", response_model=AgentResponse) +async def create_agent( + agent: AgentCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.ADMIN)) +): + """Create a new agent (admin only).""" + db_agent = await crud.create_agent( + db=db, + name=agent.name, + agent_type=agent.agent_type, + capabilities=agent.capabilities, + config=agent.config + ) + + return AgentResponse( + id=db_agent.id, + name=db_agent.name, + agent_type=db_agent.agent_type, + status=db_agent.status, + capabilities=db_agent.capabilities, + config=db_agent.config + ) + + +@router.get("/agents") +async def list_agents( + status: Optional[str] = None, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """List all agents.""" + agents = await crud.get_agents(db, status=status) + + return { + "agents": [ + AgentResponse( + id=a.id, + name=a.name, + agent_type=a.agent_type, + status=a.status, + capabilities=a.capabilities, + config=a.config + ) + for a in agents + ] + } + + +@router.get("/agents/{agent_id}", response_model=AgentResponse) +async def get_agent( + agent_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """Get agent by ID.""" + agent = await crud.get_agent(db, agent_id) + if not agent: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Agent not found" + ) + + return AgentResponse( + id=agent.id, + name=agent.name, + agent_type=agent.agent_type, + status=agent.status, + capabilities=agent.capabilities, + config=agent.config + ) + + +@router.patch("/agents/{agent_id}/status") +async def update_agent_status( + agent_id: str, + new_status: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.ADMIN)) +): + """Update agent status.""" + agent = await crud.update_agent_status(db, agent_id, new_status) + if not agent: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Agent not found" + ) + + return {"status": "updated", "new_status": new_status} + + +# Model Registry + +@router.post("/models", response_model=ModelResponse) +async def register_model( + model: ModelCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """Register a trained model in the registry.""" + db_model = await crud.create_model( + db=db, + name=model.name, + version=model.version, + agent_id=model.agent_id, + training_job_id=model.training_job_id, + metrics=model.metrics, + artifact_path=model.artifact_path, + is_active=model.is_active + ) + + return ModelResponse( + id=db_model.id, + name=db_model.name, + version=db_model.version, + agent_id=db_model.agent_id, + metrics=db_model.metrics, + artifact_path=db_model.artifact_path, + is_active=db_model.is_active + ) + + +@router.get("/models") +async def list_models( + agent_id: Optional[str] = None, + active_only: bool = False, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """List models from the registry.""" + models = await crud.get_models( + db=db, + agent_id=agent_id, + active_only=active_only + ) + + return { + "models": [ + ModelResponse( + id=m.id, + name=m.name, + version=m.version, + agent_id=m.agent_id, + metrics=m.metrics, + artifact_path=m.artifact_path, + is_active=m.is_active + ) + for m in models + ] + } + + +@router.post("/models/{model_id}/activate") +async def activate_model( + model_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """Activate a model version (deactivates other versions of same model).""" + model = await crud.activate_model(db, model_id) + if not model: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Model not found" + ) + + return {"status": "activated", "model_id": model_id} + + +# Training Jobs + +@router.post("/training-jobs") +async def create_training_job( + job: TrainingJobCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """Create and enqueue a training job.""" + # In a full implementation, this would enqueue a Celery task + db_job = await crud.create_training_job( + db=db, + model_name=job.model_name, + agent_id=job.agent_id, + config=job.config, + feedback_ids=job.feedback_ids + ) + + # TODO: Enqueue Celery task + # from app.tasks.training import run_training_job + # run_training_job.delay(db_job.id) + + return { + "job_id": db_job.id, + "status": db_job.status, + "message": "Training job created and queued" + } + + +@router.get("/training-jobs") +async def list_training_jobs( + status: Optional[str] = None, + agent_id: Optional[str] = None, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """List training jobs.""" + jobs = await crud.get_training_jobs( + db=db, + status=status, + agent_id=agent_id + ) + + return { + "jobs": [ + { + "id": j.id, + "model_name": j.model_name, + "agent_id": j.agent_id, + "status": j.status, + "config": j.config, + "created_at": j.created_at, + "started_at": j.started_at, + "completed_at": j.completed_at + } + for j in jobs + ] + } + + +# Audit Log + +@router.get("/audits") +async def get_audit_log( + entity_type: Optional[str] = None, + user_id: Optional[str] = None, + limit: int = 100, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.ADMIN)) +): + """Get audit log (admin only).""" + audits = await crud.get_audits( + db=db, + entity_type=entity_type, + user_id=user_id, + limit=limit + ) + + return { + "audits": [ + { + "id": a.id, + "action": a.action, + "entity_type": a.entity_type, + "entity_id": a.entity_id, + "user_id": a.user_id, + "details": a.details, + "created_at": a.created_at + } + for a in audits + ] + } + + +# System Stats + +@router.get("/stats") +async def get_system_stats( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """Get system statistics.""" + stats = await crud.get_system_stats(db) + return stats diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py new file mode 100644 index 0000000..db7e9e0 --- /dev/null +++ b/backend/app/api/auth.py @@ -0,0 +1,348 @@ +"""Authentication API endpoints with OIDC support.""" + +from datetime import datetime, timedelta, timezone +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, status, Request +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from pydantic import BaseModel +import httpx + +from app.core import get_settings + + +settings = get_settings() +router = APIRouter() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) + + +# RBAC Role definitions +class Role: + ADMIN = "admin" + REVIEWER = "reviewer" + AGENT_MANAGER = "agent_manager" + VIEWER = "viewer" + + +ROLE_HIERARCHY = { + Role.ADMIN: [Role.ADMIN, Role.REVIEWER, Role.AGENT_MANAGER, Role.VIEWER], + Role.REVIEWER: [Role.REVIEWER, Role.VIEWER], + Role.AGENT_MANAGER: [Role.AGENT_MANAGER, Role.VIEWER], + Role.VIEWER: [Role.VIEWER], +} + + +class Token(BaseModel): + access_token: str + token_type: str + expires_in: int + + +class TokenData(BaseModel): + sub: str + email: Optional[str] = None + roles: list[str] = [] + exp: Optional[datetime] = None + + +class User(BaseModel): + id: str + email: str + name: Optional[str] = None + roles: list[str] = [] + is_active: bool = True + + +class OIDCConfig(BaseModel): + """OIDC discovery configuration.""" + issuer: str + authorization_endpoint: str + token_endpoint: str + userinfo_endpoint: str + jwks_uri: str + + +# Cache for OIDC configuration and JWKS +_oidc_config_cache: Optional[OIDCConfig] = None +_jwks_cache: Optional[dict] = None + + +async def get_oidc_config() -> Optional[OIDCConfig]: + """Fetch OIDC configuration from issuer's well-known endpoint.""" + global _oidc_config_cache + + if not settings.OIDC_ISSUER: + return None + + if _oidc_config_cache: + return _oidc_config_cache + + async with httpx.AsyncClient() as client: + try: + response = await client.get( + f"{settings.OIDC_ISSUER}/.well-known/openid-configuration" + ) + response.raise_for_status() + data = response.json() + + _oidc_config_cache = OIDCConfig( + issuer=data["issuer"], + authorization_endpoint=data["authorization_endpoint"], + token_endpoint=data["token_endpoint"], + userinfo_endpoint=data["userinfo_endpoint"], + jwks_uri=data["jwks_uri"] + ) + return _oidc_config_cache + except Exception: + return None + + +async def get_jwks() -> Optional[dict]: + """Fetch JWKS from OIDC provider.""" + global _jwks_cache + + if _jwks_cache: + return _jwks_cache + + oidc_config = await get_oidc_config() + if not oidc_config: + return None + + async with httpx.AsyncClient() as client: + try: + response = await client.get(oidc_config.jwks_uri) + response.raise_for_status() + _jwks_cache = response.json() + return _jwks_cache + except Exception: + return None + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """Create a JWT access token.""" + to_encode = data.copy() + + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.JWT_EXPIRE_MINUTES) + + to_encode.update({"exp": expire}) + + encoded_jwt = jwt.encode( + to_encode, + settings.JWT_SECRET_KEY, + algorithm=settings.JWT_ALGORITHM + ) + return encoded_jwt + + +async def validate_token(token: str) -> Optional[TokenData]: + """Validate a JWT token (local or OIDC).""" + try: + # First try local validation + payload = jwt.decode( + token, + settings.JWT_SECRET_KEY, + algorithms=[settings.JWT_ALGORITHM] + ) + + return TokenData( + sub=payload.get("sub", ""), + email=payload.get("email"), + roles=payload.get("roles", []), + exp=datetime.fromtimestamp(payload.get("exp", 0), tz=timezone.utc) + ) + except JWTError: + pass + + # Try OIDC validation if configured + if settings.OIDC_ISSUER: + try: + jwks = await get_jwks() + if jwks: + # In production, use proper JWKS validation + # This is simplified for the scaffold + payload = jwt.decode( + token, + settings.JWT_SECRET_KEY, # Would use JWKS in production + algorithms=["RS256", settings.JWT_ALGORITHM], + options={"verify_signature": False} # Simplified for demo + ) + + return TokenData( + sub=payload.get("sub", ""), + email=payload.get("email"), + roles=payload.get("roles", payload.get("groups", [])), + exp=datetime.fromtimestamp(payload.get("exp", 0), tz=timezone.utc) + ) + except Exception: + pass + + return None + + +async def get_current_user(token: Optional[str] = Depends(oauth2_scheme)) -> User: + """Get the current authenticated user from token.""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if not token: + raise credentials_exception + + token_data = await validate_token(token) + if not token_data: + raise credentials_exception + + return User( + id=token_data.sub, + email=token_data.email or f"{token_data.sub}@local", + roles=token_data.roles or [Role.VIEWER] + ) + + +async def get_current_active_user( + current_user: User = Depends(get_current_user) +) -> User: + """Ensure the current user is active.""" + if not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Inactive user" + ) + return current_user + + +def require_role(*required_roles: str): + """Dependency factory for role-based access control.""" + async def role_checker(user: User = Depends(get_current_active_user)) -> User: + user_permissions = set() + for role in user.roles: + user_permissions.update(ROLE_HIERARCHY.get(role, [role])) + + if not any(role in user_permissions for role in required_roles): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Required role: {required_roles}" + ) + return user + + return role_checker + + +# API Endpoints + +@router.post("/token", response_model=Token) +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +): + """ + OAuth2 compatible token login endpoint. + + For development, accepts any username/password combination. + In production, this would validate against a user database or OIDC. + """ + # Development mode - accept any credentials + # In production, validate against database or OIDC + + # Assign default roles based on username for testing + roles = [Role.VIEWER] + if form_data.username.startswith("admin"): + roles = [Role.ADMIN] + elif form_data.username.startswith("reviewer"): + roles = [Role.REVIEWER] + elif form_data.username.startswith("manager"): + roles = [Role.AGENT_MANAGER] + + access_token = create_access_token( + data={ + "sub": form_data.username, + "email": f"{form_data.username}@local", + "roles": roles + } + ) + + return Token( + access_token=access_token, + token_type="bearer", + expires_in=settings.JWT_EXPIRE_MINUTES * 60 + ) + + +@router.get("/me", response_model=User) +async def read_users_me( + current_user: User = Depends(get_current_active_user) +): + """Get current user profile.""" + return current_user + + +@router.get("/oidc/config") +async def get_oidc_configuration(): + """Get OIDC configuration for frontend.""" + if not settings.OIDC_ISSUER: + return { + "enabled": False, + "message": "OIDC not configured" + } + + config = await get_oidc_config() + if not config: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Could not fetch OIDC configuration" + ) + + return { + "enabled": True, + "issuer": config.issuer, + "authorization_endpoint": config.authorization_endpoint, + "client_id": settings.OIDC_CLIENT_ID + } + + +@router.post("/oidc/callback") +async def oidc_callback(code: str, state: Optional[str] = None): + """ + Handle OIDC callback and exchange code for tokens. + + This endpoint receives the authorization code from the OIDC provider + and exchanges it for access and ID tokens. + """ + oidc_config = await get_oidc_config() + if not oidc_config: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="OIDC not configured" + ) + + async with httpx.AsyncClient() as client: + try: + response = await client.post( + oidc_config.token_endpoint, + data={ + "grant_type": "authorization_code", + "code": code, + "client_id": settings.OIDC_CLIENT_ID, + "client_secret": settings.OIDC_CLIENT_SECRET, + "redirect_uri": f"{settings.API_V1_PREFIX}/auth/oidc/callback" + } + ) + response.raise_for_status() + tokens = response.json() + + return { + "access_token": tokens.get("access_token"), + "id_token": tokens.get("id_token"), + "token_type": tokens.get("token_type", "Bearer"), + "expires_in": tokens.get("expires_in", 3600) + } + except httpx.HTTPError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Token exchange failed: {str(e)}" + ) diff --git a/backend/app/api/decisions.py b/backend/app/api/decisions.py new file mode 100644 index 0000000..ae77e83 --- /dev/null +++ b/backend/app/api/decisions.py @@ -0,0 +1,379 @@ +"""Decisions API for task management and agent orchestration.""" + +import uuid +from datetime import datetime, timezone +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.auth import get_current_active_user, require_role, Role, User +from app.db.session import get_db +from app.db import crud +from app.agents.communication import TaskState + + +router = APIRouter() + + +class TaskCreate(BaseModel): + """Task creation request.""" + title: str + description: Optional[str] = None + task_type: str = "default" + priority: str = "medium" + payload: dict = {} + assigned_agent_id: Optional[str] = None + + +class TaskResponse(BaseModel): + """Task response model.""" + id: str + title: str + description: Optional[str] + task_type: str + priority: str + state: str + payload: dict + assigned_agent_id: Optional[str] + retry_count: int + created_at: datetime + updated_at: datetime + + +class TaskStateUpdate(BaseModel): + """Task state update request.""" + state: TaskState + metadata: Optional[dict] = None + + +class DecisionOverride(BaseModel): + """Human decision override request.""" + reason: str + new_decision: str + metadata: Optional[dict] = None + + +@router.post("/task", response_model=TaskResponse) +async def create_task( + task: TaskCreate, + background_tasks: BackgroundTasks, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """ + Create a new task and publish to agent orchestration. + + This endpoint: + 1. Persists the task to the database + 2. Creates an initial task event + 3. Publishes the task to Redis for agent assignment + 4. Returns the created task + """ + task_id = str(uuid.uuid4()) + + # Create task in database + db_task = await crud.create_task( + db=db, + task_id=task_id, + title=task.title, + description=task.description, + task_type=task.task_type, + priority=task.priority, + payload=task.payload, + assigned_agent_id=task.assigned_agent_id + ) + + # Create initial event + await crud.create_event( + db=db, + event_type="task_created", + entity_type="task", + entity_id=task_id, + data={ + "title": task.title, + "task_type": task.task_type, + "priority": task.priority + }, + user_id=current_user.id + ) + + # Schedule background task for Redis publication + # In a full implementation, this would use the AgentCommunicationSystem + + return TaskResponse( + id=db_task.id, + title=db_task.title, + description=db_task.description, + task_type=db_task.task_type, + priority=db_task.priority, + state=db_task.state, + payload=db_task.payload, + assigned_agent_id=db_task.assigned_agent_id, + retry_count=db_task.retry_count, + created_at=db_task.created_at, + updated_at=db_task.updated_at + ) + + +@router.get("/task/{task_id}", response_model=TaskResponse) +async def get_task( + task_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """Get task by ID.""" + task = await crud.get_task(db, task_id) + if not task: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Task not found" + ) + + return TaskResponse( + id=task.id, + title=task.title, + description=task.description, + task_type=task.task_type, + priority=task.priority, + state=task.state, + payload=task.payload, + assigned_agent_id=task.assigned_agent_id, + retry_count=task.retry_count, + created_at=task.created_at, + updated_at=task.updated_at + ) + + +@router.get("/tasks") +async def list_tasks( + state: Optional[TaskState] = None, + limit: int = 50, + offset: int = 0, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """List tasks with optional state filter.""" + tasks = await crud.get_tasks( + db=db, + state=state.value if state else None, + limit=limit, + offset=offset + ) + + return { + "tasks": [ + TaskResponse( + id=t.id, + title=t.title, + description=t.description, + task_type=t.task_type, + priority=t.priority, + state=t.state, + payload=t.payload, + assigned_agent_id=t.assigned_agent_id, + retry_count=t.retry_count, + created_at=t.created_at, + updated_at=t.updated_at + ) + for t in tasks + ], + "limit": limit, + "offset": offset + } + + +@router.patch("/task/{task_id}/state") +async def update_task_state( + task_id: str, + state_update: TaskStateUpdate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.ADMIN)) +): + """Update task state and create state change event.""" + task = await crud.get_task(db, task_id) + if not task: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Task not found" + ) + + old_state = task.state + + # Update task state + updated_task = await crud.update_task_state( + db=db, + task_id=task_id, + state=state_update.state.value + ) + + # Create state change event + await crud.create_event( + db=db, + event_type="task_state_changed", + entity_type="task", + entity_id=task_id, + data={ + "old_state": old_state, + "new_state": state_update.state.value, + "metadata": state_update.metadata + }, + user_id=current_user.id + ) + + return {"status": "updated", "new_state": state_update.state.value} + + +@router.post("/task/{task_id}/override") +async def override_decision( + task_id: str, + override: DecisionOverride, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.REVIEWER, Role.ADMIN)) +): + """ + Override an agent decision (human in the loop). + + Only reviewers and admins can override decisions. + """ + task = await crud.get_task(db, task_id) + if not task: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Task not found" + ) + + # Create override event + await crud.create_event( + db=db, + event_type="decision_override", + entity_type="task", + entity_id=task_id, + data={ + "reason": override.reason, + "new_decision": override.new_decision, + "metadata": override.metadata, + "overridden_by": current_user.id + }, + user_id=current_user.id + ) + + # Create audit record + await crud.create_audit( + db=db, + action="decision_override", + entity_type="task", + entity_id=task_id, + user_id=current_user.id, + details={ + "reason": override.reason, + "new_decision": override.new_decision + } + ) + + return { + "status": "overridden", + "task_id": task_id, + "new_decision": override.new_decision + } + + +@router.post("/task/{task_id}/escalate") +async def escalate_task( + task_id: str, + reason: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.AGENT_MANAGER, Role.REVIEWER, Role.ADMIN)) +): + """ + Escalate a task to higher priority or human review. + """ + task = await crud.get_task(db, task_id) + if not task: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Task not found" + ) + + # Update task state to escalated + await crud.update_task_state(db, task_id, TaskState.ESCALATED.value) + + # Create escalation event + await crud.create_event( + db=db, + event_type="task_escalated", + entity_type="task", + entity_id=task_id, + data={ + "reason": reason, + "escalated_by": current_user.id, + "previous_state": task.state + }, + user_id=current_user.id + ) + + return { + "status": "escalated", + "task_id": task_id, + "reason": reason + } + + +@router.get("/events") +async def get_events( + entity_type: Optional[str] = None, + entity_id: Optional[str] = None, + limit: int = 100, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """Get events with optional filters.""" + events = await crud.get_events( + db=db, + entity_type=entity_type, + entity_id=entity_id, + limit=limit + ) + + return { + "events": [ + { + "id": e.id, + "event_type": e.event_type, + "entity_type": e.entity_type, + "entity_id": e.entity_id, + "data": e.data, + "user_id": e.user_id, + "created_at": e.created_at + } + for e in events + ] + } + + +@router.get("/escalations") +async def get_escalations( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role(Role.REVIEWER, Role.ADMIN)) +): + """Get all escalated tasks for review.""" + tasks = await crud.get_tasks(db, state=TaskState.ESCALATED.value) + + return { + "escalations": [ + TaskResponse( + id=t.id, + title=t.title, + description=t.description, + task_type=t.task_type, + priority=t.priority, + state=t.state, + payload=t.payload, + assigned_agent_id=t.assigned_agent_id, + retry_count=t.retry_count, + created_at=t.created_at, + updated_at=t.updated_at + ) + for t in tasks + ] + } diff --git a/backend/app/api/oversight_ws.py b/backend/app/api/oversight_ws.py new file mode 100644 index 0000000..afa61e4 --- /dev/null +++ b/backend/app/api/oversight_ws.py @@ -0,0 +1,294 @@ +"""WebSocket endpoint for real-time oversight dashboard.""" + +import asyncio +import json +from datetime import datetime, timezone +from typing import Optional, Set +from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, Query +from fastapi.websockets import WebSocketState + +from app.api.auth import validate_token, Role, ROLE_HIERARCHY + + +router = APIRouter() + + +class ConnectionManager: + """Manage WebSocket connections with authentication and RBAC.""" + + def __init__(self): + self.active_connections: dict[str, WebSocket] = {} + self.user_roles: dict[str, list[str]] = {} + self.subscriptions: dict[str, Set[str]] = {} # user_id -> set of channels + + async def connect( + self, + websocket: WebSocket, + user_id: str, + roles: list[str] + ): + """Accept and register a WebSocket connection.""" + await websocket.accept() + self.active_connections[user_id] = websocket + self.user_roles[user_id] = roles + self.subscriptions[user_id] = {"events", "decisions"} # Default subscriptions + + def disconnect(self, user_id: str): + """Remove a WebSocket connection.""" + if user_id in self.active_connections: + del self.active_connections[user_id] + if user_id in self.user_roles: + del self.user_roles[user_id] + if user_id in self.subscriptions: + del self.subscriptions[user_id] + + def has_permission(self, user_id: str, required_roles: list[str]) -> bool: + """Check if user has required roles.""" + user_roles = self.user_roles.get(user_id, []) + user_permissions = set() + for role in user_roles: + user_permissions.update(ROLE_HIERARCHY.get(role, [role])) + + return any(role in user_permissions for role in required_roles) + + async def send_personal(self, user_id: str, message: dict): + """Send message to a specific user.""" + if user_id in self.active_connections: + websocket = self.active_connections[user_id] + if websocket.client_state == WebSocketState.CONNECTED: + try: + await websocket.send_json(message) + except Exception: + self.disconnect(user_id) + + async def broadcast( + self, + message: dict, + channel: str = "events", + required_roles: Optional[list[str]] = None + ): + """Broadcast message to all subscribed users with proper roles.""" + disconnected = [] + + for user_id, websocket in self.active_connections.items(): + # Check channel subscription + if channel not in self.subscriptions.get(user_id, set()): + continue + + # Check role requirements + if required_roles and not self.has_permission(user_id, required_roles): + continue + + try: + if websocket.client_state == WebSocketState.CONNECTED: + await websocket.send_json({ + "channel": channel, + "data": message, + "timestamp": datetime.now(timezone.utc).isoformat() + }) + except Exception: + disconnected.append(user_id) + + # Clean up disconnected clients + for user_id in disconnected: + self.disconnect(user_id) + + async def broadcast_event(self, event: dict): + """Broadcast a decision/state event to all authenticated users.""" + await self.broadcast(event, channel="events") + + async def broadcast_escalation(self, escalation: dict): + """Broadcast escalation to reviewers and admins only.""" + await self.broadcast( + escalation, + channel="escalations", + required_roles=[Role.REVIEWER, Role.ADMIN] + ) + + def subscribe(self, user_id: str, channel: str): + """Subscribe user to a channel.""" + if user_id in self.subscriptions: + self.subscriptions[user_id].add(channel) + + def unsubscribe(self, user_id: str, channel: str): + """Unsubscribe user from a channel.""" + if user_id in self.subscriptions: + self.subscriptions[user_id].discard(channel) + + +# Global connection manager +manager = ConnectionManager() + + +@router.websocket("/ws") +async def websocket_oversight( + websocket: WebSocket, + token: Optional[str] = Query(None) +): + """ + WebSocket endpoint for real-time oversight streaming. + + Authentication is done via query parameter token. + Messages are filtered based on user roles. + + Message types: + - subscribe: Subscribe to a channel + - unsubscribe: Unsubscribe from a channel + - action: Perform an action (override, escalate) - requires proper roles + """ + # Authenticate + if not token: + await websocket.close(code=4001, reason="Authentication required") + return + + token_data = await validate_token(token) + if not token_data: + await websocket.close(code=4001, reason="Invalid token") + return + + user_id = token_data.sub + roles = token_data.roles or [Role.VIEWER] + + # Connect + await manager.connect(websocket, user_id, roles) + + try: + # Send initial connection success message + await websocket.send_json({ + "type": "connected", + "user_id": user_id, + "roles": roles, + "subscriptions": list(manager.subscriptions.get(user_id, set())), + "timestamp": datetime.now(timezone.utc).isoformat() + }) + + # Listen for messages + while True: + try: + data = await websocket.receive_json() + await handle_websocket_message(websocket, user_id, data) + except json.JSONDecodeError: + await websocket.send_json({ + "type": "error", + "message": "Invalid JSON" + }) + + except WebSocketDisconnect: + manager.disconnect(user_id) + except Exception: + manager.disconnect(user_id) + + +async def handle_websocket_message( + websocket: WebSocket, + user_id: str, + data: dict +): + """Handle incoming WebSocket messages.""" + message_type = data.get("type") + + if message_type == "subscribe": + channel = data.get("channel") + if channel: + manager.subscribe(user_id, channel) + await websocket.send_json({ + "type": "subscribed", + "channel": channel + }) + + elif message_type == "unsubscribe": + channel = data.get("channel") + if channel: + manager.unsubscribe(user_id, channel) + await websocket.send_json({ + "type": "unsubscribed", + "channel": channel + }) + + elif message_type == "action": + action = data.get("action") + task_id = data.get("task_id") + + if action == "override": + # Check permission + if not manager.has_permission(user_id, [Role.REVIEWER, Role.ADMIN]): + await websocket.send_json({ + "type": "error", + "message": "Insufficient permissions for override" + }) + return + + # Broadcast override action + await manager.broadcast({ + "action": "override", + "task_id": task_id, + "user_id": user_id, + "data": data.get("data", {}) + }, channel="actions") + + await websocket.send_json({ + "type": "action_acknowledged", + "action": "override", + "task_id": task_id + }) + + elif action == "escalate": + # Check permission + if not manager.has_permission( + user_id, + [Role.AGENT_MANAGER, Role.REVIEWER, Role.ADMIN] + ): + await websocket.send_json({ + "type": "error", + "message": "Insufficient permissions for escalation" + }) + return + + # Broadcast escalation + await manager.broadcast_escalation({ + "action": "escalate", + "task_id": task_id, + "user_id": user_id, + "reason": data.get("reason", "Manual escalation") + }) + + await websocket.send_json({ + "type": "action_acknowledged", + "action": "escalate", + "task_id": task_id + }) + + elif message_type == "ping": + await websocket.send_json({ + "type": "pong", + "timestamp": datetime.now(timezone.utc).isoformat() + }) + + else: + await websocket.send_json({ + "type": "error", + "message": f"Unknown message type: {message_type}" + }) + + +# Helper function to broadcast events from other parts of the application +async def broadcast_decision_event(event: dict): + """Broadcast a decision event to all connected oversight clients.""" + await manager.broadcast_event(event) + + +async def broadcast_task_state_change( + task_id: str, + old_state: str, + new_state: str, + metadata: Optional[dict] = None +): + """Broadcast a task state change event.""" + await manager.broadcast_event({ + "event_type": "task_state_changed", + "task_id": task_id, + "old_state": old_state, + "new_state": new_state, + "metadata": metadata or {}, + "timestamp": datetime.now(timezone.utc).isoformat() + }) diff --git a/backend/app/core.py b/backend/app/core.py new file mode 100644 index 0000000..e9373fa --- /dev/null +++ b/backend/app/core.py @@ -0,0 +1,50 @@ +"""Core configuration and settings for the Agentic platform.""" + +from pydantic_settings import BaseSettings, SettingsConfigDict +from functools import lru_cache +from typing import Optional + + +class Settings(BaseSettings): + """Application settings loaded from environment variables.""" + + model_config = SettingsConfigDict(env_file=".env", case_sensitive=True) + + # Application + APP_NAME: str = "Agentic Agent Platform" + DEBUG: bool = False + API_V1_PREFIX: str = "/api/v1" + + # Database + DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/agentic" + DATABASE_ECHO: bool = False + + # Redis + REDIS_URL: str = "redis://localhost:6379/0" + + # OIDC Authentication + OIDC_ISSUER: Optional[str] = None + OIDC_CLIENT_ID: Optional[str] = None + OIDC_CLIENT_SECRET: Optional[str] = None + OIDC_JWKS_URL: Optional[str] = None + + # JWT Settings (for local development without OIDC) + JWT_SECRET_KEY: str = "dev-secret-key-change-in-production" + JWT_ALGORITHM: str = "HS256" + JWT_EXPIRE_MINUTES: int = 60 + + # Task orchestration + TASK_ACK_TIMEOUT_SECONDS: int = 30 + TASK_MAX_RETRIES: int = 3 + TASK_BACKOFF_BASE: float = 2.0 + TASK_ESCALATION_THRESHOLD: int = 3 + + # Celery + CELERY_BROKER_URL: str = "redis://localhost:6379/1" + CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2" + + +@lru_cache() +def get_settings() -> Settings: + """Get cached settings instance.""" + return Settings() diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py new file mode 100644 index 0000000..65f47a9 --- /dev/null +++ b/backend/app/db/__init__.py @@ -0,0 +1 @@ +# Database module diff --git a/backend/app/db/base.py b/backend/app/db/base.py new file mode 100644 index 0000000..7f09abb --- /dev/null +++ b/backend/app/db/base.py @@ -0,0 +1,21 @@ +"""SQLAlchemy base classes and metadata.""" + +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy import MetaData + + +# Naming convention for constraints +convention = { + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=convention) + + +class Base(DeclarativeBase): + """Base class for all models.""" + metadata = metadata diff --git a/backend/app/db/crud.py b/backend/app/db/crud.py new file mode 100644 index 0000000..79e7b95 --- /dev/null +++ b/backend/app/db/crud.py @@ -0,0 +1,471 @@ +"""CRUD operations for database models.""" + +import uuid +from datetime import datetime, timezone +from typing import Optional +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.models import Event, Task, Agent, Audit, Feedback, Model, TrainingJob + + +# Event operations + +async def create_event( + db: AsyncSession, + event_type: str, + entity_type: str, + entity_id: str, + data: dict, + user_id: Optional[str] = None +) -> Event: + """Create a new event in the event store.""" + event = Event( + id=str(uuid.uuid4()), + event_type=event_type, + entity_type=entity_type, + entity_id=entity_id, + data=data, + user_id=user_id + ) + db.add(event) + await db.flush() + return event + + +async def get_events( + db: AsyncSession, + entity_type: Optional[str] = None, + entity_id: Optional[str] = None, + event_type: Optional[str] = None, + limit: int = 100 +) -> list[Event]: + """Get events with optional filters.""" + query = select(Event).order_by(Event.created_at.desc()).limit(limit) + + if entity_type: + query = query.where(Event.entity_type == entity_type) + if entity_id: + query = query.where(Event.entity_id == entity_id) + if event_type: + query = query.where(Event.event_type == event_type) + + result = await db.execute(query) + return list(result.scalars().all()) + + +# Task operations + +async def create_task( + db: AsyncSession, + task_id: str, + title: str, + description: Optional[str] = None, + task_type: str = "default", + priority: str = "medium", + payload: dict = None, + assigned_agent_id: Optional[str] = None +) -> Task: + """Create a new task.""" + task = Task( + id=task_id, + title=title, + description=description, + task_type=task_type, + priority=priority, + payload=payload or {}, + assigned_agent_id=assigned_agent_id, + state="queued" + ) + db.add(task) + await db.flush() + return task + + +async def get_task(db: AsyncSession, task_id: str) -> Optional[Task]: + """Get a task by ID.""" + result = await db.execute(select(Task).where(Task.id == task_id)) + return result.scalar_one_or_none() + + +async def get_tasks( + db: AsyncSession, + state: Optional[str] = None, + agent_id: Optional[str] = None, + limit: int = 50, + offset: int = 0 +) -> list[Task]: + """Get tasks with optional filters.""" + query = select(Task).order_by(Task.created_at.desc()).limit(limit).offset(offset) + + if state: + query = query.where(Task.state == state) + if agent_id: + query = query.where(Task.assigned_agent_id == agent_id) + + result = await db.execute(query) + return list(result.scalars().all()) + + +async def update_task_state( + db: AsyncSession, + task_id: str, + state: str, + increment_retry: bool = False +) -> Optional[Task]: + """Update task state.""" + task = await get_task(db, task_id) + if not task: + return None + + task.state = state + task.updated_at = datetime.now(timezone.utc) + if increment_retry: + task.retry_count += 1 + + await db.flush() + return task + + +# Agent operations + +async def create_agent( + db: AsyncSession, + name: str, + agent_type: str, + capabilities: list = None, + config: dict = None +) -> Agent: + """Create a new agent.""" + agent = Agent( + id=str(uuid.uuid4()), + name=name, + agent_type=agent_type, + capabilities=capabilities or [], + config=config or {}, + status="available" + ) + db.add(agent) + await db.flush() + return agent + + +async def get_agent(db: AsyncSession, agent_id: str) -> Optional[Agent]: + """Get an agent by ID.""" + result = await db.execute(select(Agent).where(Agent.id == agent_id)) + return result.scalar_one_or_none() + + +async def get_agents( + db: AsyncSession, + status: Optional[str] = None +) -> list[Agent]: + """Get all agents with optional status filter.""" + query = select(Agent).order_by(Agent.name) + + if status: + query = query.where(Agent.status == status) + + result = await db.execute(query) + return list(result.scalars().all()) + + +async def update_agent_status( + db: AsyncSession, + agent_id: str, + status: str +) -> Optional[Agent]: + """Update agent status.""" + agent = await get_agent(db, agent_id) + if not agent: + return None + + agent.status = status + agent.updated_at = datetime.now(timezone.utc) + await db.flush() + return agent + + +# Audit operations + +async def create_audit( + db: AsyncSession, + action: str, + entity_type: str, + entity_id: str, + user_id: str, + details: dict = None +) -> Audit: + """Create an audit record.""" + audit = Audit( + id=str(uuid.uuid4()), + action=action, + entity_type=entity_type, + entity_id=entity_id, + user_id=user_id, + details=details or {} + ) + db.add(audit) + await db.flush() + return audit + + +async def get_audits( + db: AsyncSession, + entity_type: Optional[str] = None, + user_id: Optional[str] = None, + limit: int = 100 +) -> list[Audit]: + """Get audit records with optional filters.""" + query = select(Audit).order_by(Audit.created_at.desc()).limit(limit) + + if entity_type: + query = query.where(Audit.entity_type == entity_type) + if user_id: + query = query.where(Audit.user_id == user_id) + + result = await db.execute(query) + return list(result.scalars().all()) + + +# Feedback operations + +async def create_feedback( + db: AsyncSession, + task_id: str, + agent_id: str, + feedback_type: str, + content: dict, + source_user_id: Optional[str] = None +) -> Feedback: + """Create feedback record.""" + feedback = Feedback( + id=str(uuid.uuid4()), + task_id=task_id, + agent_id=agent_id, + feedback_type=feedback_type, + content=content, + source_user_id=source_user_id + ) + db.add(feedback) + await db.flush() + return feedback + + +async def get_feedback( + db: AsyncSession, + agent_id: Optional[str] = None, + task_id: Optional[str] = None, + limit: int = 100 +) -> list[Feedback]: + """Get feedback records.""" + query = select(Feedback).order_by(Feedback.created_at.desc()).limit(limit) + + if agent_id: + query = query.where(Feedback.agent_id == agent_id) + if task_id: + query = query.where(Feedback.task_id == task_id) + + result = await db.execute(query) + return list(result.scalars().all()) + + +# Model operations + +async def create_model( + db: AsyncSession, + name: str, + version: str, + agent_id: str, + training_job_id: Optional[str] = None, + metrics: dict = None, + artifact_path: Optional[str] = None, + is_active: bool = False +) -> Model: + """Create a model registry entry.""" + model = Model( + id=str(uuid.uuid4()), + name=name, + version=version, + agent_id=agent_id, + training_job_id=training_job_id, + metrics=metrics or {}, + artifact_path=artifact_path, + is_active=is_active + ) + db.add(model) + await db.flush() + return model + + +async def get_models( + db: AsyncSession, + agent_id: Optional[str] = None, + active_only: bool = False +) -> list[Model]: + """Get models from registry.""" + query = select(Model).order_by(Model.created_at.desc()) + + if agent_id: + query = query.where(Model.agent_id == agent_id) + if active_only: + query = query.where(Model.is_active == True) + + result = await db.execute(query) + return list(result.scalars().all()) + + +async def activate_model( + db: AsyncSession, + model_id: str +) -> Optional[Model]: + """Activate a model (deactivates others with same name).""" + model = await db.execute(select(Model).where(Model.id == model_id)) + model = model.scalar_one_or_none() + + if not model: + return None + + # Deactivate other versions + other_models = await db.execute( + select(Model).where( + Model.name == model.name, + Model.id != model_id + ) + ) + for m in other_models.scalars().all(): + m.is_active = False + + model.is_active = True + await db.flush() + return model + + +# Training job operations + +async def create_training_job( + db: AsyncSession, + model_name: str, + agent_id: str, + config: dict = None, + feedback_ids: list = None +) -> TrainingJob: + """Create a training job.""" + job = TrainingJob( + id=str(uuid.uuid4()), + model_name=model_name, + agent_id=agent_id, + status="pending", + config={ + **(config or {}), + "feedback_ids": feedback_ids or [] + } + ) + db.add(job) + await db.flush() + return job + + +async def get_training_jobs( + db: AsyncSession, + status: Optional[str] = None, + agent_id: Optional[str] = None +) -> list[TrainingJob]: + """Get training jobs.""" + query = select(TrainingJob).order_by(TrainingJob.created_at.desc()) + + if status: + query = query.where(TrainingJob.status == status) + if agent_id: + query = query.where(TrainingJob.agent_id == agent_id) + + result = await db.execute(query) + return list(result.scalars().all()) + + +async def update_training_job_status( + db: AsyncSession, + job_id: str, + status: str, + error_message: Optional[str] = None +) -> Optional[TrainingJob]: + """Update training job status.""" + result = await db.execute(select(TrainingJob).where(TrainingJob.id == job_id)) + job = result.scalar_one_or_none() + + if not job: + return None + + job.status = status + + if status == "running": + job.started_at = datetime.now(timezone.utc) + elif status in ["completed", "failed"]: + job.completed_at = datetime.now(timezone.utc) + if error_message: + job.error_message = error_message + + await db.flush() + return job + + +# System stats + +async def get_system_stats(db: AsyncSession) -> dict: + """Get system statistics.""" + # Task stats + task_count = await db.execute(select(func.count(Task.id))) + task_count = task_count.scalar() or 0 + + escalated_count = await db.execute( + select(func.count(Task.id)).where(Task.state == "escalated") + ) + escalated_count = escalated_count.scalar() or 0 + + # Agent stats + agent_count = await db.execute(select(func.count(Agent.id))) + agent_count = agent_count.scalar() or 0 + + available_agents = await db.execute( + select(func.count(Agent.id)).where(Agent.status == "available") + ) + available_agents = available_agents.scalar() or 0 + + # Model stats + model_count = await db.execute(select(func.count(Model.id))) + model_count = model_count.scalar() or 0 + + active_models = await db.execute( + select(func.count(Model.id)).where(Model.is_active == True) + ) + active_models = active_models.scalar() or 0 + + # Training job stats + pending_jobs = await db.execute( + select(func.count(TrainingJob.id)).where(TrainingJob.status == "pending") + ) + pending_jobs = pending_jobs.scalar() or 0 + + running_jobs = await db.execute( + select(func.count(TrainingJob.id)).where(TrainingJob.status == "running") + ) + running_jobs = running_jobs.scalar() or 0 + + return { + "tasks": { + "total": task_count, + "escalated": escalated_count + }, + "agents": { + "total": agent_count, + "available": available_agents + }, + "models": { + "total": model_count, + "active": active_models + }, + "training_jobs": { + "pending": pending_jobs, + "running": running_jobs + } + } diff --git a/backend/app/db/models.py b/backend/app/db/models.py new file mode 100644 index 0000000..9e794ee --- /dev/null +++ b/backend/app/db/models.py @@ -0,0 +1,135 @@ +"""Database models for the Agentic platform.""" + +import uuid +from datetime import datetime, timezone +from typing import Optional +from sqlalchemy import String, Text, Boolean, Integer, DateTime, JSON, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.db.base import Base + + +def generate_uuid() -> str: + return str(uuid.uuid4()) + + +def utc_now() -> datetime: + return datetime.now(timezone.utc) + + +class Event(Base): + """Event store for all system events.""" + __tablename__ = "events" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) + event_type: Mapped[str] = mapped_column(String(100), nullable=False, index=True) + entity_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True) + entity_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) + data: Mapped[dict] = mapped_column(JSON, default=dict) + user_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) + + +class Task(Base): + """Tasks managed by the agent orchestration system.""" + __tablename__ = "tasks" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) + title: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + task_type: Mapped[str] = mapped_column(String(50), nullable=False, default="default") + priority: Mapped[str] = mapped_column(String(20), nullable=False, default="medium") + state: Mapped[str] = mapped_column(String(20), nullable=False, default="queued", index=True) + payload: Mapped[dict] = mapped_column(JSON, default=dict) + assigned_agent_id: Mapped[Optional[str]] = mapped_column(String(36), ForeignKey("agents.id"), nullable=True) + retry_count: Mapped[int] = mapped_column(Integer, default=0) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) + + # Relationships + agent: Mapped[Optional["Agent"]] = relationship("Agent", back_populates="tasks") + + +class Agent(Base): + """Agent definitions and configurations.""" + __tablename__ = "agents" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) + name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) + agent_type: Mapped[str] = mapped_column(String(50), nullable=False) + status: Mapped[str] = mapped_column(String(20), nullable=False, default="available") + capabilities: Mapped[list] = mapped_column(JSON, default=list) + config: Mapped[dict] = mapped_column(JSON, default=dict) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) + + # Relationships + tasks: Mapped[list["Task"]] = relationship("Task", back_populates="agent") + models: Mapped[list["Model"]] = relationship("Model", back_populates="agent") + feedback: Mapped[list["Feedback"]] = relationship("Feedback", back_populates="agent") + + +class Audit(Base): + """Audit log for tracking all administrative actions.""" + __tablename__ = "audits" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) + action: Mapped[str] = mapped_column(String(100), nullable=False, index=True) + entity_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True) + entity_id: Mapped[str] = mapped_column(String(36), nullable=False) + user_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True) + details: Mapped[dict] = mapped_column(JSON, default=dict) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) + + +class Feedback(Base): + """Feedback data for agent learning.""" + __tablename__ = "feedback" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) + task_id: Mapped[str] = mapped_column(String(36), ForeignKey("tasks.id"), nullable=False) + agent_id: Mapped[str] = mapped_column(String(36), ForeignKey("agents.id"), nullable=False) + feedback_type: Mapped[str] = mapped_column(String(50), nullable=False) + content: Mapped[dict] = mapped_column(JSON, default=dict) + source_user_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) + + # Relationships + agent: Mapped["Agent"] = relationship("Agent", back_populates="feedback") + + +class Model(Base): + """Model registry for tracking trained models.""" + __tablename__ = "models" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) + name: Mapped[str] = mapped_column(String(100), nullable=False, index=True) + version: Mapped[str] = mapped_column(String(50), nullable=False) + agent_id: Mapped[str] = mapped_column(String(36), ForeignKey("agents.id"), nullable=False) + training_job_id: Mapped[Optional[str]] = mapped_column(String(36), ForeignKey("training_jobs.id"), nullable=True) + metrics: Mapped[dict] = mapped_column(JSON, default=dict) + artifact_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) + + # Relationships + agent: Mapped["Agent"] = relationship("Agent", back_populates="models") + training_job: Mapped[Optional["TrainingJob"]] = relationship("TrainingJob", back_populates="model") + + +class TrainingJob(Base): + """Training job tracking for ML pipeline.""" + __tablename__ = "training_jobs" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) + model_name: Mapped[str] = mapped_column(String(100), nullable=False) + agent_id: Mapped[str] = mapped_column(String(36), ForeignKey("agents.id"), nullable=False) + status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending", index=True) + config: Mapped[dict] = mapped_column(JSON, default=dict) + error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) + started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + + # Relationships + model: Mapped[Optional["Model"]] = relationship("Model", back_populates="training_job") diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 0000000..1640e64 --- /dev/null +++ b/backend/app/db/session.py @@ -0,0 +1,38 @@ +"""Database session management.""" + +from typing import AsyncGenerator +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker + +from app.core import get_settings + + +settings = get_settings() + +# Create async engine +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DATABASE_ECHO, + future=True +) + +# Create async session factory +async_session_maker = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + autocommit=False, + autoflush=False +) + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """Dependency to get database session.""" + async with async_session_maker() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..7159432 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,95 @@ +"""FastAPI application main entry point.""" + +import asyncio +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +import redis.asyncio as redis + +from app.core import get_settings +from app.db.session import engine, async_session_maker +from app.db.base import Base +from app.api import decisions, oversight_ws, auth, admin + + +settings = get_settings() + +# Redis connection pool +redis_pool: redis.Redis | None = None + + +async def init_redis(): + """Initialize Redis connection pool.""" + global redis_pool + redis_pool = redis.from_url( + settings.REDIS_URL, + encoding="utf-8", + decode_responses=True + ) + return redis_pool + + +async def close_redis(): + """Close Redis connection pool.""" + global redis_pool + if redis_pool: + await redis_pool.close() + + +def get_redis() -> redis.Redis: + """Get Redis connection.""" + if redis_pool is None: + raise RuntimeError("Redis pool not initialized") + return redis_pool + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan manager.""" + # Startup + await init_redis() + + # Create tables (for development; use migrations in production) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + yield + + # Shutdown + await close_redis() + await engine.dispose() + + +app = FastAPI( + title=settings.APP_NAME, + description="Agentic Agent Platform API", + version="1.0.0", + lifespan=lifespan +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173", "http://localhost:3000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(auth.router, prefix=f"{settings.API_V1_PREFIX}/auth", tags=["auth"]) +app.include_router(decisions.router, prefix=f"{settings.API_V1_PREFIX}/decisions", tags=["decisions"]) +app.include_router(admin.router, prefix=f"{settings.API_V1_PREFIX}/admin", tags=["admin"]) +app.include_router(oversight_ws.router, prefix=f"{settings.API_V1_PREFIX}/oversight", tags=["oversight"]) + + +@app.get("/") +async def root(): + """Root endpoint.""" + return {"message": "Agentic Agent Platform API", "version": "1.0.0"} + + +@app.get("/health") +async def health(): + """Health check endpoint.""" + return {"status": "healthy"} diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..cdce43f --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +asyncio_mode = auto +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..7b75ab4 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,32 @@ +# FastAPI and server +fastapi>=0.109.1 +uvicorn[standard]>=0.22.0 +python-multipart>=0.0.6 + +# Database +sqlalchemy[asyncio]>=2.0.0 +asyncpg>=0.28.0 +alembic>=1.11.0 +psycopg2-binary>=2.9.0 + +# Redis +redis>=4.6.0 + +# Authentication +python-jose[cryptography]>=3.4.0 +authlib>=1.2.0 +httpx>=0.24.0 + +# Task queue (Celery) +celery[redis]>=5.3.0 + +# Testing +pytest>=7.4.0 +pytest-asyncio>=0.21.0 +pytest-cov>=4.1.0 +httpx>=0.24.0 + +# Utilities +pydantic>=2.0.0 +pydantic-settings>=2.0.0 +python-dotenv>=1.0.0 diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..11754ee --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +# Tests module diff --git a/backend/tests/test_ack_flow.py b/backend/tests/test_ack_flow.py new file mode 100644 index 0000000..4692426 --- /dev/null +++ b/backend/tests/test_ack_flow.py @@ -0,0 +1,276 @@ +"""Tests for the ack flow and escalation mechanism.""" + +import asyncio +import json +import pytest +from unittest.mock import AsyncMock, MagicMock, patch +from datetime import datetime, timezone + +from app.agents.communication import ( + AgentCommunicationSystem, + AgentMessage, + TaskState +) + + +class MockRedis: + """Mock Redis client for testing.""" + + def __init__(self): + self.published = [] + self.lists = {} + self._pubsub = MockPubSub() + + async def publish(self, channel: str, message: str): + self.published.append({"channel": channel, "message": message}) + return 1 + + async def lpush(self, key: str, value: str): + if key not in self.lists: + self.lists[key] = [] + self.lists[key].insert(0, value) + return len(self.lists[key]) + + async def rpop(self, key: str): + if key in self.lists and self.lists[key]: + return self.lists[key].pop() + return None + + def pubsub(self): + return self._pubsub + + +class MockPubSub: + """Mock PubSub for testing.""" + + def __init__(self): + self.subscribed = [] + self.messages = [] + + async def subscribe(self, channel: str): + self.subscribed.append(channel) + + async def unsubscribe(self, channel: str): + if channel in self.subscribed: + self.subscribed.remove(channel) + + async def close(self): + pass + + async def listen(self): + for msg in self.messages: + yield msg + + +@pytest.fixture +def mock_redis(): + """Create mock Redis client.""" + return MockRedis() + + +@pytest.fixture +def comm_system(mock_redis): + """Create AgentCommunicationSystem with mock Redis.""" + return AgentCommunicationSystem(mock_redis) + + +class TestAgentMessage: + """Tests for AgentMessage class.""" + + def test_create_message(self): + """Test creating an agent message.""" + msg = AgentMessage( + message_id="msg-123", + task_id="task-456", + message_type="task_assignment", + payload={"action": "test"}, + sender_id="orchestrator" + ) + + assert msg.message_id == "msg-123" + assert msg.task_id == "task-456" + assert msg.message_type == "task_assignment" + assert msg.payload == {"action": "test"} + assert msg.sender_id == "orchestrator" + assert msg.correlation_id == "msg-123" + + def test_message_to_dict(self): + """Test converting message to dictionary.""" + msg = AgentMessage( + message_id="msg-123", + task_id="task-456", + message_type="task_assignment", + payload={"action": "test"}, + sender_id="orchestrator" + ) + + data = msg.to_dict() + + assert data["message_id"] == "msg-123" + assert data["task_id"] == "task-456" + assert data["message_type"] == "task_assignment" + assert data["payload"] == {"action": "test"} + assert "timestamp" in data + + def test_message_from_dict(self): + """Test creating message from dictionary.""" + data = { + "message_id": "msg-123", + "task_id": "task-456", + "message_type": "task_assignment", + "payload": {"action": "test"}, + "sender_id": "orchestrator", + "timestamp": datetime.now(timezone.utc).isoformat() + } + + msg = AgentMessage.from_dict(data) + + assert msg.message_id == "msg-123" + assert msg.task_id == "task-456" + + +class TestAgentCommunicationSystem: + """Tests for AgentCommunicationSystem.""" + + @pytest.mark.asyncio + async def test_publish_task_timeout(self, comm_system, mock_redis): + """Test that task publication times out when no ack received.""" + # Set very short timeout for testing + with patch.object(comm_system, 'redis', mock_redis): + success, ack_data, retry_count = await comm_system.publish_task( + task_id="task-123", + payload={"action": "test"}, + timeout=0.01, # Very short timeout + max_retries=0 # No retries + ) + + assert success is False + assert ack_data is None + # retry_count is 1 after loop exits (incremented when timeout occurs) + + @pytest.mark.asyncio + async def test_publish_task_with_retries(self, comm_system, mock_redis): + """Test that task publication retries on timeout.""" + with patch.object(comm_system, 'redis', mock_redis): + success, ack_data, retry_count = await comm_system.publish_task( + task_id="task-123", + payload={"action": "test"}, + timeout=0.01, + max_retries=2 + ) + + assert success is False + assert retry_count == 3 # Initial + 2 retries + + # Verify multiple publishes occurred + assert len(mock_redis.published) >= 2 + + @pytest.mark.asyncio + async def test_escalate_task(self, comm_system, mock_redis): + """Test task escalation after max retries.""" + with patch.object(comm_system, 'redis', mock_redis): + success, ack_data, retry_count = await comm_system.publish_task( + task_id="task-123", + payload={"action": "test"}, + timeout=0.01, + max_retries=1 + ) + + assert success is False + + # Check escalation was published + escalation_messages = [ + p for p in mock_redis.published + if p["channel"] == "agentic:escalations" + ] + assert len(escalation_messages) == 1 + + # Check escalation queue + assert "agentic:escalation_queue" in mock_redis.lists + assert len(mock_redis.lists["agentic:escalation_queue"]) == 1 + + @pytest.mark.asyncio + async def test_send_ack(self, comm_system, mock_redis): + """Test sending acknowledgement.""" + with patch.object(comm_system, 'redis', mock_redis): + await comm_system.send_ack( + correlation_id="corr-123", + task_id="task-456", + agent_id="agent-789", + status="acked", + metadata={"processed": True} + ) + + # Verify ack was published + ack_messages = [ + p for p in mock_redis.published + if p["channel"] == "agentic:acks" + ] + assert len(ack_messages) == 1 + + ack_data = json.loads(ack_messages[0]["message"]) + assert ack_data["correlation_id"] == "corr-123" + assert ack_data["task_id"] == "task-456" + assert ack_data["agent_id"] == "agent-789" + + @pytest.mark.asyncio + async def test_get_escalated_tasks(self, comm_system, mock_redis): + """Test retrieving escalated tasks.""" + # Add some escalated tasks to queue + for i in range(3): + await mock_redis.lpush( + "agentic:escalation_queue", + json.dumps({"task_id": f"task-{i}"}) + ) + + with patch.object(comm_system, 'redis', mock_redis): + tasks = await comm_system.get_escalated_tasks(count=2) + + assert len(tasks) == 2 + + +class TestTaskState: + """Tests for TaskState enum.""" + + def test_task_states(self): + """Test all task states are defined.""" + assert TaskState.QUEUED.value == "queued" + assert TaskState.ASSIGNED.value == "assigned" + assert TaskState.ACKED.value == "acked" + assert TaskState.IN_PROGRESS.value == "in_progress" + assert TaskState.COMPLETED.value == "completed" + assert TaskState.VERIFIED.value == "verified" + assert TaskState.FAILED.value == "failed" + assert TaskState.ESCALATED.value == "escalated" + + +class TestExponentialBackoff: + """Tests for exponential backoff behavior.""" + + @pytest.mark.asyncio + async def test_backoff_increases(self, comm_system, mock_redis): + """Test that backoff increases with retries.""" + timeouts = [] + original_wait_for = asyncio.wait_for + + async def mock_wait_for(coro, timeout): + timeouts.append(timeout) + raise asyncio.TimeoutError() + + with patch('asyncio.wait_for', mock_wait_for): + with patch.object(comm_system, 'redis', mock_redis): + await comm_system.publish_task( + task_id="task-123", + payload={}, + timeout=1.0, + max_retries=2 + ) + + # Verify exponential increase (base 2.0) + # Retry 0: 1.0 * 2^0 = 1.0 + # Retry 1: 1.0 * 2^1 = 2.0 + # Retry 2: 1.0 * 2^2 = 4.0 + assert len(timeouts) == 3 + assert timeouts[0] == pytest.approx(1.0) + assert timeouts[1] == pytest.approx(2.0) + assert timeouts[2] == pytest.approx(4.0) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 0000000..bdc8588 --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,160 @@ +"""Tests for the API endpoints.""" + +import pytest +from httpx import AsyncClient, ASGITransport +from unittest.mock import patch, AsyncMock + +from app.main import app +from app.api.auth import create_access_token, Role + + +@pytest.fixture +def admin_token(): + """Create admin token for testing.""" + return create_access_token({ + "sub": "admin_user", + "email": "admin@test.com", + "roles": [Role.ADMIN] + }) + + +@pytest.fixture +def viewer_token(): + """Create viewer token for testing.""" + return create_access_token({ + "sub": "viewer_user", + "email": "viewer@test.com", + "roles": [Role.VIEWER] + }) + + +@pytest.fixture +def reviewer_token(): + """Create reviewer token for testing.""" + return create_access_token({ + "sub": "reviewer_user", + "email": "reviewer@test.com", + "roles": [Role.REVIEWER] + }) + + +class TestAuthEndpoints: + """Tests for authentication endpoints.""" + + @pytest.mark.asyncio + async def test_login(self): + """Test login endpoint.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.post( + "/api/v1/auth/token", + data={"username": "testuser", "password": "testpass"} + ) + + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + + @pytest.mark.asyncio + async def test_login_admin_role(self): + """Test that admin username gets admin role.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.post( + "/api/v1/auth/token", + data={"username": "admin_test", "password": "pass"} + ) + + assert response.status_code == 200 + + @pytest.mark.asyncio + async def test_get_current_user(self, admin_token): + """Test getting current user profile.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get( + "/api/v1/auth/me", + headers={"Authorization": f"Bearer {admin_token}"} + ) + + assert response.status_code == 200 + data = response.json() + assert data["id"] == "admin_user" + assert Role.ADMIN in data["roles"] + + @pytest.mark.asyncio + async def test_unauthorized_without_token(self): + """Test that endpoints require authentication.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/api/v1/auth/me") + + assert response.status_code == 401 + + +class TestRBACEndpoints: + """Tests for RBAC enforcement.""" + + @pytest.mark.asyncio + async def test_viewer_cannot_create_task(self, viewer_token): + """Test that viewers cannot create tasks.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.post( + "/api/v1/decisions/task", + headers={"Authorization": f"Bearer {viewer_token}"}, + json={ + "title": "Test Task", + "description": "Test Description", + "task_type": "test", + "priority": "medium" + } + ) + + assert response.status_code == 403 + + @pytest.mark.asyncio + async def test_viewer_cannot_access_admin_endpoints(self, viewer_token): + """Test that viewers cannot access admin endpoints.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get( + "/api/v1/admin/audits", + headers={"Authorization": f"Bearer {viewer_token}"} + ) + + assert response.status_code == 403 + + @pytest.mark.asyncio + async def test_reviewer_can_override(self, reviewer_token): + """Test that reviewers can perform overrides.""" + # This test would require database setup + # For now, we verify the endpoint exists and requires proper role + pass + + +class TestHealthEndpoints: + """Tests for health check endpoints.""" + + @pytest.mark.asyncio + async def test_root_endpoint(self): + """Test root endpoint.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/") + + assert response.status_code == 200 + data = response.json() + assert "version" in data + + @pytest.mark.asyncio + async def test_health_endpoint(self): + """Test health check endpoint.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/health") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d5c4c6d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,103 @@ +# Agentic Agent Platform - Docker Compose Configuration +# +# Usage: +# docker-compose up --build +# +# Services: +# - postgres: PostgreSQL database +# - redis: Redis for pub/sub and caching +# - backend: FastAPI backend API +# - frontend: React oversight UI + +services: + postgres: + image: postgres:15-alpine + container_name: agentic-postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: agentic + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: agentic-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: agentic-backend + environment: + DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/agentic + REDIS_URL: redis://redis:6379/0 + CELERY_BROKER_URL: redis://redis:6379/1 + CELERY_RESULT_BACKEND: redis://redis:6379/2 + # OIDC Configuration (uncomment and configure for production) + # OIDC_ISSUER: https://your-idp.com + # OIDC_CLIENT_ID: your-client-id + # OIDC_CLIENT_SECRET: your-client-secret + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-dev-secret-key-change-in-production} + DEBUG: ${DEBUG:-false} + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: agentic-frontend + ports: + - "3000:80" + depends_on: + - backend + + # Optional: Celery worker for training jobs + # celery-worker: + # build: + # context: ./backend + # dockerfile: Dockerfile + # container_name: agentic-celery + # command: celery -A app.tasks.celery_app worker --loglevel=info + # environment: + # DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/agentic + # REDIS_URL: redis://redis:6379/0 + # CELERY_BROKER_URL: redis://redis:6379/1 + # CELERY_RESULT_BACKEND: redis://redis:6379/2 + # depends_on: + # postgres: + # condition: service_healthy + # redis: + # condition: service_healthy + +volumes: + postgres_data: + redis_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b725e56 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,30 @@ +# Agentic Agent Platform Frontend + +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source +COPY . . + +# Build +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built files +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..e78814e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,24 @@ + + + + + + + Agentic Agent Platform - Oversight Dashboard + + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..f61eef3 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,43 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # API proxy + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # WebSocket proxy + location /api/v1/oversight/ws { + proxy_pass http://backend:8000/api/v1/oversight/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400; + } + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..a03f973 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4857 @@ +{ + "name": "agentic-oversight-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agentic-oversight-ui", + "version": "1.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0" + }, + "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.2.0", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "vite": "^5.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", + "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.260", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz", + "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0c16b5d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "agentic-oversight-ui", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0" + }, + "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.2.0", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "vite": "^5.0.12" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..8ee7d26 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,63 @@ +import { Routes, Route, Navigate } from 'react-router-dom' +import { useAuth } from './hooks/useAuth.jsx' +import Login from './components/Login.jsx' +import OversightDashboard from './components/OversightDashboard.jsx' + +function ProtectedRoute({ children, requiredRoles = [] }) { + const { user, isLoading } = useAuth() + + if (isLoading) { + return
Loading...
+ } + + if (!user) { + return + } + + if (requiredRoles.length > 0) { + const hasRole = requiredRoles.some(role => user.roles?.includes(role)) + if (!hasRole) { + return
Insufficient permissions
+ } + } + + return children +} + +function App() { + const { user, logout } = useAuth() + + return ( +
+ {user && ( +
+

🤖 Agentic Agent Platform

+
+
+
{user.email}
+
{user.roles?.join(', ')}
+
+ +
+
+ )} + + + } /> + + + + } + /> + } /> + +
+ ) +} + +export default App diff --git a/frontend/src/components/Login.jsx b/frontend/src/components/Login.jsx new file mode 100644 index 0000000..d35d4f0 --- /dev/null +++ b/frontend/src/components/Login.jsx @@ -0,0 +1,135 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuth } from '../hooks/useAuth.jsx' + +function Login() { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const { login, loginWithOIDC, oidcConfig } = useAuth() + const navigate = useNavigate() + + const handleSubmit = async (e) => { + e.preventDefault() + setError(null) + setIsLoading(true) + + try { + await login(username, password) + navigate('/') + } catch (err) { + setError('Login failed. Please check your credentials.') + } finally { + setIsLoading(false) + } + } + + const handleOIDCLogin = () => { + try { + loginWithOIDC() + } catch (err) { + setError('OIDC login is not configured') + } + } + + return ( +
+
+

+ 🤖 Agentic Platform +

+

+ Sign in to Oversight Dashboard +

+ + {error && ( +
{error}
+ )} + +
+
+ + setUsername(e.target.value)} + placeholder="Enter username" + required + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="Enter password" + required + /> +
+ + +
+ + {oidcConfig?.enabled && ( + <> +
+ or +
+ + + )} + +
+ Demo Credentials: +
    +
  • admin_* - Admin role
  • +
  • reviewer_* - Reviewer role
  • +
  • manager_* - Agent Manager role
  • +
  • Any other username - Viewer role
  • +
+

+ Password can be anything in development mode. +

+
+
+
+ ) +} + +export default Login diff --git a/frontend/src/components/OversightDashboard.jsx b/frontend/src/components/OversightDashboard.jsx new file mode 100644 index 0000000..085ebf1 --- /dev/null +++ b/frontend/src/components/OversightDashboard.jsx @@ -0,0 +1,433 @@ +import { useState, useEffect, useRef, useCallback } from 'react' +import { useAuth } from '../hooks/useAuth.jsx' + +const API_BASE = '/api/v1' + +// Role constants matching backend +const ROLES = { + ADMIN: 'admin', + REVIEWER: 'reviewer', + AGENT_MANAGER: 'agent_manager', + VIEWER: 'viewer' +} + +function OversightDashboard() { + const { user, hasRole, getAuthHeader, token } = useAuth() + const [tasks, setTasks] = useState([]) + const [events, setEvents] = useState([]) + const [stats, setStats] = useState(null) + const [escalations, setEscalations] = useState([]) + const [wsConnected, setWsConnected] = useState(false) + const [error, setError] = useState(null) + const wsRef = useRef(null) + + // Fetch tasks + const fetchTasks = useCallback(async () => { + try { + const response = await fetch(`${API_BASE}/decisions/tasks`, { + headers: getAuthHeader() + }) + if (response.ok) { + const data = await response.json() + setTasks(data.tasks || []) + } + } catch (err) { + console.error('Failed to fetch tasks:', err) + } + }, [getAuthHeader]) + + // Fetch events + const fetchEvents = useCallback(async () => { + try { + const response = await fetch(`${API_BASE}/decisions/events?limit=50`, { + headers: getAuthHeader() + }) + if (response.ok) { + const data = await response.json() + setEvents(data.events || []) + } + } catch (err) { + console.error('Failed to fetch events:', err) + } + }, [getAuthHeader]) + + // Fetch stats (admin/agent_manager only) + const fetchStats = useCallback(async () => { + if (!hasRole([ROLES.ADMIN, ROLES.AGENT_MANAGER])) return + + try { + const response = await fetch(`${API_BASE}/admin/stats`, { + headers: getAuthHeader() + }) + if (response.ok) { + const data = await response.json() + setStats(data) + } + } catch (err) { + console.error('Failed to fetch stats:', err) + } + }, [getAuthHeader, hasRole]) + + // Fetch escalations (reviewer/admin only) + const fetchEscalations = useCallback(async () => { + if (!hasRole([ROLES.ADMIN, ROLES.REVIEWER])) return + + try { + const response = await fetch(`${API_BASE}/decisions/escalations`, { + headers: getAuthHeader() + }) + if (response.ok) { + const data = await response.json() + setEscalations(data.escalations || []) + } + } catch (err) { + console.error('Failed to fetch escalations:', err) + } + }, [getAuthHeader, hasRole]) + + // WebSocket connection for real-time updates + useEffect(() => { + if (!token) return + + const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${API_BASE}/oversight/ws?token=${token}` + + const connect = () => { + try { + wsRef.current = new WebSocket(wsUrl) + + wsRef.current.onopen = () => { + setWsConnected(true) + setError(null) + } + + wsRef.current.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + handleWebSocketMessage(data) + } catch (err) { + console.error('Failed to parse WS message:', err) + } + } + + wsRef.current.onclose = () => { + setWsConnected(false) + // Reconnect after 3 seconds + setTimeout(connect, 3000) + } + + wsRef.current.onerror = (err) => { + console.error('WebSocket error:', err) + setError('WebSocket connection failed') + } + } catch (err) { + console.error('Failed to connect WebSocket:', err) + } + } + + connect() + + return () => { + if (wsRef.current) { + wsRef.current.close() + } + } + }, [token]) + + const handleWebSocketMessage = (data) => { + switch (data.type) { + case 'connected': + console.log('WebSocket connected:', data) + break + case 'event': + case 'task_state_changed': + // Refresh data on events + fetchTasks() + fetchEvents() + fetchEscalations() + break + default: + console.log('WebSocket message:', data) + } + } + + // Initial data fetch + useEffect(() => { + fetchTasks() + fetchEvents() + fetchStats() + fetchEscalations() + }, [fetchTasks, fetchEvents, fetchStats, fetchEscalations]) + + // Override task (reviewer/admin only) + const handleOverride = async (taskId) => { + const reason = prompt('Enter reason for override:') + if (!reason) return + + try { + const response = await fetch(`${API_BASE}/decisions/task/${taskId}/override`, { + method: 'POST', + headers: { + ...getAuthHeader(), + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + reason, + new_decision: 'manual_override', + metadata: { overridden_at: new Date().toISOString() } + }) + }) + + if (response.ok) { + fetchTasks() + fetchEvents() + alert('Override successful') + } else { + const data = await response.json() + alert(`Override failed: ${data.detail}`) + } + } catch (err) { + alert(`Override failed: ${err.message}`) + } + } + + // Escalate task + const handleEscalate = async (taskId) => { + const reason = prompt('Enter reason for escalation:') + if (!reason) return + + try { + const response = await fetch(`${API_BASE}/decisions/task/${taskId}/escalate?reason=${encodeURIComponent(reason)}`, { + method: 'POST', + headers: getAuthHeader() + }) + + if (response.ok) { + fetchTasks() + fetchEvents() + fetchEscalations() + alert('Escalation successful') + } else { + const data = await response.json() + alert(`Escalation failed: ${data.detail}`) + } + } catch (err) { + alert(`Escalation failed: ${err.message}`) + } + } + + // Send WebSocket action + const sendWsAction = (action, taskId, data = {}) => { + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ + type: 'action', + action, + task_id: taskId, + data + })) + } + } + + const getStateBadgeClass = (state) => { + switch (state) { + case 'completed': + case 'verified': + return 'badge-success' + case 'failed': + case 'escalated': + return 'badge-danger' + case 'in_progress': + case 'acked': + return 'badge-info' + default: + return 'badge-warning' + } + } + + return ( +
+ {error &&
{error}
} + + {/* Connection Status */} +
+ + + {wsConnected ? 'Connected to real-time updates' : 'Disconnected'} + +
+ + {/* Stats Cards (Admin/Agent Manager only) */} + {stats && hasRole([ROLES.ADMIN, ROLES.AGENT_MANAGER]) && ( +
+
+
{stats.tasks?.total || 0}
+
Total Tasks
+
+
+
{stats.tasks?.escalated || 0}
+
Escalated
+
+
+
{stats.agents?.available || 0} / {stats.agents?.total || 0}
+
Agents Available
+
+
+
{stats.models?.active || 0}
+
Active Models
+
+
+ )} + +
+ {/* Tasks Panel */} +
+
+

Tasks

+ +
+ + {tasks.length === 0 ? ( +

No tasks found

+ ) : ( + + + + + + + + + + + {tasks.map(task => ( + + + + + + + ))} + +
TitleTypeStateActions
{task.title}{task.task_type} + + {task.state} + + + {/* Override button - Reviewer/Admin only */} + {hasRole([ROLES.REVIEWER, ROLES.ADMIN]) && ( + + )} + + {/* Escalate button - Agent Manager/Reviewer/Admin */} + {hasRole([ROLES.AGENT_MANAGER, ROLES.REVIEWER, ROLES.ADMIN]) && task.state !== 'escalated' && ( + + )} +
+ )} +
+ + {/* Events Panel */} +
+
+

Recent Events

+ +
+ + {events.length === 0 ? ( +

No events found

+ ) : ( +
+ {events.slice(0, 20).map(event => ( +
+
{event.event_type}
+
+ {event.entity_type} / {event.entity_id} +
+
+ {new Date(event.created_at).toLocaleString()} +
+
+ ))} +
+ )} +
+
+ + {/* Escalations Panel (Reviewer/Admin only) */} + {hasRole([ROLES.REVIEWER, ROLES.ADMIN]) && ( +
+
+

⚠️ Escalated Tasks

+ +
+ + {escalations.length === 0 ? ( +

No escalated tasks

+ ) : ( + + + + + + + + + + + + {escalations.map(task => ( + + + + + + + + ))} + +
TitleTypePriorityRetriesActions
{task.title}{task.task_type}{task.priority}{task.retry_count} + +
+ )} +
+ )} +
+ ) +} + +export default OversightDashboard diff --git a/frontend/src/hooks/useAuth.jsx b/frontend/src/hooks/useAuth.jsx new file mode 100644 index 0000000..f395ac4 --- /dev/null +++ b/frontend/src/hooks/useAuth.jsx @@ -0,0 +1,176 @@ +import { createContext, useContext, useState, useEffect, useCallback } from 'react' + +const AuthContext = createContext(null) + +const API_BASE = '/api/v1' + +// OIDC Configuration +const getOIDCConfig = async () => { + try { + const response = await fetch(`${API_BASE}/auth/oidc/config`) + if (response.ok) { + return await response.json() + } + } catch (error) { + console.warn('OIDC not configured:', error) + } + return null +} + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null) + const [token, setToken] = useState(() => localStorage.getItem('auth_token')) + const [isLoading, setIsLoading] = useState(true) + const [oidcConfig, setOidcConfig] = useState(null) + + // Load OIDC config on mount + useEffect(() => { + getOIDCConfig().then(setOidcConfig) + }, []) + + // Load user profile on token change + useEffect(() => { + const loadUser = async () => { + if (!token) { + setUser(null) + setIsLoading(false) + return + } + + try { + const response = await fetch(`${API_BASE}/auth/me`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }) + + if (response.ok) { + const userData = await response.json() + setUser(userData) + } else { + // Token invalid + localStorage.removeItem('auth_token') + setToken(null) + setUser(null) + } + } catch (error) { + console.error('Failed to load user:', error) + setUser(null) + } finally { + setIsLoading(false) + } + } + + loadUser() + }, [token]) + + // Login with username/password + const login = useCallback(async (username, password) => { + const response = await fetch(`${API_BASE}/auth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ username, password }) + }) + + if (!response.ok) { + throw new Error('Login failed') + } + + const data = await response.json() + localStorage.setItem('auth_token', data.access_token) + setToken(data.access_token) + return data + }, []) + + // OIDC login redirect + const loginWithOIDC = useCallback(() => { + if (!oidcConfig?.enabled) { + throw new Error('OIDC not configured') + } + + const state = crypto.randomUUID() + sessionStorage.setItem('oidc_state', state) + + const params = new URLSearchParams({ + client_id: oidcConfig.client_id, + redirect_uri: `${window.location.origin}/auth/callback`, + response_type: 'code', + scope: 'openid email profile', + state + }) + + window.location.href = `${oidcConfig.authorization_endpoint}?${params}` + }, [oidcConfig]) + + // Handle OIDC callback + const handleOIDCCallback = useCallback(async (code, state) => { + const savedState = sessionStorage.getItem('oidc_state') + if (state !== savedState) { + throw new Error('Invalid state') + } + sessionStorage.removeItem('oidc_state') + + const response = await fetch(`${API_BASE}/auth/oidc/callback?code=${code}&state=${state}`, { + method: 'POST' + }) + + if (!response.ok) { + throw new Error('OIDC callback failed') + } + + const data = await response.json() + localStorage.setItem('auth_token', data.access_token) + setToken(data.access_token) + return data + }, []) + + // Logout + const logout = useCallback(() => { + localStorage.removeItem('auth_token') + setToken(null) + setUser(null) + }, []) + + // Check if user has specific role(s) + const hasRole = useCallback((requiredRoles) => { + if (!user?.roles) return false + if (typeof requiredRoles === 'string') { + return user.roles.includes(requiredRoles) + } + return requiredRoles.some(role => user.roles.includes(role)) + }, [user]) + + // Get auth header for API calls + const getAuthHeader = useCallback(() => { + return token ? { 'Authorization': `Bearer ${token}` } : {} + }, [token]) + + const value = { + user, + token, + isLoading, + oidcConfig, + login, + loginWithOIDC, + handleOIDCCallback, + logout, + hasRole, + getAuthHeader + } + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..9b37444 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,287 @@ +:root { + --primary-color: #2563eb; + --primary-dark: #1d4ed8; + --danger-color: #dc2626; + --success-color: #16a34a; + --warning-color: #f59e0b; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-300: #d1d5db; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-900: #111827; +} + +* { + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background-color: var(--gray-100); + color: var(--gray-900); + line-height: 1.5; +} + +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.header { + background: white; + border-bottom: 1px solid var(--gray-200); + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.header h1 { + font-size: 1.25rem; + font-weight: 600; +} + +.main { + flex: 1; + padding: 2rem; + max-width: 1400px; + margin: 0 auto; + width: 100%; +} + +.btn { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.15s ease; + border: none; +} + +.btn-primary { + background: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background: var(--primary-dark); +} + +.btn-danger { + background: var(--danger-color); + color: white; +} + +.btn-outline { + background: transparent; + border: 1px solid var(--gray-300); + color: var(--gray-700); +} + +.btn-outline:hover { + background: var(--gray-100); +} + +.card { + background: white; + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + margin-bottom: 1rem; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--gray-200); +} + +.card-title { + font-size: 1.125rem; + font-weight: 600; +} + +.badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; +} + +.badge-success { + background: #dcfce7; + color: #166534; +} + +.badge-warning { + background: #fef3c7; + color: #92400e; +} + +.badge-danger { + background: #fee2e2; + color: #991b1b; +} + +.badge-info { + background: #dbeafe; + color: #1e40af; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid var(--gray-200); +} + +.table th { + font-weight: 600; + font-size: 0.875rem; + color: var(--gray-600); +} + +.table tbody tr:hover { + background: var(--gray-100); +} + +.login-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.login-card { + background: white; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-label { + display: block; + font-weight: 500; + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.form-input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid var(--gray-300); + border-radius: 0.375rem; + font-size: 1rem; +} + +.form-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); +} + +.grid { + display: grid; + gap: 1rem; +} + +.grid-cols-2 { + grid-template-columns: repeat(2, 1fr); +} + +.grid-cols-3 { + grid-template-columns: repeat(3, 1fr); +} + +.grid-cols-4 { + grid-template-columns: repeat(4, 1fr); +} + +.stat-card { + background: white; + padding: 1.25rem; + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: var(--gray-900); +} + +.stat-label { + font-size: 0.875rem; + color: var(--gray-600); + margin-top: 0.25rem; +} + +.alert { + padding: 1rem; + border-radius: 0.375rem; + margin-bottom: 1rem; +} + +.alert-danger { + background: #fee2e2; + color: #991b1b; + border: 1px solid #fecaca; +} + +.alert-success { + background: #dcfce7; + color: #166534; + border: 1px solid #bbf7d0; +} + +.user-menu { + display: flex; + align-items: center; + gap: 1rem; +} + +.user-info { + text-align: right; +} + +.user-name { + font-weight: 500; +} + +.user-role { + font-size: 0.75rem; + color: var(--gray-600); +} + +@media (max-width: 768px) { + .grid-cols-2, + .grid-cols-3, + .grid-cols-4 { + grid-template-columns: 1fr; + } + + .header { + flex-direction: column; + gap: 1rem; + } +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..55b77ca --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,16 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App.jsx' +import { AuthProvider } from './hooks/useAuth.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + + + , +) diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..b82319f --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + sourcemap: true, + }, +})