Compare commits
99
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd57c75e41 | ||
|
|
237a863dfd | ||
|
|
cb92ba16c1 | ||
|
|
70e9f2c6bc | ||
|
|
a760401407 | ||
|
|
22a5e9791c | ||
|
|
d1bef49b4e | ||
|
|
76abf11eba | ||
|
|
c4850fe6c1 | ||
|
|
0809f4e787 | ||
|
|
6a4c020179 | ||
|
|
3bb401641e | ||
|
|
54b821d8bd | ||
|
|
09e649fc7e | ||
|
|
f208f72dc0 | ||
|
|
d42cd68ea4 | ||
|
|
07647c8382 | ||
|
|
8633823257 | ||
|
|
d0999a8e37 | ||
|
|
ea800e3f14 | ||
|
|
5d2e6fae63 | ||
|
|
fcd22c788a | ||
|
|
ab61a15edc | ||
|
|
2c60459851 | ||
|
|
ea524a6ba1 | ||
|
|
997a6d134f | ||
|
|
8aaf229483 | ||
|
|
049bb719e8 | ||
|
|
014ebdacda | ||
|
|
72a73c859c | ||
|
|
6d2b81f6e4 | ||
|
|
9d01a0d484 | ||
|
|
d5102f62fa | ||
|
|
a881e707e2 | ||
|
|
7d04874f3c | ||
|
|
9f036242fa | ||
|
|
c4e52085e3 | ||
|
|
84e1868028 | ||
|
|
f94f9f672b | ||
|
|
cd29fc8708 | ||
|
|
6e22c0fdeb | ||
|
|
1f4d54e474 | ||
|
|
b7a39b45d7 | ||
|
|
1bfdc91f90 | ||
|
|
58a90ac9d7 | ||
|
|
684dbdc6a4 | ||
|
|
e92de12cf9 | ||
|
|
1f784a6a04 | ||
|
|
ab37c2e69f | ||
|
|
c8f3e0db44 | ||
|
|
02772a3910 | ||
|
|
85a25fd995 | ||
|
|
20f302367c | ||
|
|
54c6bfded0 | ||
|
|
ca5472bc31 | ||
|
|
55b5a31c3c | ||
|
|
01e9891243 | ||
|
|
446a424c1f | ||
|
|
02a0d515d9 | ||
|
|
2bf3816efc | ||
|
|
96902bab44 | ||
|
|
280c5351e2 | ||
|
|
9eb48e1d9b | ||
|
|
d83ebe65e9 | ||
|
|
4fe7d09e5c | ||
|
|
e44cb7e56a | ||
|
|
fd4b7c2ba2 | ||
|
|
a1a1976b38 | ||
|
|
f0d1d4bafa | ||
|
|
600da70960 | ||
|
|
780f85e0d6 | ||
|
|
5d936d58a0 | ||
|
|
8da2759fec | ||
|
|
b03c603759 | ||
|
|
77da12a5ee | ||
|
|
7f6dc43a2d | ||
|
|
f3a7eadcea | ||
|
|
7f6815feaf | ||
|
|
7485f32e69 | ||
|
|
95f95f54cf | ||
|
|
aa9ee75a2a | ||
|
|
c54b69f7ce | ||
|
|
38897fe423 | ||
|
|
cbac5902db | ||
|
|
4cd5cbf893 | ||
|
|
6e3cccc812 | ||
|
|
2b1723e898 | ||
|
|
573484c83e | ||
|
|
bf2cb3a271 | ||
|
|
35e4e2e527 | ||
|
|
f6f05cf23a | ||
|
|
15352448d5 | ||
|
|
c58003e5e3 | ||
|
|
04528459ec | ||
|
|
4f84a01072 | ||
|
|
888bc32be1 | ||
|
|
6d0d288e31 | ||
|
|
339641352e | ||
|
|
e7f338e3a9 |
@@ -1,58 +0,0 @@
|
||||
# Dependencies (installed fresh in Docker)
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Build outputs (built fresh in Docker)
|
||||
dist
|
||||
**/dist
|
||||
.next
|
||||
**/.next
|
||||
|
||||
# TurboRepo cache
|
||||
.turbo
|
||||
**/.turbo
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Credentials
|
||||
.admin-credentials
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
**/coverage
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
*.tsbuildinfo
|
||||
**/*.tsbuildinfo
|
||||
.pnpm-approve-builds
|
||||
.husky/_
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# Documentation (not needed in container)
|
||||
docs
|
||||
*.md
|
||||
!README.md
|
||||
+86
-468
@@ -1,511 +1,129 @@
|
||||
# ==============================================
|
||||
# Mosaic Stack Environment Configuration
|
||||
# ==============================================
|
||||
# Copy this file to .env and customize for your environment
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Mosaic — Environment Variables Reference
|
||||
# Copy this file to .env and fill in the values for your deployment.
|
||||
# Lines beginning with # are comments; optional vars are commented out.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ======================
|
||||
# Application Ports
|
||||
# ======================
|
||||
API_PORT=3001
|
||||
API_HOST=0.0.0.0
|
||||
WEB_PORT=3000
|
||||
|
||||
# ======================
|
||||
# Web Configuration
|
||||
# ======================
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||
# Frontend auth mode:
|
||||
# - real: Normal auth/session flow
|
||||
# - mock: Local-only seeded user for FE development (blocked outside NODE_ENV=development)
|
||||
# Use `mock` locally to continue FE work when auth flow is unstable.
|
||||
# If omitted, web runtime defaults:
|
||||
# - development -> mock
|
||||
# - production -> real
|
||||
NEXT_PUBLIC_AUTH_MODE=real
|
||||
# ─── Database (PostgreSQL 17 + pgvector) ─────────────────────────────────────
|
||||
# Full connection string used by the gateway, ORM, and migration runner.
|
||||
# Port 5433 avoids conflict with a host-side PostgreSQL instance.
|
||||
DATABASE_URL=postgresql://mosaic:mosaic@localhost:5433/mosaic
|
||||
|
||||
# ======================
|
||||
# PostgreSQL Database
|
||||
# ======================
|
||||
# Bundled PostgreSQL
|
||||
# SECURITY: Change POSTGRES_PASSWORD to a strong random password in production
|
||||
DATABASE_URL=postgresql://mosaic:REPLACE_WITH_SECURE_PASSWORD@postgres:5432/mosaic
|
||||
POSTGRES_USER=mosaic
|
||||
POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
POSTGRES_DB=mosaic
|
||||
POSTGRES_PORT=5432
|
||||
# Docker Compose host-port override for the PostgreSQL container (default: 5433)
|
||||
# PG_HOST_PORT=5433
|
||||
|
||||
# External PostgreSQL (managed service)
|
||||
# To use an external instance, update DATABASE_URL above
|
||||
# Example: DATABASE_URL=postgresql://user:[email protected]:5432/mosaic
|
||||
|
||||
# PostgreSQL Performance Tuning (Optional)
|
||||
POSTGRES_SHARED_BUFFERS=256MB
|
||||
POSTGRES_EFFECTIVE_CACHE_SIZE=1GB
|
||||
POSTGRES_MAX_CONNECTIONS=100
|
||||
# ─── Queue (Valkey 8 / Redis-compatible) ─────────────────────────────────────
|
||||
# Port 6380 avoids conflict with a host-side Redis/Valkey instance.
|
||||
VALKEY_URL=redis://localhost:6380
|
||||
|
||||
# ======================
|
||||
# Valkey Cache (Redis-compatible)
|
||||
# ======================
|
||||
# Bundled Valkey
|
||||
VALKEY_URL=redis://valkey:6379
|
||||
VALKEY_HOST=valkey
|
||||
VALKEY_PORT=6379
|
||||
# VALKEY_PASSWORD= # Optional: Password for Valkey authentication
|
||||
VALKEY_MAXMEMORY=256mb
|
||||
# Docker Compose host-port override for the Valkey container (default: 6380)
|
||||
# VALKEY_HOST_PORT=6380
|
||||
|
||||
# External Redis/Valkey (managed service)
|
||||
# To use an external instance, update VALKEY_URL above
|
||||
# Example: VALKEY_URL=redis://elasticache.amazonaws.com:6379
|
||||
# Example with auth: VALKEY_URL=redis://:[email protected]:6379
|
||||
|
||||
# Knowledge Module Cache Configuration
|
||||
# Set KNOWLEDGE_CACHE_ENABLED=false to disable caching (useful for development)
|
||||
KNOWLEDGE_CACHE_ENABLED=true
|
||||
# Cache TTL in seconds (default: 300 = 5 minutes)
|
||||
KNOWLEDGE_CACHE_TTL=300
|
||||
# ─── Gateway ─────────────────────────────────────────────────────────────────
|
||||
# TCP port the NestJS/Fastify gateway listens on (default: 4000)
|
||||
GATEWAY_PORT=4000
|
||||
|
||||
# ======================
|
||||
# Authentication (Authentik OIDC)
|
||||
# ======================
|
||||
# Set to 'true' to enable OIDC authentication with Authentik
|
||||
# When enabled, OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_REDIRECT_URI are required
|
||||
OIDC_ENABLED=false
|
||||
# Comma-separated list of allowed CORS origins.
|
||||
# Must include the web app origin in production.
|
||||
GATEWAY_CORS_ORIGIN=http://localhost:3000
|
||||
|
||||
# Authentik Server URLs (required when OIDC_ENABLED=true)
|
||||
# OIDC_ISSUER must end with a trailing slash (/)
|
||||
OIDC_ISSUER=https://auth.example.com/application/o/mosaic-stack/
|
||||
OIDC_CLIENT_ID=your-client-id-here
|
||||
OIDC_CLIENT_SECRET=your-client-secret-here
|
||||
# Redirect URI must match what's configured in Authentik
|
||||
# Development: http://localhost:3001/auth/oauth2/callback/authentik
|
||||
# Production: https://mosaic-api.woltje.com/auth/oauth2/callback/authentik
|
||||
OIDC_REDIRECT_URI=http://localhost:3001/auth/oauth2/callback/authentik
|
||||
|
||||
# Authentik PostgreSQL Database
|
||||
AUTHENTIK_POSTGRES_USER=authentik
|
||||
AUTHENTIK_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_POSTGRES_DB=authentik
|
||||
# ─── Auth (BetterAuth) ───────────────────────────────────────────────────────
|
||||
# REQUIRED — random secret used to sign sessions and tokens.
|
||||
# Generate with: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET=change-me-to-a-random-32-char-string
|
||||
|
||||
# Authentik Configuration
|
||||
# CRITICAL: Generate a random secret key with at least 50 characters
|
||||
# Example: openssl rand -base64 50
|
||||
AUTHENTIK_SECRET_KEY=REPLACE_WITH_RANDOM_SECRET_MINIMUM_50_CHARS
|
||||
AUTHENTIK_ERROR_REPORTING=false
|
||||
# SECURITY: Change bootstrap password immediately after first login
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL=admin@localhost
|
||||
AUTHENTIK_COOKIE_DOMAIN=.localhost
|
||||
# Public base URL of the gateway (used by BetterAuth for callback URLs)
|
||||
BETTER_AUTH_URL=http://localhost:4000
|
||||
|
||||
# Authentik Ports
|
||||
AUTHENTIK_PORT_HTTP=9000
|
||||
AUTHENTIK_PORT_HTTPS=9443
|
||||
|
||||
# ======================
|
||||
# CSRF Protection
|
||||
# ======================
|
||||
# CRITICAL: Generate a random secret for CSRF token signing
|
||||
# Required in production; auto-generated in development (not persistent across restarts)
|
||||
# Command to generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
CSRF_SECRET=REPLACE_WITH_64_CHAR_HEX_STRING
|
||||
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
|
||||
# Public gateway URL — accessible from the browser, not just the server.
|
||||
NEXT_PUBLIC_GATEWAY_URL=http://localhost:4000
|
||||
|
||||
# ======================
|
||||
# JWT Configuration
|
||||
# ======================
|
||||
# CRITICAL: Generate a random secret key with at least 32 characters
|
||||
# Example: openssl rand -base64 32
|
||||
JWT_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS
|
||||
JWT_EXPIRATION=24h
|
||||
|
||||
# ======================
|
||||
# BetterAuth Configuration
|
||||
# ======================
|
||||
# CRITICAL: Generate a random secret key with at least 32 characters
|
||||
# This is used by BetterAuth for session management and CSRF protection
|
||||
# Example: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS
|
||||
# Optional explicit BetterAuth origin for callback/error URL generation.
|
||||
# When empty, backend falls back to NEXT_PUBLIC_API_URL.
|
||||
BETTER_AUTH_URL=
|
||||
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
|
||||
# OTLP HTTP endpoint (otel-collector or any OpenTelemetry-compatible backend)
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||
|
||||
# Trusted Origins (comma-separated list of additional trusted origins for CORS and auth)
|
||||
# These are added to NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_API_URL automatically
|
||||
TRUSTED_ORIGINS=
|
||||
# Service name shown in traces
|
||||
OTEL_SERVICE_NAME=mosaic-gateway
|
||||
|
||||
# Cookie Domain (for cross-subdomain session sharing)
|
||||
# Leave empty for single-domain setups. Set to ".example.com" for cross-subdomain.
|
||||
COOKIE_DOMAIN=
|
||||
|
||||
# ======================
|
||||
# Encryption (Credential Security)
|
||||
# ======================
|
||||
# CRITICAL: Generate a random 32-byte (256-bit) encryption key
|
||||
# This key is used for AES-256-GCM encryption of OAuth tokens and sensitive data
|
||||
# Command to generate: openssl rand -hex 32
|
||||
# SECURITY: Never commit this key to version control
|
||||
# SECURITY: Use different keys for development, staging, and production
|
||||
# SECURITY: Store production keys in a secure secrets manager (see docs/design/credential-security.md)
|
||||
ENCRYPTION_KEY=REPLACE_WITH_64_CHAR_HEX_STRING_GENERATE_WITH_OPENSSL_RAND_HEX_32
|
||||
# ─── AI Providers ────────────────────────────────────────────────────────────
|
||||
|
||||
# ======================
|
||||
# OpenBao Secrets Management
|
||||
# ======================
|
||||
# OpenBao provides Transit encryption for sensitive credentials
|
||||
# Enable with: COMPOSE_PROFILES=openbao or COMPOSE_PROFILES=full
|
||||
# Auto-initialized on first run via openbao-init sidecar
|
||||
# Ollama (local models — set OLLAMA_BASE_URL to enable)
|
||||
# OLLAMA_BASE_URL=http://localhost:11434
|
||||
# OLLAMA_HOST is a legacy alias for OLLAMA_BASE_URL
|
||||
# OLLAMA_HOST=http://localhost:11434
|
||||
# Comma-separated list of Ollama model IDs to register (default: llama3.2,codellama,mistral)
|
||||
# OLLAMA_MODELS=llama3.2,codellama,mistral
|
||||
|
||||
# Bundled OpenBao (when openbao profile enabled)
|
||||
OPENBAO_ADDR=http://openbao:8200
|
||||
OPENBAO_PORT=8200
|
||||
|
||||
# External OpenBao/Vault (managed service)
|
||||
# Disable 'openbao' profile and set OPENBAO_ADDR to your external instance
|
||||
# Example: OPENBAO_ADDR=https://vault.example.com:8200
|
||||
# Example: OPENBAO_ADDR=https://vault.hashicorp.com:8200
|
||||
|
||||
# AppRole Authentication (Optional)
|
||||
# If not set, credentials are read from /openbao/init/approle-credentials volume
|
||||
# Required when using external OpenBao
|
||||
# OPENBAO_ROLE_ID=your-role-id-here
|
||||
# OPENBAO_SECRET_ID=your-secret-id-here
|
||||
|
||||
# Fallback Mode
|
||||
# When OpenBao is unavailable, API automatically falls back to AES-256-GCM
|
||||
# encryption using ENCRYPTION_KEY. This provides graceful degradation.
|
||||
|
||||
# ======================
|
||||
# Ollama (Optional AI Service)
|
||||
# ======================
|
||||
# Set OLLAMA_ENDPOINT to use local or remote Ollama
|
||||
# For bundled Docker service: http://ollama:11434
|
||||
# For external service: http://your-ollama-server:11434
|
||||
OLLAMA_ENDPOINT=http://ollama:11434
|
||||
OLLAMA_PORT=11434
|
||||
|
||||
# Embedding Model Configuration
|
||||
# Model used for generating knowledge entry embeddings
|
||||
# Default: mxbai-embed-large (1024-dim, padded to 1536)
|
||||
# Alternative: nomic-embed-text (768-dim, padded to 1536)
|
||||
# Note: Embeddings are padded/truncated to 1536 dimensions to match schema
|
||||
OLLAMA_EMBEDDING_MODEL=mxbai-embed-large
|
||||
|
||||
# Semantic Search Configuration
|
||||
# Similarity threshold for semantic search (0.0 to 1.0, where 1.0 is identical)
|
||||
# Lower values return more results but may be less relevant
|
||||
# Default: 0.5 (50% similarity)
|
||||
SEMANTIC_SEARCH_SIMILARITY_THRESHOLD=0.5
|
||||
|
||||
# ======================
|
||||
# OpenAI API (For Semantic Search)
|
||||
# ======================
|
||||
# OPTIONAL: Semantic search requires an OpenAI API key
|
||||
# Get your API key from: https://platform.openai.com/api-keys
|
||||
# If not configured, semantic search endpoints will return an error
|
||||
# OpenAI — required for embedding and log-summarization features
|
||||
# OPENAI_API_KEY=sk-...
|
||||
|
||||
# ======================
|
||||
# Application Environment
|
||||
# ======================
|
||||
NODE_ENV=development
|
||||
# Custom providers — JSON array of provider configs
|
||||
# Format: [{"id":"<id>","baseUrl":"<url>","apiKey":"<key>","models":[{"id":"<model-id>","name":"<label>"}]}]
|
||||
# MOSAIC_CUSTOM_PROVIDERS=
|
||||
|
||||
# ======================
|
||||
# Docker Image Configuration
|
||||
# ======================
|
||||
# Docker image tag for pulling pre-built images from git.mosaicstack.dev registry
|
||||
# Used by docker-compose.yml (pulls images) and docker-swarm.yml
|
||||
# For local builds, use docker-compose.build.yml instead
|
||||
# Options:
|
||||
# - latest: Pull latest images from registry (default, built from main branch)
|
||||
# - <version>: Use specific version tag (e.g., v1.0.0)
|
||||
IMAGE_TAG=latest
|
||||
|
||||
# ======================
|
||||
# Docker Compose Profiles
|
||||
# ======================
|
||||
# Enable optional services via profiles. Combine multiple profiles with commas.
|
||||
#
|
||||
# Available profiles:
|
||||
# - database: PostgreSQL database (disable to use external database)
|
||||
# - cache: Valkey cache (disable to use external Redis)
|
||||
# - openbao: OpenBao secrets management (disable to use external vault or fallback encryption)
|
||||
# - authentik: Authentik OIDC authentication (disable to use external auth provider)
|
||||
# - ollama: Ollama AI/LLM service (disable to use external LLM service)
|
||||
# - traefik-bundled: Bundled Traefik reverse proxy (disable to use external proxy)
|
||||
# - full: Enable all optional services (turnkey deployment)
|
||||
#
|
||||
# Examples:
|
||||
# COMPOSE_PROFILES=full # Everything bundled (development)
|
||||
# COMPOSE_PROFILES=database,cache,openbao # Core services only
|
||||
# COMPOSE_PROFILES= # All external services (production)
|
||||
COMPOSE_PROFILES=full
|
||||
# ─── Embedding Service ───────────────────────────────────────────────────────
|
||||
# OpenAI-compatible embeddings endpoint (default: OpenAI)
|
||||
# EMBEDDING_API_URL=https://api.openai.com/v1
|
||||
# EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
# ======================
|
||||
# Traefik Reverse Proxy
|
||||
# ======================
|
||||
# TRAEFIK_MODE options:
|
||||
# - bundled: Use bundled Traefik (requires traefik-bundled profile)
|
||||
# - upstream: Connect to external Traefik instance
|
||||
# - none: Direct port exposure without reverse proxy (default)
|
||||
TRAEFIK_MODE=none
|
||||
|
||||
# Domain configuration for Traefik routing
|
||||
MOSAIC_API_DOMAIN=api.mosaic.local
|
||||
MOSAIC_WEB_DOMAIN=mosaic.local
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.local
|
||||
# ─── Log Summarization Service ───────────────────────────────────────────────
|
||||
# OpenAI-compatible chat completions endpoint for log summarization (default: OpenAI)
|
||||
# SUMMARIZATION_API_URL=https://api.openai.com/v1
|
||||
# SUMMARIZATION_MODEL=gpt-4o-mini
|
||||
|
||||
# External Traefik network name (for upstream mode and swarm)
|
||||
# Must match the network name of your existing Traefik instance
|
||||
TRAEFIK_NETWORK=traefik-public
|
||||
TRAEFIK_DOCKER_NETWORK=traefik-public
|
||||
# Cron schedule for summarization job (default: every 6 hours)
|
||||
# SUMMARIZATION_CRON=0 */6 * * *
|
||||
|
||||
# TLS/SSL Configuration
|
||||
TRAEFIK_TLS_ENABLED=true
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
# Cert resolver name (leave empty if TLS is handled externally or using self-signed certs)
|
||||
TRAEFIK_CERTRESOLVER=
|
||||
# For Let's Encrypt (production):
|
||||
TRAEFIK_ACME_EMAIL=[email protected]
|
||||
# For self-signed certificates (development), leave TRAEFIK_ACME_EMAIL empty
|
||||
# Cron schedule for log tier management (default: daily at 03:00)
|
||||
# TIER_MANAGEMENT_CRON=0 3 * * *
|
||||
|
||||
# Traefik Dashboard (bundled mode only)
|
||||
TRAEFIK_DASHBOARD_ENABLED=true
|
||||
TRAEFIK_DASHBOARD_PORT=8080
|
||||
|
||||
# ======================
|
||||
# Gitea Integration (Coordinator)
|
||||
# ======================
|
||||
# Gitea instance URL
|
||||
GITEA_URL=https://git.mosaicstack.dev
|
||||
# ─── Agent ───────────────────────────────────────────────────────────────────
|
||||
# Filesystem sandbox root for agent file tools (default: process.cwd())
|
||||
# AGENT_FILE_SANDBOX_DIR=/var/lib/mosaic/sandbox
|
||||
|
||||
# Coordinator bot credentials (see docs/1-getting-started/3-configuration/4-gitea-coordinator.md)
|
||||
# SECURITY: Store GITEA_BOT_TOKEN in secrets vault, not in version control
|
||||
GITEA_BOT_USERNAME=mosaic
|
||||
GITEA_BOT_TOKEN=REPLACE_WITH_COORDINATOR_BOT_API_TOKEN
|
||||
GITEA_BOT_PASSWORD=REPLACE_WITH_COORDINATOR_BOT_PASSWORD
|
||||
# Comma-separated list of tool names available to non-admin users.
|
||||
# Leave unset to allow all tools for all authenticated users.
|
||||
# AGENT_USER_TOOLS=read_file,list_directory,search_files
|
||||
|
||||
# Repository configuration
|
||||
GITEA_REPO_OWNER=mosaic
|
||||
GITEA_REPO_NAME=stack
|
||||
# System prompt injected into every agent session (optional)
|
||||
# AGENT_SYSTEM_PROMPT=You are a helpful assistant.
|
||||
|
||||
# Webhook secret for coordinator (HMAC SHA256 signature verification)
|
||||
# SECURITY: Generate random secret with: openssl rand -hex 32
|
||||
# Configure in Gitea: Repository Settings → Webhooks → Add Webhook
|
||||
GITEA_WEBHOOK_SECRET=REPLACE_WITH_RANDOM_WEBHOOK_SECRET
|
||||
|
||||
# Gatekeeper merge automation
|
||||
# Uses the same Gitea host to validate PR webhooks and to merge approved PRs after green CI.
|
||||
GITEA_API_TOKEN=REPLACE_WITH_GATEKEEPER_GITEA_API_TOKEN
|
||||
GATEKEEPER_ENABLED=true
|
||||
# ─── MCP Servers ─────────────────────────────────────────────────────────────
|
||||
# JSON array of MCP server configs — set to enable MCP tool integration.
|
||||
# Each entry: {"name":"<id>","url":"<http-or-sse-url>"}
|
||||
# MCP_SERVERS=[{"name":"my-mcp","url":"http://localhost:3100/sse"}]
|
||||
|
||||
# Coordinator API Key (service-to-service authentication)
|
||||
# CRITICAL: Generate a random API key with at least 32 characters
|
||||
# Example: openssl rand -base64 32
|
||||
# The coordinator service uses this key to authenticate with the API
|
||||
COORDINATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS
|
||||
|
||||
# Anthropic API Key (used by coordinator for issue parsing)
|
||||
# Get your API key from: https://console.anthropic.com/
|
||||
ANTHROPIC_API_KEY=REPLACE_WITH_ANTHROPIC_API_KEY
|
||||
# ─── Coordinator ─────────────────────────────────────────────────────────────
|
||||
# Root directory used to scope coordinator (worktree/repo) operations.
|
||||
# Defaults to the monorepo root auto-detected from process.cwd().
|
||||
# MOSAIC_WORKSPACE_ROOT=/home/user/projects/mosaic
|
||||
|
||||
# Coordinator tuning
|
||||
COORDINATOR_POLL_INTERVAL=5.0
|
||||
COORDINATOR_MAX_CONCURRENT_AGENTS=10
|
||||
COORDINATOR_ENABLED=true
|
||||
|
||||
# ======================
|
||||
# Rate Limiting
|
||||
# ======================
|
||||
# Rate limiting prevents DoS attacks on webhook and API endpoints
|
||||
# TTL is in seconds, limits are per TTL window
|
||||
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
|
||||
# DISCORD_BOT_TOKEN=
|
||||
# DISCORD_GUILD_ID=
|
||||
# DISCORD_GATEWAY_URL=http://localhost:4000
|
||||
|
||||
# Global rate limit (applies to all endpoints unless overridden)
|
||||
# Time window in seconds
|
||||
RATE_LIMIT_TTL=60
|
||||
# Requests per window
|
||||
RATE_LIMIT_GLOBAL_LIMIT=100
|
||||
|
||||
# Webhook endpoints (/stitcher/webhook, /stitcher/dispatch) — requests per minute
|
||||
RATE_LIMIT_WEBHOOK_LIMIT=60
|
||||
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
|
||||
# TELEGRAM_BOT_TOKEN=
|
||||
# TELEGRAM_GATEWAY_URL=http://localhost:4000
|
||||
|
||||
# Coordinator endpoints (/coordinator/*) — requests per minute
|
||||
RATE_LIMIT_COORDINATOR_LIMIT=100
|
||||
|
||||
# Health check endpoints (/coordinator/health) — requests per minute (higher for monitoring)
|
||||
RATE_LIMIT_HEALTH_LIMIT=300
|
||||
|
||||
# Storage backend for rate limiting (redis or memory)
|
||||
# redis: Uses Valkey for distributed rate limiting (recommended for production)
|
||||
# memory: Uses in-memory storage (single instance only, for development)
|
||||
RATE_LIMIT_STORAGE=redis
|
||||
|
||||
# ======================
|
||||
# Discord Bridge (Optional)
|
||||
# ======================
|
||||
# Discord bot integration for chat-based control
|
||||
# Get bot token from: https://discord.com/developers/applications
|
||||
# DISCORD_BOT_TOKEN=your-discord-bot-token-here
|
||||
# DISCORD_GUILD_ID=your-discord-server-id
|
||||
# DISCORD_CONTROL_CHANNEL_ID=channel-id-for-commands
|
||||
# DISCORD_WORKSPACE_ID=your-workspace-uuid
|
||||
#
|
||||
# Agent channel routing: Maps Discord channels to specific agents.
|
||||
# Format: <channelId>:<agentName>,<channelId>:<agentName>
|
||||
# Example: 123456789:jarvis,987654321:builder
|
||||
# DISCORD_AGENT_CHANNELS=
|
||||
#
|
||||
# SECURITY: DISCORD_WORKSPACE_ID must be a valid workspace UUID from your database.
|
||||
# All Discord commands will execute within this workspace context for proper
|
||||
# multi-tenant isolation. Each Discord bot instance should be configured for
|
||||
# a single workspace.
|
||||
|
||||
# ======================
|
||||
# Matrix Bridge (Optional)
|
||||
# ======================
|
||||
# Matrix bot integration for chat-based control via Matrix protocol
|
||||
# Requires a Matrix account with an access token for the bot user
|
||||
# Set these AFTER deploying Synapse and creating the bot account.
|
||||
#
|
||||
# SECURITY: MATRIX_WORKSPACE_ID must be a valid workspace UUID from your database.
|
||||
# All Matrix commands will execute within this workspace context for proper
|
||||
# multi-tenant isolation. Each Matrix bot instance should be configured for
|
||||
# a single workspace.
|
||||
MATRIX_HOMESERVER_URL=http://synapse:8008
|
||||
MATRIX_ACCESS_TOKEN=
|
||||
MATRIX_BOT_USER_ID=@mosaic-bot:matrix.woltje.com
|
||||
MATRIX_SERVER_NAME=matrix.woltje.com
|
||||
# MATRIX_CONTROL_ROOM_ID=!roomid:matrix.woltje.com
|
||||
# MATRIX_WORKSPACE_ID=your-workspace-uuid
|
||||
|
||||
# ======================
|
||||
# Matrix / Synapse Deployment
|
||||
# ======================
|
||||
# Domains for Traefik routing to Matrix services
|
||||
MATRIX_DOMAIN=matrix.woltje.com
|
||||
ELEMENT_DOMAIN=chat.woltje.com
|
||||
|
||||
# Synapse database (created automatically by synapse-db-init in the swarm compose)
|
||||
SYNAPSE_POSTGRES_DB=synapse
|
||||
SYNAPSE_POSTGRES_USER=synapse
|
||||
SYNAPSE_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_SYNAPSE_DB_PASSWORD
|
||||
|
||||
# Image tags for Matrix services
|
||||
SYNAPSE_IMAGE_TAG=latest
|
||||
ELEMENT_IMAGE_TAG=latest
|
||||
|
||||
# ======================
|
||||
# Orchestrator Configuration
|
||||
# ======================
|
||||
# API Key for orchestrator agent management endpoints
|
||||
# CRITICAL: Generate a random API key with at least 32 characters
|
||||
# Example: openssl rand -base64 32
|
||||
# Required for all /agents/* endpoints (spawn, kill, kill-all, status)
|
||||
# Health endpoints (/health/*) remain unauthenticated
|
||||
ORCHESTRATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS
|
||||
|
||||
# Runtime safety defaults (recommended for low-memory hosts)
|
||||
MAX_CONCURRENT_AGENTS=2
|
||||
SESSION_CLEANUP_DELAY_MS=30000
|
||||
ORCHESTRATOR_QUEUE_NAME=orchestrator-tasks
|
||||
ORCHESTRATOR_QUEUE_CONCURRENCY=1
|
||||
ORCHESTRATOR_QUEUE_MAX_RETRIES=3
|
||||
ORCHESTRATOR_QUEUE_BASE_DELAY_MS=1000
|
||||
ORCHESTRATOR_QUEUE_MAX_DELAY_MS=60000
|
||||
SANDBOX_DEFAULT_MEMORY_MB=256
|
||||
SANDBOX_DEFAULT_CPU_LIMIT=1.0
|
||||
|
||||
# ======================
|
||||
# AI Provider Configuration
|
||||
# ======================
|
||||
# Choose the AI provider for orchestrator agents
|
||||
# Options: ollama, claude, openai
|
||||
# Default: ollama (no API key required)
|
||||
AI_PROVIDER=ollama
|
||||
|
||||
# Ollama Configuration (when AI_PROVIDER=ollama)
|
||||
# For local Ollama: http://localhost:11434
|
||||
# For remote Ollama: http://your-ollama-server:11434
|
||||
OLLAMA_MODEL=llama3.1:latest
|
||||
|
||||
# Claude API Key
|
||||
# Required only when AI_PROVIDER=claude.
|
||||
# Get your API key from: https://console.anthropic.com/
|
||||
CLAUDE_API_KEY=REPLACE_WITH_CLAUDE_API_KEY
|
||||
|
||||
# OpenAI API Configuration (when AI_PROVIDER=openai)
|
||||
# OPTIONAL: Only required if AI_PROVIDER=openai
|
||||
# Get your API key from: https://platform.openai.com/api-keys
|
||||
# OPENAI_API_KEY=sk-...
|
||||
|
||||
# ======================
|
||||
# Speech Services (STT / TTS)
|
||||
# ======================
|
||||
# Speech-to-Text (STT) - Whisper via Speaches
|
||||
# Set STT_ENABLED=true to enable speech-to-text transcription
|
||||
# STT_BASE_URL is required when STT_ENABLED=true
|
||||
STT_ENABLED=true
|
||||
STT_BASE_URL=http://speaches:8000/v1
|
||||
STT_MODEL=Systran/faster-whisper-large-v3-turbo
|
||||
STT_LANGUAGE=en
|
||||
|
||||
# Text-to-Speech (TTS) - Default Engine (Kokoro)
|
||||
# Set TTS_ENABLED=true to enable text-to-speech synthesis
|
||||
# TTS_DEFAULT_URL is required when TTS_ENABLED=true
|
||||
TTS_ENABLED=true
|
||||
TTS_DEFAULT_URL=http://kokoro-tts:8880/v1
|
||||
TTS_DEFAULT_VOICE=af_heart
|
||||
TTS_DEFAULT_FORMAT=mp3
|
||||
|
||||
# Text-to-Speech (TTS) - Premium Engine (Chatterbox) - Optional
|
||||
# Higher quality voice cloning engine, disabled by default
|
||||
# TTS_PREMIUM_URL is required when TTS_PREMIUM_ENABLED=true
|
||||
TTS_PREMIUM_ENABLED=false
|
||||
TTS_PREMIUM_URL=http://chatterbox-tts:8881/v1
|
||||
|
||||
# Text-to-Speech (TTS) - Fallback Engine (Piper/OpenedAI) - Optional
|
||||
# Lightweight fallback engine, disabled by default
|
||||
# TTS_FALLBACK_URL is required when TTS_FALLBACK_ENABLED=true
|
||||
TTS_FALLBACK_ENABLED=false
|
||||
TTS_FALLBACK_URL=http://openedai-speech:8000/v1
|
||||
|
||||
# Whisper model for Speaches STT engine
|
||||
SPEACHES_WHISPER_MODEL=Systran/faster-whisper-large-v3-turbo
|
||||
|
||||
# Speech Service Limits
|
||||
# Maximum upload file size in bytes (default: 25MB)
|
||||
SPEECH_MAX_UPLOAD_SIZE=25000000
|
||||
# Maximum audio duration in seconds (default: 600 = 10 minutes)
|
||||
SPEECH_MAX_DURATION_SECONDS=600
|
||||
# Maximum text length for TTS in characters (default: 4096)
|
||||
SPEECH_MAX_TEXT_LENGTH=4096
|
||||
|
||||
# ======================
|
||||
# Mosaic Telemetry (Task Completion Tracking & Predictions)
|
||||
# ======================
|
||||
# Telemetry tracks task completion patterns to provide time estimates and predictions.
|
||||
# Data is sent to the Mosaic Telemetry API (a separate service).
|
||||
|
||||
# Master switch: set to false to completely disable telemetry (no HTTP calls will be made)
|
||||
MOSAIC_TELEMETRY_ENABLED=true
|
||||
|
||||
# URL of the telemetry API server
|
||||
# For Docker Compose (internal): http://telemetry-api:8000
|
||||
# For production/swarm: https://tel-api.mosaicstack.dev
|
||||
MOSAIC_TELEMETRY_SERVER_URL=http://telemetry-api:8000
|
||||
|
||||
# API key for authenticating with the telemetry server
|
||||
# Generate with: openssl rand -hex 32
|
||||
MOSAIC_TELEMETRY_API_KEY=your-64-char-hex-api-key-here
|
||||
|
||||
# Unique identifier for this Mosaic Stack instance
|
||||
# Generate with: uuidgen or python -c "import uuid; print(uuid.uuid4())"
|
||||
MOSAIC_TELEMETRY_INSTANCE_ID=your-instance-uuid-here
|
||||
|
||||
# Dry run mode: set to true to log telemetry events to console instead of sending HTTP requests
|
||||
# Useful for development and debugging telemetry payloads
|
||||
MOSAIC_TELEMETRY_DRY_RUN=false
|
||||
|
||||
# ======================
|
||||
# Logging & Debugging
|
||||
# ======================
|
||||
LOG_LEVEL=info
|
||||
DEBUG=false
|
||||
# ─── Authentik SSO (optional — set AUTHENTIK_CLIENT_ID to enable) ────────────
|
||||
# AUTHENTIK_ISSUER=https://auth.example.com/application/o/mosaic/
|
||||
# AUTHENTIK_CLIENT_ID=
|
||||
# AUTHENTIK_CLIENT_SECRET=
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# Traefik Bundled Mode Configuration
|
||||
# Copy this to .env to enable bundled Traefik reverse proxy
|
||||
#
|
||||
# Usage:
|
||||
# cp .env.traefik-bundled.example .env
|
||||
# docker compose --profile traefik-bundled up -d
|
||||
|
||||
# ======================
|
||||
# Traefik Configuration
|
||||
# ======================
|
||||
TRAEFIK_MODE=bundled
|
||||
TRAEFIK_ENABLE=true
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
TRAEFIK_DOCKER_NETWORK=mosaic-public
|
||||
|
||||
# Domain configuration
|
||||
MOSAIC_API_DOMAIN=api.mosaic.local
|
||||
MOSAIC_WEB_DOMAIN=mosaic.local
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.local
|
||||
|
||||
# TLS/SSL Configuration
|
||||
TRAEFIK_TLS_ENABLED=true
|
||||
# For Let's Encrypt (production):
|
||||
# [email protected]
|
||||
# TRAEFIK_CERTRESOLVER=letsencrypt
|
||||
# For self-signed certificates (development), leave TRAEFIK_ACME_EMAIL empty
|
||||
TRAEFIK_ACME_EMAIL=
|
||||
|
||||
# Traefik Dashboard
|
||||
TRAEFIK_DASHBOARD_ENABLED=true
|
||||
TRAEFIK_DASHBOARD_PORT=8080
|
||||
|
||||
# Traefik Ports
|
||||
TRAEFIK_HTTP_PORT=80
|
||||
TRAEFIK_HTTPS_PORT=443
|
||||
|
||||
# ======================
|
||||
# Application Ports (not exposed when using Traefik)
|
||||
# ======================
|
||||
API_PORT=3001
|
||||
WEB_PORT=3000
|
||||
|
||||
# ======================
|
||||
# PostgreSQL Database
|
||||
# ======================
|
||||
POSTGRES_USER=mosaic
|
||||
POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
POSTGRES_DB=mosaic
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# ======================
|
||||
# Valkey Cache
|
||||
# ======================
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_MAXMEMORY=256mb
|
||||
|
||||
# ======================
|
||||
# Authentication (Authentik OIDC)
|
||||
# ======================
|
||||
OIDC_ISSUER=https://auth.mosaic.local/application/o/mosaic-stack/
|
||||
OIDC_CLIENT_ID=your-client-id-here
|
||||
OIDC_CLIENT_SECRET=your-client-secret-here
|
||||
OIDC_REDIRECT_URI=https://api.mosaic.local/auth/callback
|
||||
|
||||
# Authentik Configuration
|
||||
AUTHENTIK_SECRET_KEY=REPLACE_WITH_RANDOM_SECRET_MINIMUM_50_CHARS
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL=admin@localhost
|
||||
AUTHENTIK_COOKIE_DOMAIN=.mosaic.local
|
||||
|
||||
AUTHENTIK_POSTGRES_USER=authentik
|
||||
AUTHENTIK_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_POSTGRES_DB=authentik
|
||||
|
||||
# ======================
|
||||
# JWT Configuration
|
||||
# ======================
|
||||
JWT_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS
|
||||
JWT_EXPIRATION=24h
|
||||
|
||||
# ======================
|
||||
# Docker Compose Profiles
|
||||
# ======================
|
||||
# Enable bundled Traefik and optional services
|
||||
COMPOSE_PROFILES=traefik-bundled,authentik
|
||||
@@ -1,83 +0,0 @@
|
||||
# Traefik Upstream Mode Configuration
|
||||
# Connect to an existing external Traefik instance
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. External Traefik instance must be running
|
||||
# 2. External network must exist: docker network create traefik-public
|
||||
# 3. Copy docker-compose.override.yml.example to docker-compose.override.yml
|
||||
# 4. Uncomment upstream mode network configuration in override file
|
||||
#
|
||||
# Usage:
|
||||
# cp .env.traefik-upstream.example .env
|
||||
# docker compose up -d
|
||||
|
||||
# ======================
|
||||
# Traefik Configuration
|
||||
# ======================
|
||||
TRAEFIK_MODE=upstream
|
||||
TRAEFIK_ENABLE=true
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
TRAEFIK_DOCKER_NETWORK=traefik-public
|
||||
TRAEFIK_NETWORK=traefik-public
|
||||
|
||||
# Domain configuration
|
||||
# These domains must be configured in your DNS or /etc/hosts
|
||||
MOSAIC_API_DOMAIN=api.mosaic.uscllc.com
|
||||
MOSAIC_WEB_DOMAIN=mosaic.uscllc.com
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.uscllc.com
|
||||
|
||||
# TLS/SSL Configuration
|
||||
TRAEFIK_TLS_ENABLED=true
|
||||
# ACME/Certresolver managed by upstream Traefik
|
||||
TRAEFIK_CERTRESOLVER=
|
||||
|
||||
# ======================
|
||||
# Application Ports (not exposed when using Traefik)
|
||||
# ======================
|
||||
# These ports are only used internally within Docker network
|
||||
API_PORT=3001
|
||||
WEB_PORT=3000
|
||||
|
||||
# ======================
|
||||
# PostgreSQL Database
|
||||
# ======================
|
||||
POSTGRES_USER=mosaic
|
||||
POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
POSTGRES_DB=mosaic
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# ======================
|
||||
# Valkey Cache
|
||||
# ======================
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_MAXMEMORY=256mb
|
||||
|
||||
# ======================
|
||||
# Authentication (Authentik OIDC)
|
||||
# ======================
|
||||
OIDC_ISSUER=https://auth.mosaic.uscllc.com/application/o/mosaic-stack/
|
||||
OIDC_CLIENT_ID=your-client-id-here
|
||||
OIDC_CLIENT_SECRET=your-client-secret-here
|
||||
OIDC_REDIRECT_URI=https://api.mosaic.uscllc.com/auth/callback
|
||||
|
||||
# Authentik Configuration
|
||||
AUTHENTIK_SECRET_KEY=REPLACE_WITH_RANDOM_SECRET_MINIMUM_50_CHARS
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL=admin@localhost
|
||||
AUTHENTIK_COOKIE_DOMAIN=.mosaic.uscllc.com
|
||||
|
||||
AUTHENTIK_POSTGRES_USER=authentik
|
||||
AUTHENTIK_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_POSTGRES_DB=authentik
|
||||
|
||||
# ======================
|
||||
# JWT Configuration
|
||||
# ======================
|
||||
JWT_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS
|
||||
JWT_EXPIRATION=24h
|
||||
|
||||
# ======================
|
||||
# Docker Compose Profiles
|
||||
# ======================
|
||||
# Enable optional services (do NOT enable traefik-bundled in upstream mode)
|
||||
COMPOSE_PROFILES=authentik
|
||||
+5
-65
@@ -1,71 +1,11 @@
|
||||
# Dependencies
|
||||
logs/
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
|
||||
# Compiled source (prevent accidental commits)
|
||||
apps/*/src/**/*.js
|
||||
apps/*/src/**/*.d.ts
|
||||
apps/*/src/**/*.js.map
|
||||
apps/*/src/**/*.d.ts.map
|
||||
packages/*/src/**/*.js
|
||||
packages/*/src/**/*.d.ts
|
||||
packages/*/src/**/*.js.map
|
||||
packages/*/src/**/*.d.ts.map
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.next
|
||||
coverage
|
||||
.env
|
||||
.env.local
|
||||
.env.test
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.bak.*
|
||||
*.bak
|
||||
|
||||
# Credentials (never commit)
|
||||
.admin-credentials
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Misc
|
||||
*.tsbuildinfo
|
||||
.pnpm-approve-builds
|
||||
|
||||
# Husky
|
||||
.husky/_
|
||||
|
||||
# Orchestrator reports (generated by QA automation, cleaned up after processing)
|
||||
docs/reports/qa-automation/
|
||||
|
||||
# Repo-local orchestrator runtime artifacts
|
||||
.mosaic/orchestrator/orchestrator.pid
|
||||
.mosaic/orchestrator/state.json
|
||||
.mosaic/orchestrator/tasks.json
|
||||
.mosaic/orchestrator/matrix_state.json
|
||||
.mosaic/orchestrator/logs/*.log
|
||||
.mosaic/orchestrator/results/*
|
||||
!.mosaic/orchestrator/logs/.gitkeep
|
||||
!.mosaic/orchestrator/results/.gitkeep
|
||||
.pnpm-store
|
||||
docs/reports/
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm typecheck && pnpm lint && pnpm format:check
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"**/*.{ts,tsx,js,jsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"**/*.{json,md,yaml,yml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Monorepo-aware lint-staged configuration
|
||||
// STRICT ENFORCEMENT ENABLED: Blocks commits if affected packages have violations
|
||||
//
|
||||
// IMPORTANT: This lints ENTIRE packages, not just changed files.
|
||||
// If you touch ANY file in a package with violations, you must fix the whole package.
|
||||
// This forces incremental cleanup - work in a package = clean up that package.
|
||||
//
|
||||
export default {
|
||||
// TypeScript files - lint and typecheck affected packages
|
||||
'**/*.{ts,tsx}': (filenames) => {
|
||||
const commands = [];
|
||||
|
||||
// 1. Format first (auto-fixes what it can)
|
||||
commands.push(`prettier --write ${filenames.join(' ')}`);
|
||||
|
||||
// 2. Extract affected packages from absolute paths
|
||||
// lint-staged passes absolute paths, so we need to extract the relative part
|
||||
const packages = [...new Set(filenames.map(f => {
|
||||
// Match either absolute or relative paths: .../packages/shared/... or packages/shared/...
|
||||
const match = f.match(/(?:^|\/)(apps|packages)\/([^/]+)\//);
|
||||
if (!match) return null;
|
||||
// Return package name format for turbo (e.g., "@mosaic/api")
|
||||
return `@mosaic/${match[2]}`;
|
||||
}))].filter(Boolean);
|
||||
|
||||
if (packages.length === 0) {
|
||||
return commands;
|
||||
}
|
||||
|
||||
// 3. Lint entire affected packages via turbo
|
||||
// --max-warnings=0 means ANY warning/error blocks the commit
|
||||
packages.forEach(pkg => {
|
||||
commands.push(`pnpm turbo run lint --filter=${pkg} -- --max-warnings=0`);
|
||||
});
|
||||
|
||||
// 4. Type-check affected packages
|
||||
packages.forEach(pkg => {
|
||||
commands.push(`pnpm turbo run typecheck --filter=${pkg}`);
|
||||
});
|
||||
|
||||
return commands;
|
||||
},
|
||||
|
||||
// Format all other files
|
||||
'**/*.{js,jsx,json,md,yml,yaml}': [
|
||||
'prettier --write',
|
||||
],
|
||||
};
|
||||
@@ -13,3 +13,66 @@ This repository is attached to the machine-wide Mosaic framework.
|
||||
- Keep universal standards in `~/.config/mosaic`
|
||||
- Keep repo-specific behavior in this repo
|
||||
- Avoid copying large runtime configs into each project
|
||||
|
||||
## Optional Quality Rails
|
||||
|
||||
Use `.mosaic/quality-rails.yml` to track whether quality rails are enabled for this repo.
|
||||
|
||||
Apply a template:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target .
|
||||
```
|
||||
|
||||
Verify enforcement:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-quality-verify --target .
|
||||
```
|
||||
|
||||
## Optional Matrix Orchestrator Rail
|
||||
|
||||
Repo-local orchestrator state lives in `.mosaic/orchestrator/`.
|
||||
|
||||
Run one cycle:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-matrix-cycle
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-run --once
|
||||
```
|
||||
|
||||
Run continuously:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-run --poll-sec 10
|
||||
```
|
||||
|
||||
Bridge events to Matrix:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-matrix-publish
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-matrix-consume
|
||||
```
|
||||
|
||||
Run until queue is drained (syncs from `docs/tasks.md` first):
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-drain
|
||||
```
|
||||
|
||||
Set worker command if auto-detect does not match your CLI:
|
||||
|
||||
```bash
|
||||
export MOSAIC_WORKER_EXEC="codex -p"
|
||||
# or
|
||||
export MOSAIC_WORKER_EXEC="opencode -p"
|
||||
```
|
||||
|
||||
Use repo helper (foreground or detached):
|
||||
|
||||
```bash
|
||||
bash scripts/agent/orchestrator-daemon.sh drain
|
||||
bash scripts/agent/orchestrator-daemon.sh start
|
||||
bash scripts/agent/orchestrator-daemon.sh status
|
||||
bash scripts/agent/orchestrator-daemon.sh stop
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"transport": "matrix",
|
||||
"matrix": {
|
||||
"control_room_id": "",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"last_published_line": 0,
|
||||
"since": null
|
||||
}
|
||||
@@ -1,69 +1,14 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"mission_id": "ms22-p2-named-agent-fleet-20260304",
|
||||
"name": "MS22-P2 Named Agent Fleet",
|
||||
"mission_id": "mvp-20260312",
|
||||
"name": "MVP",
|
||||
"description": "",
|
||||
"project_path": "/home/jwoltje/src/mosaic-stack",
|
||||
"created_at": "2026-03-05T01:53:28Z",
|
||||
"project_path": "/home/jwoltje/src/mosaic-mono-v1",
|
||||
"created_at": "2026-03-13T00:44:02Z",
|
||||
"status": "active",
|
||||
"task_prefix": "",
|
||||
"quality_gates": "",
|
||||
"milestone_version": "0.0.1",
|
||||
"milestones": [
|
||||
{
|
||||
"id": "phase-1",
|
||||
"name": "Schema+Seed",
|
||||
"status": "pending",
|
||||
"branch": "schema-seed",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-2",
|
||||
"name": "Admin CRUD",
|
||||
"status": "pending",
|
||||
"branch": "admin-crud",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-3",
|
||||
"name": "User CRUD",
|
||||
"status": "pending",
|
||||
"branch": "user-crud",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-4",
|
||||
"name": "Agent Routing",
|
||||
"status": "pending",
|
||||
"branch": "agent-routing",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-5",
|
||||
"name": "Discord+UI",
|
||||
"status": "pending",
|
||||
"branch": "discord-ui",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-6",
|
||||
"name": "Verification",
|
||||
"status": "pending",
|
||||
"branch": "verification",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
}
|
||||
],
|
||||
"milestones": [],
|
||||
"sessions": []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"generated_at": "2026-03-13T00:44:51Z",
|
||||
"runtime": "claude",
|
||||
"mission_id": "mvp-20260312",
|
||||
"mission_name": "MVP",
|
||||
"project_path": "/home/jwoltje/src/mosaic-mono-v1",
|
||||
"quality_gates": "",
|
||||
"current_milestone": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"next_task": "",
|
||||
"progress": {
|
||||
"tasks_done": 0,
|
||||
"tasks_total": 0,
|
||||
"pct": 0
|
||||
},
|
||||
"current_branch": ""
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"session_id": "claude-20260312-194506-357136",
|
||||
"runtime": "claude",
|
||||
"pid": 357136,
|
||||
"started_at": "2026-03-13T00:45:06Z",
|
||||
"project_path": "/home/jwoltje/src/mosaic-mono-v1",
|
||||
"milestone_id": ""
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"running_task_id": null,
|
||||
"updated_at": null
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"tasks": []
|
||||
}
|
||||
@@ -7,4 +7,4 @@ template: ""
|
||||
# - monorepo
|
||||
#
|
||||
# Apply manually:
|
||||
# ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target <repo>
|
||||
# ~/.mosaic/bin/mosaic-quality-apply --template <template> --target <repo>
|
||||
|
||||
+13
-25
@@ -1,29 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Repo-specific hooks used by scripts/agent/*.sh for Mosaic Stack.
|
||||
# Optional repo-specific hooks used by scripts/agent/*.sh
|
||||
|
||||
mosaic_hook_session_start() {
|
||||
echo "[mosaic-stack] Branch: $(git rev-parse --abbrev-ref HEAD)"
|
||||
echo "[mosaic-stack] Remotes:"
|
||||
git remote -v | sed 's/^/[mosaic-stack] /'
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
echo "[mosaic-stack] Node: $(node -v)"
|
||||
fi
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
echo "[mosaic-stack] pnpm: $(pnpm -v)"
|
||||
fi
|
||||
}
|
||||
# Called by session-start.sh
|
||||
# mosaic_hook_session_start() {
|
||||
# echo "Run repo-specific startup checks"
|
||||
# }
|
||||
|
||||
mosaic_hook_critical() {
|
||||
echo "[mosaic-stack] Recent commits:"
|
||||
git log --oneline --decorate -n 5 | sed 's/^/[mosaic-stack] /'
|
||||
echo "[mosaic-stack] Open TODO/FIXME markers (top 20):"
|
||||
rg -n "(TODO|FIXME|HACK|SECURITY)" apps packages plugins docs --glob '!**/node_modules/**' -S \
|
||||
| head -n 20 \
|
||||
| sed 's/^/[mosaic-stack] /' \
|
||||
|| true
|
||||
}
|
||||
# Called by critical.sh
|
||||
# mosaic_hook_critical() {
|
||||
# echo "Run repo-specific critical queries"
|
||||
# }
|
||||
|
||||
mosaic_hook_session_end() {
|
||||
echo "[mosaic-stack] Working tree summary:"
|
||||
git status --short | sed 's/^/[mosaic-stack] /' || true
|
||||
}
|
||||
# Called by session-end.sh
|
||||
# mosaic_hook_session_end() {
|
||||
# echo "Run repo-specific end-of-session checks"
|
||||
# }
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/
|
||||
supportedArchitectures[libc][]=glibc
|
||||
supportedArchitectures[cpu][]=x64
|
||||
@mosaic:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/
|
||||
|
||||
+6
-5
@@ -1,6 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
coverage
|
||||
pnpm-lock.yaml
|
||||
**/next-env.d.ts
|
||||
**/dist
|
||||
**/node_modules
|
||||
**/drizzle
|
||||
**/.next
|
||||
.claude/
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
# Trivy CVE Suppressions — Upstream Dependencies
|
||||
# Reviewed: 2026-02-13 | Milestone: M11-CIPipeline
|
||||
#
|
||||
# MITIGATED:
|
||||
# - Go stdlib CVEs (6): gosu rebuilt from source with Go 1.26
|
||||
# - npm bundled CVEs (5): npm removed from production Node.js images
|
||||
# - Node.js 20 → 24 LTS migration (#367): base images updated
|
||||
#
|
||||
# REMAINING: OpenBao (5 CVEs) + Next.js bundled tar/minimatch (5 CVEs)
|
||||
# Re-evaluate when upgrading openbao image beyond 2.5.0 or Next.js beyond 16.1.6.
|
||||
|
||||
# === OpenBao false positives ===
|
||||
# Trivy reads Go module pseudo-version (v0.0.0-20260204...) from bin/bao
|
||||
# and reports CVEs fixed in openbao 2.0.3–2.4.4. We run openbao:2.5.0.
|
||||
CVE-2024-8185 # HIGH: DoS via Raft join (fixed in 2.0.3)
|
||||
CVE-2024-9180 # HIGH: privilege escalation (fixed in 2.0.3)
|
||||
CVE-2025-59043 # HIGH: DoS via malicious JSON (fixed in 2.4.1)
|
||||
CVE-2025-64761 # HIGH: identity group root escalation (fixed in 2.4.4)
|
||||
|
||||
# === Next.js bundled tar/minimatch CVEs (upstream — waiting on Next.js release) ===
|
||||
# Next.js 16.1.6 bundles [email protected] and [email protected] in next/dist/compiled/ (pre-compiled).
|
||||
# These are NOT pnpm dependencies — they're embedded in the Next.js package itself.
|
||||
# pnpm overrides cannot reach these; only a Next.js upgrade can fix them.
|
||||
# Affects web image only (orchestrator and API are clean).
|
||||
# npm was also removed from all production images, eliminating the npm-bundled copy.
|
||||
# To resolve: upgrade Next.js when a release bundles tar >= 7.5.8 and minimatch >= 10.2.1.
|
||||
CVE-2026-23745 # HIGH: tar arbitrary file overwrite via unsanitized linkpaths (fixed in 7.5.3)
|
||||
CVE-2026-23950 # HIGH: tar arbitrary file overwrite via Unicode path collision (fixed in 7.5.4)
|
||||
CVE-2026-24842 # HIGH: tar arbitrary file creation via hardlink path traversal (needs tar >= 7.5.7)
|
||||
CVE-2026-26960 # HIGH: tar arbitrary file read/write via malicious archive hardlink (needs tar >= 7.5.8)
|
||||
CVE-2026-26996 # HIGH: minimatch DoS via specially crafted glob patterns (needs minimatch >= 10.2.1)
|
||||
|
||||
# === OpenBao Go stdlib (waiting on upstream rebuild) ===
|
||||
# OpenBao 2.5.0 compiled with Go 1.25.6, fix needs Go >= 1.25.7.
|
||||
# Cannot build OpenBao from source (large project). Waiting for upstream release.
|
||||
CVE-2025-68121 # CRITICAL: crypto/tls session resumption
|
||||
|
||||
# === multer CVEs (upstream via @nestjs/platform-express) ===
|
||||
# multer <2.1.0 — waiting on NestJS to update their dependency
|
||||
# These are DoS vulnerabilities in file upload handling
|
||||
GHSA-xf7r-hgr6-v32p # HIGH: DoS via incomplete cleanup
|
||||
GHSA-v52c-386h-88mc # HIGH: DoS via resource exhaustion
|
||||
@@ -1,141 +0,0 @@
|
||||
# Woodpecker CI Configuration for Mosaic Stack
|
||||
|
||||
## Pipeline Architecture
|
||||
|
||||
Split per-package pipelines with path filtering. Only affected packages rebuild on push.
|
||||
|
||||
```
|
||||
.woodpecker/
|
||||
├── api.yml # @mosaic/api (NestJS)
|
||||
├── web.yml # @mosaic/web (Next.js)
|
||||
├── orchestrator.yml # @mosaic/orchestrator (NestJS)
|
||||
├── coordinator.yml # mosaic-coordinator (Python/FastAPI)
|
||||
├── infra.yml # postgres + openbao Docker images
|
||||
├── codex-review.yml # AI code/security review (PRs only)
|
||||
├── README.md
|
||||
└── schemas/
|
||||
├── code-review-schema.json
|
||||
└── security-review-schema.json
|
||||
```
|
||||
|
||||
## Path Filtering
|
||||
|
||||
| Pipeline | Triggers On |
|
||||
| ------------------ | --------------------------------------------------- |
|
||||
| `api.yml` | `apps/api/**`, `packages/**`, root configs |
|
||||
| `web.yml` | `apps/web/**`, `packages/**`, root configs |
|
||||
| `orchestrator.yml` | `apps/orchestrator/**`, `packages/**`, root configs |
|
||||
| `coordinator.yml` | `apps/coordinator/**` |
|
||||
| `infra.yml` | `docker/**` |
|
||||
| `codex-review.yml` | All PRs (no path filter) |
|
||||
|
||||
**Root configs** = `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `turbo.json`, `package.json`
|
||||
|
||||
## Security Chain
|
||||
|
||||
Every pipeline follows the full security chain required by the CI/CD guide:
|
||||
|
||||
```
|
||||
source scanning (lint + pnpm audit / bandit + pip-audit)
|
||||
-> docker build (Kaniko)
|
||||
-> container scanning (Trivy: HIGH,CRITICAL)
|
||||
-> package linking (Gitea registry)
|
||||
```
|
||||
|
||||
Docker builds gate on ALL quality + security steps passing.
|
||||
|
||||
## Pipeline Dependency Graphs
|
||||
|
||||
### Node.js Apps (api, web, orchestrator)
|
||||
|
||||
```
|
||||
install -> [security-audit, lint, prisma-generate*]
|
||||
prisma-generate* -> [typecheck, prisma-migrate*]
|
||||
prisma-migrate* -> test
|
||||
[all quality gates] -> build -> docker-build -> trivy -> link
|
||||
```
|
||||
|
||||
_\*prisma steps: api.yml only_
|
||||
|
||||
### Coordinator (Python)
|
||||
|
||||
```
|
||||
install -> [ruff-check, mypy, security-bandit, security-pip-audit, test]
|
||||
[all quality gates] -> docker-build -> trivy -> link
|
||||
```
|
||||
|
||||
### Infrastructure
|
||||
|
||||
```
|
||||
[docker-build-postgres, docker-build-openbao]
|
||||
-> [trivy-postgres, trivy-openbao]
|
||||
-> link
|
||||
```
|
||||
|
||||
## Docker Images
|
||||
|
||||
| Image | Registry Path | Context |
|
||||
| ------------------ | ----------------------------------------------- | ------------------- |
|
||||
| stack-api | `git.mosaicstack.dev/mosaic/stack-api` | `.` (monorepo root) |
|
||||
| stack-web | `git.mosaicstack.dev/mosaic/stack-web` | `.` (monorepo root) |
|
||||
| stack-orchestrator | `git.mosaicstack.dev/mosaic/stack-orchestrator` | `.` (monorepo root) |
|
||||
| stack-coordinator | `git.mosaicstack.dev/mosaic/stack-coordinator` | `apps/coordinator` |
|
||||
| stack-postgres | `git.mosaicstack.dev/mosaic/stack-postgres` | `docker/postgres` |
|
||||
| stack-openbao | `git.mosaicstack.dev/mosaic/stack-openbao` | `docker/openbao` |
|
||||
|
||||
## Image Tagging
|
||||
|
||||
| Condition | Tag | Purpose |
|
||||
| ------------- | -------------------------- | -------------------------- |
|
||||
| Always | `${CI_COMMIT_SHA:0:8}` | Immutable commit reference |
|
||||
| `main` branch | `latest` | Current latest build |
|
||||
| Git tag | tag value (e.g., `v1.0.0`) | Semantic version release |
|
||||
|
||||
## Required Secrets
|
||||
|
||||
Configure in Woodpecker UI (Settings > Secrets):
|
||||
|
||||
| Secret | Scope | Purpose |
|
||||
| ---------------- | ----------------- | ------------------------------------------- |
|
||||
| `gitea_username` | push, manual, tag | Gitea registry auth |
|
||||
| `gitea_token` | push, manual, tag | Gitea registry auth (`package:write` scope) |
|
||||
| `codex_api_key` | pull_request | Codex AI reviews |
|
||||
|
||||
## Codex AI Review Pipeline
|
||||
|
||||
The `codex-review.yml` pipeline runs independently on all PRs:
|
||||
|
||||
- **Code review**: Correctness, code quality, testing, performance
|
||||
- **Security review**: OWASP Top 10, hardcoded secrets, injection flaws
|
||||
|
||||
Fails on blockers or critical/high severity security findings.
|
||||
|
||||
### Local Testing
|
||||
|
||||
```bash
|
||||
~/.claude/scripts/codex/codex-code-review.sh --uncommitted
|
||||
~/.claude/scripts/codex/codex-security-review.sh --uncommitted
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "unauthorized: authentication required"
|
||||
|
||||
- Verify `gitea_username` and `gitea_token` secrets in Woodpecker
|
||||
- Verify token has `package:write` scope
|
||||
|
||||
### Trivy scan fails with HIGH/CRITICAL
|
||||
|
||||
- Check if the vulnerability is in the base image (not our code)
|
||||
- Add to `.trivyignore` if it's a known, accepted risk
|
||||
- Use `--ignore-unfixed` (already set) to skip unfixable CVEs
|
||||
|
||||
### Package linking returns 404
|
||||
|
||||
- Normal for recently pushed packages — retry logic handles this
|
||||
- If persistent: verify package name matches exactly (case-sensitive)
|
||||
|
||||
### Pipeline runs Docker builds on pull requests
|
||||
|
||||
- Docker build steps have `when: branch: [main]` guards
|
||||
- PRs only run quality gates, not Docker builds
|
||||
@@ -1,27 +0,0 @@
|
||||
when:
|
||||
- event: manual
|
||||
- event: cron
|
||||
cron: weekly-base-image
|
||||
|
||||
variables:
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
steps:
|
||||
build-base:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- /kaniko/executor
|
||||
--context .
|
||||
--dockerfile docker/base.Dockerfile
|
||||
--destination git.mosaicstack.dev/mosaic/node-base:24-slim
|
||||
--destination git.mosaicstack.dev/mosaic/node-base:latest
|
||||
--cache=true
|
||||
--cache-repo git.mosaicstack.dev/mosaic/node-base/cache
|
||||
+46
-369
@@ -1,403 +1,80 @@
|
||||
# Unified CI Pipeline - Mosaic Stack
|
||||
# Single install, parallel quality gates, sequential deploy
|
||||
#
|
||||
# Replaces: api.yml, orchestrator.yml, web.yml
|
||||
# Keeps: coordinator.yml (Python), infra.yml (separate concerns)
|
||||
#
|
||||
# Flow:
|
||||
# install → security-audit
|
||||
# → prisma-generate → lint + typecheck (parallel)
|
||||
# → prisma-migrate → test
|
||||
# → build (after all gates pass)
|
||||
# → docker builds (main only, parallel)
|
||||
# → trivy scans (main only, parallel)
|
||||
# → package linking (main only)
|
||||
variables:
|
||||
- &node_image 'node:22-alpine'
|
||||
- &enable_pnpm 'corepack enable'
|
||||
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
path:
|
||||
include:
|
||||
- "apps/api/**"
|
||||
- "apps/orchestrator/**"
|
||||
- "apps/web/**"
|
||||
- "packages/**"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
- "package.json"
|
||||
- ".woodpecker/ci.yml"
|
||||
- ".trivyignore"
|
||||
|
||||
variables:
|
||||
- &node_image "node:24-slim"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
apt-get update && apt-get install -y --no-install-recommends python3 make g++
|
||||
pnpm config set store-dir /root/.local/share/pnpm/store
|
||||
pnpm install --frozen-lockfile
|
||||
- &use_deps |
|
||||
corepack enable
|
||||
- &turbo_env
|
||||
TURBO_API:
|
||||
from_secret: turbo_api
|
||||
TURBO_TOKEN:
|
||||
from_secret: turbo_token
|
||||
TURBO_TEAM:
|
||||
from_secret: turbo_team
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17.7-alpine3.22
|
||||
environment:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
# Turbo remote cache is at turbo.mosaicstack.dev (ducktors/turborepo-remote-cache).
|
||||
# TURBO_TOKEN is a Woodpecker secret injected via from_secret into the environment.
|
||||
# Turbo picks up TURBO_API, TURBO_TOKEN, and TURBO_TEAM automatically.
|
||||
|
||||
steps:
|
||||
# ─── Install (once) ─────────────────────────────────────────
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- corepack enable
|
||||
- pnpm install --frozen-lockfile
|
||||
|
||||
# ─── Security Audit (once) ──────────────────────────────────
|
||||
security-audit:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm audit --audit-level=high
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# ─── Prisma Generate ────────────────────────────────────────
|
||||
prisma-generate:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm --filter "@mosaic/api" prisma:generate
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# ─── Lint (all packages) ────────────────────────────────────
|
||||
lint:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
<<: *turbo_env
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm turbo lint
|
||||
depends_on:
|
||||
- prisma-generate
|
||||
|
||||
# ─── Typecheck (all packages, parallel with lint) ───────────
|
||||
typecheck:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
<<: *turbo_env
|
||||
TURBO_API: https://turbo.mosaicstack.dev
|
||||
TURBO_TEAM: mosaic
|
||||
TURBO_TOKEN:
|
||||
from_secret: turbo_token
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm turbo typecheck
|
||||
- *enable_pnpm
|
||||
- pnpm typecheck
|
||||
depends_on:
|
||||
- prisma-generate
|
||||
- install
|
||||
|
||||
# ─── Prisma Migrate (test DB) ──────────────────────────────
|
||||
prisma-migrate:
|
||||
# lint, format, and test are independent — run in parallel after typecheck
|
||||
lint:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
||||
TURBO_API: https://turbo.mosaicstack.dev
|
||||
TURBO_TEAM: mosaic
|
||||
TURBO_TOKEN:
|
||||
from_secret: turbo_token
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm --filter "@mosaic/api" prisma migrate deploy
|
||||
- *enable_pnpm
|
||||
- pnpm lint
|
||||
depends_on:
|
||||
- prisma-generate
|
||||
- typecheck
|
||||
|
||||
format:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- pnpm format:check
|
||||
depends_on:
|
||||
- typecheck
|
||||
|
||||
# ─── Test (all packages) ───────────────────────────────────
|
||||
test:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
||||
ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
<<: *turbo_env
|
||||
TURBO_API: https://turbo.mosaicstack.dev
|
||||
TURBO_TEAM: mosaic
|
||||
TURBO_TOKEN:
|
||||
from_secret: turbo_token
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm --filter "@mosaic/api" exec vitest run --exclude 'src/auth/auth-rls.integration.spec.ts' --exclude 'src/credentials/user-credential.model.spec.ts' --exclude 'src/job-events/job-events.performance.spec.ts' --exclude 'src/knowledge/services/fulltext-search.spec.ts' --exclude 'src/mosaic-telemetry/mosaic-telemetry.module.spec.ts'
|
||||
- pnpm turbo test --filter=@mosaic/orchestrator --filter=@mosaic/web
|
||||
- *enable_pnpm
|
||||
- pnpm test
|
||||
depends_on:
|
||||
- prisma-migrate
|
||||
- typecheck
|
||||
|
||||
# ─── Build (all packages) ──────────────────────────────────
|
||||
build:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
NODE_ENV: "production"
|
||||
<<: *turbo_env
|
||||
TURBO_API: https://turbo.mosaicstack.dev
|
||||
TURBO_TEAM: mosaic
|
||||
TURBO_TOKEN:
|
||||
from_secret: turbo_token
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm turbo build
|
||||
- *enable_pnpm
|
||||
- pnpm build
|
||||
depends_on:
|
||||
- lint
|
||||
- typecheck
|
||||
- format
|
||||
- test
|
||||
- security-audit
|
||||
|
||||
# ─── Docker Builds (main only, parallel) ───────────────────
|
||||
|
||||
docker-build-api:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-api:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-api:latest"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile apps/api/Dockerfile --snapshot-mode=redo --cache=true --cache-repo git.mosaicstack.dev/mosaic/stack-api/cache $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
docker-build-orchestrator:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-orchestrator:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-orchestrator:latest"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile apps/orchestrator/Dockerfile --snapshot-mode=redo --cache=true --cache-repo git.mosaicstack.dev/mosaic/stack-orchestrator/cache $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
docker-build-web:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-web:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-web:latest"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile apps/web/Dockerfile --snapshot-mode=redo --cache=true --cache-repo git.mosaicstack.dev/mosaic/stack-web/cache --build-arg NEXT_PUBLIC_API_URL=https://api.mosaicstack.dev $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
# ─── Container Security Scans (main only) ──────────────────
|
||||
|
||||
security-trivy-api:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-api:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-api
|
||||
|
||||
security-trivy-orchestrator:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-orchestrator:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-orchestrator
|
||||
|
||||
notify-webhook:
|
||||
image: curlimages/curl:8.6.0
|
||||
environment:
|
||||
MOSAIC_WEBHOOK_URL:
|
||||
from_secret: mosaic_webhook_url
|
||||
WOODPECKER_WEBHOOK_SECRET:
|
||||
from_secret: woodpecker_webhook_secret
|
||||
commands:
|
||||
- |
|
||||
BODY="{\"branch\":\"${CI_COMMIT_BRANCH}\",\"status\":\"${CI_PIPELINE_STATUS}\",\"buildUrl\":\"${CI_PIPELINE_LINK}\",\"repo\":\"${CI_REPO}\",\"prNumber\":${CI_COMMIT_PULL_REQUEST:-null},\"headSha\":\"${CI_COMMIT_SHA}\"}"
|
||||
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$WOODPECKER_WEBHOOK_SECRET" | awk '{print $2}')
|
||||
curl -s -o /dev/null -w "%{http_code}" -X POST "${MOSAIC_WEBHOOK_URL}/api/webhooks/woodpecker" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Woodpecker-Signature: ${SIG}" \
|
||||
-d "$BODY" || true
|
||||
when:
|
||||
- status: [success, failure]
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
security-trivy-web:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-web:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-web
|
||||
|
||||
# ─── Package Linking (main only, once) ─────────────────────
|
||||
|
||||
link-packages:
|
||||
image: alpine:3
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- sleep 10
|
||||
- |
|
||||
set -e
|
||||
link_package() {
|
||||
PKG="$$1"
|
||||
echo "Linking $$PKG..."
|
||||
for attempt in 1 2 3; do
|
||||
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $$GITEA_TOKEN" \
|
||||
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||
echo " Linked $$PKG"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "400" ]; then
|
||||
echo " $$PKG already linked"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||
sleep 5
|
||||
else
|
||||
echo " FAILED: $$PKG status $$STATUS"
|
||||
cat /tmp/link-response.txt
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
link_package "stack-api"
|
||||
link_package "stack-orchestrator"
|
||||
link_package "stack-web"
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- security-trivy-api
|
||||
- security-trivy-orchestrator
|
||||
- security-trivy-web
|
||||
|
||||
# ─── Deploy to Docker Swarm via Portainer API (main only) ─────────────────────
|
||||
|
||||
deploy-swarm:
|
||||
image: alpine:3
|
||||
failure: ignore
|
||||
environment:
|
||||
PORTAINER_URL:
|
||||
from_secret: portainer_url
|
||||
PORTAINER_API_KEY:
|
||||
from_secret: portainer_api_key
|
||||
PORTAINER_STACK_ID: "121"
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- |
|
||||
set -e
|
||||
echo "🚀 Deploying to Docker Swarm via Portainer API..."
|
||||
|
||||
# Use Portainer API to update the stack (forces pull of new images)
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "X-API-Key: $PORTAINER_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$PORTAINER_URL/api/stacks/$PORTAINER_STACK_ID/git/redeploy")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||||
BODY=$(echo "$RESPONSE" | head -n -1)
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "202" ]; then
|
||||
echo "✅ Stack update triggered successfully"
|
||||
else
|
||||
echo "❌ Stack update failed (HTTP $HTTP_CODE)"
|
||||
echo "$BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wait for services to converge
|
||||
echo "⏳ Waiting for services to converge..."
|
||||
sleep 30
|
||||
echo "✅ Deploy complete"
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- link-packages
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# Codex AI Review Pipeline for Woodpecker CI
|
||||
# Drop this into your repo's .woodpecker/ directory to enable automated
|
||||
# code and security reviews on every pull request.
|
||||
#
|
||||
# Required secrets:
|
||||
# - codex_api_key: OpenAI API key or Codex-compatible key
|
||||
#
|
||||
# Optional secrets:
|
||||
# - gitea_token: Gitea API token for posting PR comments (if not using tea CLI auth)
|
||||
|
||||
when:
|
||||
event: pull_request
|
||||
|
||||
variables:
|
||||
- &node_image "node:24-slim"
|
||||
- &install_codex "npm i -g @openai/codex"
|
||||
|
||||
steps:
|
||||
# --- Code Quality Review ---
|
||||
code-review:
|
||||
image: *node_image
|
||||
environment:
|
||||
CODEX_API_KEY:
|
||||
from_secret: codex_api_key
|
||||
commands:
|
||||
- *install_codex
|
||||
- apt-get update -qq && apt-get install -y -qq jq git > /dev/null 2>&1
|
||||
|
||||
# Generate the diff
|
||||
- git fetch origin ${CI_COMMIT_TARGET_BRANCH:-main}
|
||||
- DIFF=$(git diff origin/${CI_COMMIT_TARGET_BRANCH:-main}...HEAD)
|
||||
|
||||
# Run code review with structured output
|
||||
- |
|
||||
codex exec \
|
||||
--sandbox read-only \
|
||||
--output-schema .woodpecker/schemas/code-review-schema.json \
|
||||
-o /tmp/code-review.json \
|
||||
"You are an expert code reviewer. Review the following code changes for correctness, code quality, testing, performance, and documentation issues. Only flag actionable, important issues. Categorize as blocker/should-fix/suggestion. If code looks good, say so.
|
||||
|
||||
Changes:
|
||||
$DIFF"
|
||||
|
||||
# Output summary
|
||||
- echo "=== Code Review Results ==="
|
||||
- jq '.' /tmp/code-review.json
|
||||
- |
|
||||
BLOCKERS=$(jq '.stats.blockers // 0' /tmp/code-review.json)
|
||||
if [ "$BLOCKERS" -gt 0 ]; then
|
||||
echo "FAIL: $BLOCKERS blocker(s) found"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: No blockers found"
|
||||
|
||||
# --- Security Review ---
|
||||
security-review:
|
||||
image: *node_image
|
||||
environment:
|
||||
CODEX_API_KEY:
|
||||
from_secret: codex_api_key
|
||||
commands:
|
||||
- *install_codex
|
||||
- apt-get update -qq && apt-get install -y -qq jq git > /dev/null 2>&1
|
||||
|
||||
# Generate the diff
|
||||
- git fetch origin ${CI_COMMIT_TARGET_BRANCH:-main}
|
||||
- DIFF=$(git diff origin/${CI_COMMIT_TARGET_BRANCH:-main}...HEAD)
|
||||
|
||||
# Run security review with structured output
|
||||
- |
|
||||
codex exec \
|
||||
--sandbox read-only \
|
||||
--output-schema .woodpecker/schemas/security-review-schema.json \
|
||||
-o /tmp/security-review.json \
|
||||
"You are an expert application security engineer. Review the following code changes for security vulnerabilities including OWASP Top 10, hardcoded secrets, injection flaws, auth/authz gaps, XSS, CSRF, SSRF, path traversal, and supply chain risks. Include CWE IDs and remediation steps. Only flag real security issues, not code quality.
|
||||
|
||||
Changes:
|
||||
$DIFF"
|
||||
|
||||
# Output summary
|
||||
- echo "=== Security Review Results ==="
|
||||
- jq '.' /tmp/security-review.json
|
||||
- |
|
||||
CRITICAL=$(jq '.stats.critical // 0' /tmp/security-review.json)
|
||||
HIGH=$(jq '.stats.high // 0' /tmp/security-review.json)
|
||||
if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then
|
||||
echo "FAIL: $CRITICAL critical, $HIGH high severity finding(s)"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: No critical or high severity findings"
|
||||
@@ -1,178 +0,0 @@
|
||||
# Coordinator Pipeline - Mosaic Stack
|
||||
# Quality gates, build, and Docker publish for mosaic-coordinator (Python)
|
||||
#
|
||||
# Triggers on: apps/coordinator/**
|
||||
# Security chain: bandit + pip-audit + Trivy container scan
|
||||
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
path:
|
||||
include:
|
||||
- "apps/coordinator/**"
|
||||
- ".woodpecker/coordinator.yml"
|
||||
|
||||
variables:
|
||||
- &python_image "python:3.11-slim"
|
||||
- &activate_venv |
|
||||
cd apps/coordinator
|
||||
. venv/bin/activate
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
steps:
|
||||
# === Quality Gates ===
|
||||
|
||||
install:
|
||||
image: *python_image
|
||||
commands:
|
||||
- cd apps/coordinator
|
||||
- python -m venv venv
|
||||
- . venv/bin/activate
|
||||
- pip install --no-cache-dir --upgrade "pip>=25.3"
|
||||
- pip install --no-cache-dir --extra-index-url https://git.mosaicstack.dev/api/packages/mosaic/pypi/simple/ -e ".[dev]"
|
||||
- pip install --no-cache-dir bandit pip-audit
|
||||
|
||||
ruff-check:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- ruff check src/ tests/
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
mypy:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- mypy src/
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
security-bandit:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- bandit -r src/ -c bandit.yaml -f screen
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
security-pip-audit:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- pip-audit
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
test:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- pytest
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# === Docker Build & Push ===
|
||||
|
||||
docker-build-coordinator:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-coordinator:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-coordinator:latest"
|
||||
fi
|
||||
/kaniko/executor --context apps/coordinator --dockerfile apps/coordinator/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- ruff-check
|
||||
- mypy
|
||||
- security-bandit
|
||||
- security-pip-audit
|
||||
- test
|
||||
|
||||
# === Container Security Scan ===
|
||||
|
||||
security-trivy-coordinator:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
SCAN_TAG="latest"
|
||||
else
|
||||
SCAN_TAG="latest"
|
||||
fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
--ignorefile .trivyignore \
|
||||
git.mosaicstack.dev/mosaic/stack-coordinator:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-coordinator
|
||||
|
||||
# === Package Linking ===
|
||||
|
||||
link-packages:
|
||||
image: alpine:3
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- sleep 10
|
||||
- |
|
||||
set -e
|
||||
link_package() {
|
||||
PKG="$$1"
|
||||
echo "Linking $$PKG..."
|
||||
for attempt in 1 2 3; do
|
||||
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $$GITEA_TOKEN" \
|
||||
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||
echo " Linked $$PKG"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "400" ]; then
|
||||
echo " $$PKG already linked"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||
sleep 5
|
||||
else
|
||||
echo " FAILED: $$PKG status $$STATUS"
|
||||
cat /tmp/link-response.txt
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
link_package "stack-coordinator"
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- security-trivy-coordinator
|
||||
@@ -1,170 +0,0 @@
|
||||
# Infrastructure Pipeline - Mosaic Stack
|
||||
# Docker build, Trivy scan, and publish for postgres + openbao images
|
||||
#
|
||||
# Triggers on: docker/**
|
||||
# No quality gates — infrastructure images (base image + config only)
|
||||
|
||||
when:
|
||||
- event: [push, manual, tag]
|
||||
path:
|
||||
include:
|
||||
- "docker/**"
|
||||
- ".woodpecker/infra.yml"
|
||||
|
||||
variables:
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
steps:
|
||||
# === Docker Build & Push ===
|
||||
|
||||
docker-build-postgres:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-postgres:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-postgres:latest"
|
||||
fi
|
||||
/kaniko/executor --context docker/postgres --dockerfile docker/postgres/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
|
||||
docker-build-openbao:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-openbao:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-openbao:latest"
|
||||
fi
|
||||
/kaniko/executor --context docker/openbao --dockerfile docker/openbao/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
|
||||
# === Container Security Scans ===
|
||||
|
||||
security-trivy-postgres:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
SCAN_TAG="latest"
|
||||
else
|
||||
SCAN_TAG="latest"
|
||||
fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
--ignorefile .trivyignore \
|
||||
git.mosaicstack.dev/mosaic/stack-postgres:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-postgres
|
||||
|
||||
security-trivy-openbao:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
SCAN_TAG="latest"
|
||||
else
|
||||
SCAN_TAG="latest"
|
||||
fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
--ignorefile .trivyignore \
|
||||
git.mosaicstack.dev/mosaic/stack-openbao:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-openbao
|
||||
|
||||
# === Package Linking ===
|
||||
|
||||
link-packages:
|
||||
image: alpine:3
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- sleep 10
|
||||
- |
|
||||
set -e
|
||||
link_package() {
|
||||
PKG="$$1"
|
||||
echo "Linking $$PKG..."
|
||||
for attempt in 1 2 3; do
|
||||
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $$GITEA_TOKEN" \
|
||||
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||
echo " Linked $$PKG"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "400" ]; then
|
||||
echo " $$PKG already linked"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||
sleep 5
|
||||
else
|
||||
echo " FAILED: $$PKG status $$STATUS"
|
||||
cat /tmp/link-response.txt
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
link_package "stack-postgres"
|
||||
link_package "stack-openbao"
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- security-trivy-postgres
|
||||
- security-trivy-openbao
|
||||
@@ -1,92 +0,0 @@
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "Brief overall assessment of the code changes"
|
||||
},
|
||||
"verdict": {
|
||||
"type": "string",
|
||||
"enum": ["approve", "request-changes", "comment"],
|
||||
"description": "Overall review verdict"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Confidence score for the review (0-1)"
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["blocker", "should-fix", "suggestion"],
|
||||
"description": "Finding severity: blocker (must fix), should-fix (important), suggestion (optional)"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short title describing the issue"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "File path where the issue was found"
|
||||
},
|
||||
"line_start": {
|
||||
"type": "integer",
|
||||
"description": "Starting line number"
|
||||
},
|
||||
"line_end": {
|
||||
"type": "integer",
|
||||
"description": "Ending line number"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Detailed explanation of the issue"
|
||||
},
|
||||
"suggestion": {
|
||||
"type": "string",
|
||||
"description": "Suggested fix or improvement"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"severity",
|
||||
"title",
|
||||
"file",
|
||||
"line_start",
|
||||
"line_end",
|
||||
"description",
|
||||
"suggestion"
|
||||
]
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"files_reviewed": {
|
||||
"type": "integer",
|
||||
"description": "Number of files reviewed"
|
||||
},
|
||||
"blockers": {
|
||||
"type": "integer",
|
||||
"description": "Count of blocker findings"
|
||||
},
|
||||
"should_fix": {
|
||||
"type": "integer",
|
||||
"description": "Count of should-fix findings"
|
||||
},
|
||||
"suggestions": {
|
||||
"type": "integer",
|
||||
"description": "Count of suggestion findings"
|
||||
}
|
||||
},
|
||||
"required": ["files_reviewed", "blockers", "should_fix", "suggestions"]
|
||||
}
|
||||
},
|
||||
"required": ["summary", "verdict", "confidence", "findings", "stats"]
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "Brief overall security assessment of the code changes"
|
||||
},
|
||||
"risk_level": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low", "none"],
|
||||
"description": "Overall security risk level"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Confidence score for the review (0-1)"
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low"],
|
||||
"description": "Vulnerability severity level"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short title describing the vulnerability"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "File path where the vulnerability was found"
|
||||
},
|
||||
"line_start": {
|
||||
"type": "integer",
|
||||
"description": "Starting line number"
|
||||
},
|
||||
"line_end": {
|
||||
"type": "integer",
|
||||
"description": "Ending line number"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Detailed explanation of the vulnerability"
|
||||
},
|
||||
"cwe_id": {
|
||||
"type": "string",
|
||||
"description": "CWE identifier if applicable (e.g., CWE-79)"
|
||||
},
|
||||
"owasp_category": {
|
||||
"type": "string",
|
||||
"description": "OWASP Top 10 category if applicable (e.g., A03:2021-Injection)"
|
||||
},
|
||||
"remediation": {
|
||||
"type": "string",
|
||||
"description": "Specific remediation steps to fix the vulnerability"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"severity",
|
||||
"title",
|
||||
"file",
|
||||
"line_start",
|
||||
"line_end",
|
||||
"description",
|
||||
"cwe_id",
|
||||
"owasp_category",
|
||||
"remediation"
|
||||
]
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"files_reviewed": {
|
||||
"type": "integer",
|
||||
"description": "Number of files reviewed"
|
||||
},
|
||||
"critical": {
|
||||
"type": "integer",
|
||||
"description": "Count of critical findings"
|
||||
},
|
||||
"high": {
|
||||
"type": "integer",
|
||||
"description": "Count of high findings"
|
||||
},
|
||||
"medium": {
|
||||
"type": "integer",
|
||||
"description": "Count of medium findings"
|
||||
},
|
||||
"low": {
|
||||
"type": "integer",
|
||||
"description": "Count of low findings"
|
||||
}
|
||||
},
|
||||
"required": ["files_reviewed", "critical", "high", "medium", "low"]
|
||||
}
|
||||
},
|
||||
"required": ["summary", "risk_level", "confidence", "findings", "stats"]
|
||||
}
|
||||
@@ -1,82 +1,55 @@
|
||||
# Mosaic Stack — Agent Guidelines
|
||||
# Agent Guidelines — Mosaic Stack
|
||||
|
||||
## Load Order
|
||||
## Required Load Order
|
||||
|
||||
1. `SOUL.md` (repo identity + behavior invariants)
|
||||
2. `~/.config/mosaic/STANDARDS.md` (machine-wide standards rails)
|
||||
3. `AGENTS.md` (repo-specific overlay)
|
||||
4. `.mosaic/repo-hooks.sh` (repo lifecycle hooks)
|
||||
1. `~/.config/mosaic/SOUL.md`
|
||||
2. `~/.config/mosaic/STANDARDS.md`
|
||||
3. `~/.config/mosaic/AGENTS.md`
|
||||
4. `~/.config/mosaic/guides/E2E-DELIVERY.md`
|
||||
5. `AGENTS.md` (this file)
|
||||
6. Runtime-specific guide: `~/.config/mosaic/runtime/<runtime>/RUNTIME.md`
|
||||
|
||||
## Runtime Contract
|
||||
## Project Context
|
||||
|
||||
- This file is authoritative for repo-local operations.
|
||||
- `CLAUDE.md` is a compatibility pointer to `AGENTS.md`.
|
||||
- Follow universal rails from `~/.config/mosaic/guides/` and `~/.config/mosaic/rails/`.
|
||||
Mosaic Stack is a self-hosted, multi-user AI agent platform. TypeScript monorepo with NestJS gateway, Next.js web dashboard, Pi SDK agent runtime, and plugin architecture for Discord/Telegram.
|
||||
|
||||
## Session Lifecycle
|
||||
## Package Map
|
||||
|
||||
| Package | Purpose | Key Dependencies |
|
||||
| ------------------ | ------------------------------- | -------------------------------- |
|
||||
| `apps/gateway` | NestJS API + WebSocket hub | Fastify, Socket.IO, Pi SDK, OTEL |
|
||||
| `apps/web` | Next.js dashboard | React 19, Tailwind |
|
||||
| `packages/types` | Shared TypeScript contracts | class-validator |
|
||||
| `packages/db` | Drizzle ORM schema + migrations | drizzle-orm, postgres |
|
||||
| `packages/auth` | BetterAuth configuration | better-auth, @mosaic/db |
|
||||
| `packages/brain` | Data layer (PG-backed) | @mosaic/db |
|
||||
| `packages/queue` | Valkey task queue + MCP | ioredis |
|
||||
| `packages/coord` | Mission coordination | @mosaic/queue |
|
||||
| `packages/cli` | Unified CLI + Pi TUI | Ink, Pi SDK |
|
||||
| `plugins/discord` | Discord channel plugin | discord.js |
|
||||
| `plugins/telegram` | Telegram channel plugin | Telegraf |
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
1. Gateway is the single API surface — all clients connect through it
|
||||
2. Pi SDK is ESM-only — gateway and CLI must use ESM
|
||||
3. Socket.IO typed events defined in `@mosaic/types` enforce compile-time contracts
|
||||
4. OTEL auto-instrumentation loads before NestJS bootstrap
|
||||
5. BetterAuth manages auth tables; schema defined in `@mosaic/db`
|
||||
6. Docker Compose provides PG (5433), Valkey (6380), OTEL Collector (4317/4318), Jaeger (16686)
|
||||
7. Explicit `@Inject()` decorators required in NestJS (tsx/esbuild doesn't emit decorator metadata)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
```bash
|
||||
bash scripts/agent/session-start.sh
|
||||
bash scripts/agent/critical.sh
|
||||
bash scripts/agent/session-end.sh
|
||||
docker compose up -d # Infrastructure
|
||||
pnpm install # Dependencies
|
||||
pnpm typecheck && pnpm lint && pnpm format:check # Quality gates
|
||||
```
|
||||
|
||||
Optional:
|
||||
## Repo-Specific Notes
|
||||
|
||||
```bash
|
||||
bash scripts/agent/log-limitation.sh "Short Name"
|
||||
bash scripts/agent/orchestrator-daemon.sh status
|
||||
bash scripts/agent/orchestrator-events.sh recent --limit 50
|
||||
```
|
||||
|
||||
## Repo Context
|
||||
|
||||
- Platform: multi-tenant personal assistant stack
|
||||
- Monorepo: `pnpm` workspaces + Turborepo
|
||||
- Core apps: `apps/api` (NestJS), `apps/web` (Next.js), orchestrator/coordinator services
|
||||
- Infrastructure: Docker Compose + PostgreSQL + Valkey + Authentik
|
||||
|
||||
## Quick Command Set
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
pnpm test
|
||||
pnpm lint
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Versioning Protocol (HARD GATE)
|
||||
|
||||
**This project is ALPHA. All versions MUST be `0.0.x`.**
|
||||
|
||||
- The `0.1.0` release is FORBIDDEN until Jason explicitly authorizes it.
|
||||
- Every milestone bump increments the patch: `0.0.20` → `0.0.21` → `0.0.22`, etc.
|
||||
- ALL package.json files in the monorepo MUST stay in sync at the same version.
|
||||
- Use `scripts/version-bump.sh <version>` to bump — it enforces the alpha constraint and updates all packages atomically.
|
||||
- The script rejects any version >= `0.1.0`.
|
||||
- When creating a release tag, the tag MUST match the package version: `v0.0.x`.
|
||||
|
||||
**Milestone-to-version mapping** is defined in the PRD (`docs/PRD.md`) under "Delivery/Milestone Intent". Agents MUST use the version from that table when tagging a milestone release.
|
||||
|
||||
**Violation of this protocol is a blocking error.** If an agent attempts to set a version >= `0.1.0`, stop and escalate.
|
||||
|
||||
## Standards and Quality
|
||||
|
||||
- Enforce strict typing and no unsafe shortcuts.
|
||||
- Keep lint/typecheck/tests green before completion.
|
||||
- Prefer small, focused commits and clear change descriptions.
|
||||
|
||||
## App-Specific Overlays
|
||||
|
||||
- `apps/api/AGENTS.md`
|
||||
- `apps/web/AGENTS.md`
|
||||
- `apps/coordinator/AGENTS.md`
|
||||
- `apps/orchestrator/AGENTS.md`
|
||||
|
||||
## Additional Guidance
|
||||
|
||||
- Orchestrator guidance: `docs/claude/orchestrator.md`
|
||||
- Security remediation context: `docs/reports/codebase-review-2026-02-05/01-security-review.md`
|
||||
- Code quality context: `docs/reports/codebase-review-2026-02-05/02-code-quality-review.md`
|
||||
- QA context: `docs/reports/codebase-review-2026-02-05/03-qa-test-coverage.md`
|
||||
- DTOs in `*.dto.ts` files at module boundaries
|
||||
- ESM everywhere (`"type": "module"`, `.js` extensions in imports)
|
||||
- NodeNext module resolution in all tsconfigs
|
||||
- Scratchpads are mandatory for non-trivial tasks
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
# CLAUDE Compatibility Pointer
|
||||
# CLAUDE.md — Mosaic Stack
|
||||
|
||||
This file exists so Claude Code sessions load Mosaic standards.
|
||||
## Project
|
||||
|
||||
## MANDATORY — Read Before Any Response
|
||||
Self-hosted, multi-user AI agent platform. TypeScript monorepo.
|
||||
|
||||
BEFORE responding to any user message, READ `~/.config/mosaic/AGENTS.md`.
|
||||
## Stack
|
||||
|
||||
That file is the universal agent configuration. Do NOT respond until you have loaded it.
|
||||
Then read the project-local `AGENTS.md` in this repository for project-specific guidance.
|
||||
- **API**: NestJS + Fastify adapter (`apps/gateway`)
|
||||
- **Web**: Next.js 16 + React 19 (`apps/web`)
|
||||
- **ORM**: Drizzle ORM + PostgreSQL 17 + pgvector (`packages/db`)
|
||||
- **Auth**: BetterAuth (`packages/auth`)
|
||||
- **Agent**: Pi SDK (`packages/agent`, `packages/cli`)
|
||||
- **Queue**: Valkey 8 (`packages/queue`)
|
||||
- **Build**: pnpm workspaces + Turborepo
|
||||
- **CI**: Woodpecker CI
|
||||
- **Observability**: OpenTelemetry → Jaeger
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm typecheck # TypeScript check (all packages)
|
||||
pnpm lint # ESLint (all packages)
|
||||
pnpm format:check # Prettier check
|
||||
pnpm test # Vitest (all packages)
|
||||
pnpm build # Build all packages
|
||||
|
||||
# Database
|
||||
pnpm --filter @mosaic/db db:push # Push schema to PG (dev)
|
||||
pnpm --filter @mosaic/db db:generate # Generate migrations
|
||||
pnpm --filter @mosaic/db db:migrate # Run migrations
|
||||
|
||||
# Dev
|
||||
docker compose up -d # Start PG, Valkey, OTEL, Jaeger
|
||||
pnpm --filter @mosaic/gateway exec tsx src/main.ts # Start gateway
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- ESM everywhere (`"type": "module"`, `.js` extensions in imports)
|
||||
- NodeNext module resolution
|
||||
- Explicit `@Inject()` decorators in NestJS (tsx/esbuild doesn't support emitDecoratorMetadata)
|
||||
- DTOs in `*.dto.ts` files at module boundaries
|
||||
- OTEL tracing imported before NestJS bootstrap (`import './tracing.js'`)
|
||||
- All three gates must pass before push: typecheck, lint, format:check
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# Cron Job Configuration - Issue #29
|
||||
|
||||
## Overview
|
||||
|
||||
Implement cron job configuration for Mosaic Stack, likely as a MoltBot plugin for scheduled reminders/commands.
|
||||
|
||||
## Requirements (inferred from CLAUDE.md pattern)
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
plugins/mosaic-plugin-cron/
|
||||
├── SKILL.md # MoltBot skill definition
|
||||
├── src/
|
||||
│ └── cron.service.ts
|
||||
└── cron.service.test.ts
|
||||
```
|
||||
|
||||
### Core Features
|
||||
|
||||
1. Create/update/delete cron schedules
|
||||
2. Trigger MoltBot commands on schedule
|
||||
3. Workspace-scoped (RLS)
|
||||
4. PDA-friendly UI
|
||||
|
||||
### API Endpoints (inferred)
|
||||
|
||||
- `POST /api/cron` - Create schedule
|
||||
- `GET /api/cron` - List schedules
|
||||
- `DELETE /api/cron/:id` - Delete schedule
|
||||
|
||||
### Database (Prisma)
|
||||
|
||||
```prisma
|
||||
model CronSchedule {
|
||||
id String @id @default(uuid())
|
||||
workspaceId String
|
||||
expression String // cron expression
|
||||
command String // MoltBot command to trigger
|
||||
enabled Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([workspaceId])
|
||||
}
|
||||
```
|
||||
|
||||
## TDD Approach
|
||||
|
||||
1. **RED** - Write tests for CronService
|
||||
2. **GREEN** - Implement minimal service
|
||||
3. **REFACTOR** - Add CRUD controller + API endpoints
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [ ] Create feature branch: `git checkout -b feature/29-cron-config`
|
||||
- [ ] Write failing tests for cron service
|
||||
- [ ] Implement service (Green)
|
||||
- [ ] Add controller & routes
|
||||
- [ ] Add Prisma schema migration
|
||||
- [ ] Create MoltBot skill (SKILL.md)
|
||||
@@ -1,145 +0,0 @@
|
||||
.PHONY: help install dev build test docker-up docker-down docker-logs docker-ps docker-build docker-restart docker-test speech-up speech-down speech-logs clean matrix-up matrix-down matrix-logs matrix-setup-bot
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Mosaic Stack - Available commands:"
|
||||
@echo ""
|
||||
@echo "Development:"
|
||||
@echo " make install Install dependencies"
|
||||
@echo " make dev Start development servers"
|
||||
@echo " make build Build all applications"
|
||||
@echo " make test Run all tests"
|
||||
@echo " make lint Run linters"
|
||||
@echo " make format Format code"
|
||||
@echo ""
|
||||
@echo "Docker:"
|
||||
@echo " make docker-up Start Docker services (core)"
|
||||
@echo " make docker-up-full Start Docker services (all)"
|
||||
@echo " make docker-up-traefik Start with bundled Traefik"
|
||||
@echo " make docker-down Stop Docker services"
|
||||
@echo " make docker-logs View Docker logs"
|
||||
@echo " make docker-ps Show Docker service status"
|
||||
@echo " make docker-build Rebuild Docker images"
|
||||
@echo " make docker-restart Restart Docker services"
|
||||
@echo " make docker-test Run Docker smoke test"
|
||||
@echo " make docker-test-traefik Run Traefik integration tests"
|
||||
@echo ""
|
||||
@echo "Speech Services:"
|
||||
@echo " make speech-up Start speech services (STT + TTS)"
|
||||
@echo " make speech-down Stop speech services"
|
||||
@echo " make speech-logs View speech service logs"
|
||||
@echo ""
|
||||
@echo "Matrix Dev Environment:"
|
||||
@echo " make matrix-up Start Matrix services (Synapse + Element)"
|
||||
@echo " make matrix-down Stop Matrix services"
|
||||
@echo " make matrix-logs View Matrix service logs"
|
||||
@echo " make matrix-setup-bot Create bot account and get access token"
|
||||
@echo ""
|
||||
@echo "Database:"
|
||||
@echo " make db-migrate Run database migrations"
|
||||
@echo " make db-seed Seed development data"
|
||||
@echo " make db-studio Open Prisma Studio"
|
||||
@echo " make db-reset Reset database (WARNING: deletes data)"
|
||||
@echo ""
|
||||
@echo "Cleanup:"
|
||||
@echo " make clean Clean build artifacts"
|
||||
@echo " make clean-all Clean everything including node_modules"
|
||||
@echo " make docker-clean Remove Docker containers and volumes"
|
||||
|
||||
# Development
|
||||
install:
|
||||
pnpm install
|
||||
|
||||
dev:
|
||||
pnpm dev
|
||||
|
||||
build:
|
||||
pnpm build
|
||||
|
||||
test:
|
||||
pnpm test
|
||||
|
||||
lint:
|
||||
pnpm lint
|
||||
|
||||
format:
|
||||
pnpm format
|
||||
|
||||
# Docker operations
|
||||
docker-up:
|
||||
docker compose up -d
|
||||
|
||||
docker-up-full:
|
||||
docker compose --profile full up -d
|
||||
|
||||
docker-up-traefik:
|
||||
docker compose --profile traefik-bundled up -d
|
||||
|
||||
docker-down:
|
||||
docker compose down
|
||||
|
||||
docker-logs:
|
||||
docker compose logs -f
|
||||
|
||||
docker-ps:
|
||||
docker compose ps
|
||||
|
||||
docker-build:
|
||||
docker compose build
|
||||
|
||||
docker-restart:
|
||||
docker compose restart
|
||||
|
||||
docker-test:
|
||||
./scripts/test-docker-deployment.sh
|
||||
|
||||
docker-test-traefik:
|
||||
./tests/integration/docker/traefik.test.sh all
|
||||
|
||||
# Speech services
|
||||
speech-up:
|
||||
docker compose -f docker-compose.yml -f docker-compose.speech.yml up -d speaches kokoro-tts
|
||||
|
||||
speech-down:
|
||||
docker compose -f docker-compose.yml -f docker-compose.speech.yml down --remove-orphans
|
||||
|
||||
speech-logs:
|
||||
docker compose -f docker-compose.yml -f docker-compose.speech.yml logs -f speaches kokoro-tts
|
||||
|
||||
# Matrix Dev Environment
|
||||
matrix-up:
|
||||
docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml up -d
|
||||
|
||||
matrix-down:
|
||||
docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml down
|
||||
|
||||
matrix-logs:
|
||||
docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml logs -f synapse element-web
|
||||
|
||||
matrix-setup-bot:
|
||||
docker/matrix/scripts/setup-bot.sh
|
||||
|
||||
# Database operations
|
||||
db-migrate:
|
||||
cd apps/api && pnpm prisma:migrate
|
||||
|
||||
db-seed:
|
||||
cd apps/api && pnpm prisma:seed
|
||||
|
||||
db-studio:
|
||||
cd apps/api && pnpm prisma:studio
|
||||
|
||||
db-reset:
|
||||
cd apps/api && pnpm prisma:reset
|
||||
|
||||
# Cleanup
|
||||
clean:
|
||||
pnpm clean
|
||||
|
||||
clean-all:
|
||||
pnpm clean
|
||||
rm -rf node_modules
|
||||
|
||||
docker-clean:
|
||||
docker compose down -v
|
||||
docker system prune -f
|
||||
@@ -1,817 +0,0 @@
|
||||
# Mosaic Stack
|
||||
|
||||
Multi-tenant personal assistant platform with PostgreSQL backend, Authentik SSO, and MoltBot integration.
|
||||
|
||||
## Overview
|
||||
|
||||
Mosaic Stack is a modern, PDA-friendly platform designed to help users manage their personal and professional lives with:
|
||||
|
||||
- **Multi-user workspaces** with team collaboration
|
||||
- **Knowledge management** with wiki-style linking and version history
|
||||
- **Task management** with flexible organization
|
||||
- **Event & calendar** integration
|
||||
- **Project tracking** with Gantt charts and Kanban boards
|
||||
- **MoltBot integration** for natural language interactions
|
||||
- **Authentik OIDC** for secure, enterprise-grade authentication
|
||||
|
||||
**Version:** 0.0.1 (Pre-MVP)
|
||||
**Repository:** https://git.mosaicstack.dev/mosaic/stack
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
| -------------- | ---------------------------------------------- |
|
||||
| **Frontend** | Next.js 16 + React + TailwindCSS + Shadcn/ui |
|
||||
| **Backend** | NestJS + Prisma ORM |
|
||||
| **Database** | PostgreSQL 17 + pgvector |
|
||||
| **Cache** | Valkey (Redis-compatible) |
|
||||
| **Auth** | Authentik (OIDC) via BetterAuth |
|
||||
| **AI** | Ollama (local or remote) |
|
||||
| **Messaging** | MoltBot (stock + plugins) |
|
||||
| **Real-time** | WebSockets (Socket.io) |
|
||||
| **Speech** | Speaches (STT) + Kokoro/Chatterbox/Piper (TTS) |
|
||||
| **Monorepo** | pnpm workspaces + TurboRepo |
|
||||
| **Testing** | Vitest + Playwright |
|
||||
| **Deployment** | Docker + docker-compose |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### One-Line Install (Recommended)
|
||||
|
||||
The fastest way to get Mosaic Stack running on macOS or Linux:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.mosaicstack.dev | bash
|
||||
```
|
||||
|
||||
This installer:
|
||||
|
||||
- ✅ Detects your platform (macOS, Debian/Ubuntu, Arch, Fedora)
|
||||
- ✅ Installs all required dependencies (Docker, Node.js, etc.)
|
||||
- ✅ Generates secure secrets automatically
|
||||
- ✅ Configures the environment for you
|
||||
- ✅ Starts all services with Docker Compose
|
||||
- ✅ Validates the installation with health checks
|
||||
|
||||
**Installer Options:**
|
||||
|
||||
```bash
|
||||
# Non-interactive Docker deployment
|
||||
curl -fsSL https://get.mosaicstack.dev | bash -s -- --non-interactive --mode docker
|
||||
|
||||
# Preview installation without making changes
|
||||
curl -fsSL https://get.mosaicstack.dev | bash -s -- --dry-run
|
||||
|
||||
# With SSO and local Ollama
|
||||
curl -fsSL https://get.mosaicstack.dev | bash -s -- \
|
||||
--mode docker \
|
||||
--enable-sso --bundled-authentik \
|
||||
--ollama-mode local
|
||||
|
||||
# Skip dependency installation (if already installed)
|
||||
curl -fsSL https://get.mosaicstack.dev | bash -s -- --skip-deps
|
||||
```
|
||||
|
||||
**After Installation:**
|
||||
|
||||
```bash
|
||||
# Check system health
|
||||
./scripts/commands/doctor.sh
|
||||
|
||||
# View service logs
|
||||
docker compose logs -f
|
||||
|
||||
# Stop services
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
If you prefer manual installation, you'll need:
|
||||
|
||||
- **Docker mode:** Docker 24+ and Docker Compose
|
||||
- **Native mode:** Node.js 24+, pnpm 10+, PostgreSQL 17+
|
||||
|
||||
The installer handles these automatically.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://git.mosaicstack.dev/mosaic/stack mosaic-stack
|
||||
cd mosaic-stack
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Copy environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Configure environment variables (see CONFIGURATION.md)
|
||||
# Edit .env with your database and auth settings
|
||||
|
||||
# Generate Prisma client
|
||||
pnpm prisma:generate
|
||||
|
||||
# Run database migrations
|
||||
pnpm prisma:migrate
|
||||
|
||||
# Seed development data (optional)
|
||||
pnpm prisma:seed
|
||||
|
||||
# Start development servers
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
**Recommended for quick setup and production deployments.**
|
||||
|
||||
#### Development (Turnkey - All Services Bundled)
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://git.mosaicstack.dev/mosaic/stack mosaic-stack
|
||||
cd mosaic-stack
|
||||
|
||||
# Copy and configure environment
|
||||
cp .env.example .env
|
||||
# Set COMPOSE_PROFILES=full in .env
|
||||
|
||||
# Start all services (PostgreSQL, Valkey, OpenBao, Authentik, Ollama, API, Web)
|
||||
docker compose up -d
|
||||
|
||||
# View logs
|
||||
docker compose logs -f
|
||||
|
||||
# Access services
|
||||
# Web: http://localhost:3000
|
||||
# API: http://localhost:3001
|
||||
# Auth: http://localhost:9000
|
||||
```
|
||||
|
||||
#### Production (External Managed Services)
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://git.mosaicstack.dev/mosaic/stack mosaic-stack
|
||||
cd mosaic-stack
|
||||
|
||||
# Copy environment template and example
|
||||
cp .env.example .env
|
||||
cp docker/docker-compose.example.external.yml docker-compose.override.yml
|
||||
|
||||
# Edit .env with external service URLs:
|
||||
# - DATABASE_URL=postgresql://... (RDS, Cloud SQL, etc.)
|
||||
# - VALKEY_URL=redis://... (ElastiCache, Memorystore, etc.)
|
||||
# - OPENBAO_ADDR=https://... (HashiCorp Vault, etc.)
|
||||
# - OIDC_ISSUER=https://... (Auth0, Okta, etc.)
|
||||
# - Set COMPOSE_PROFILES= (empty)
|
||||
|
||||
# Start API and Web only
|
||||
docker compose up -d
|
||||
|
||||
# View logs
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
#### Hybrid (Mix of Bundled and External)
|
||||
|
||||
```bash
|
||||
# Use bundled database/cache, external auth/secrets
|
||||
cp docker/docker-compose.example.hybrid.yml docker-compose.override.yml
|
||||
|
||||
# Edit .env:
|
||||
# - COMPOSE_PROFILES=database,cache,ollama
|
||||
# - OPENBAO_ADDR=https://... (external vault)
|
||||
# - OIDC_ISSUER=https://... (external auth)
|
||||
|
||||
# Start mixed deployment
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Stop services:**
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
**What's included:**
|
||||
|
||||
- PostgreSQL 17 with pgvector extension
|
||||
- Valkey (Redis-compatible cache)
|
||||
- Mosaic API (NestJS)
|
||||
- Mosaic Web (Next.js)
|
||||
- Mosaic Orchestrator (Agent lifecycle management)
|
||||
- Mosaic Coordinator (Task assignment & monitoring)
|
||||
- Authentik OIDC (optional, use `--profile authentik`)
|
||||
- Ollama AI (optional, use `--profile ollama`)
|
||||
|
||||
See [Docker Deployment Guide](docs/1-getting-started/4-docker-deployment/) for complete documentation.
|
||||
|
||||
### Docker Swarm Deployment (Production)
|
||||
|
||||
**Recommended for production deployments with high availability and auto-scaling.**
|
||||
|
||||
Deploy to a Docker Swarm cluster with integrated Traefik reverse proxy:
|
||||
|
||||
```bash
|
||||
# 1. Initialize swarm (if not already done)
|
||||
docker swarm init --advertise-addr <your-ip>
|
||||
|
||||
# 2. Create Traefik network
|
||||
docker network create --driver=overlay traefik-public
|
||||
|
||||
# 3. Configure environment for swarm
|
||||
cp .env.swarm.example .env
|
||||
nano .env # Configure domains, passwords, API keys
|
||||
|
||||
# 4. CRITICAL: Deploy OpenBao standalone FIRST
|
||||
# OpenBao cannot run in swarm mode - deploy as standalone container
|
||||
docker compose -f docker-compose.openbao.yml up -d
|
||||
sleep 30 # Wait for auto-initialization
|
||||
|
||||
# 5. Deploy swarm stack
|
||||
IMAGE_TAG=latest ./scripts/deploy-swarm.sh mosaic
|
||||
|
||||
# 6. Check deployment status
|
||||
docker stack services mosaic
|
||||
docker stack ps mosaic
|
||||
|
||||
# Access services via Traefik
|
||||
# Web: http://mosaic.mosaicstack.dev
|
||||
# API: http://api.mosaicstack.dev
|
||||
# Auth: http://auth.mosaicstack.dev (if using bundled Authentik)
|
||||
```
|
||||
|
||||
**Key features:**
|
||||
|
||||
- Automatic Traefik integration for routing
|
||||
- Overlay networking for multi-host deployments
|
||||
- Built-in health checks and rolling updates
|
||||
- Horizontal scaling for web and API services
|
||||
- Zero-downtime deployments
|
||||
- Service orchestration across multiple nodes
|
||||
|
||||
**Important Notes:**
|
||||
|
||||
- **OpenBao Requirement:** OpenBao MUST be deployed as standalone container (not in swarm). Use `docker-compose.openbao.yml` or external Vault.
|
||||
- Swarm does NOT support docker-compose profiles
|
||||
- To use external services (PostgreSQL, Authentik, etc.), manually comment them out in `docker-compose.swarm.yml`
|
||||
|
||||
See [Docker Swarm Deployment Guide](docs/SWARM-DEPLOYMENT.md) and [Quick Reference](docs/SWARM-QUICKREF.md) for complete documentation.
|
||||
|
||||
### Portainer Deployment
|
||||
|
||||
**Recommended for GUI-based stack management.**
|
||||
|
||||
Portainer provides a web UI for managing Docker containers and stacks. Use the Portainer-optimized compose file:
|
||||
|
||||
**File:** `docker-compose.portainer.yml`
|
||||
|
||||
**Key differences from standard compose:**
|
||||
|
||||
- No `env_file` directive (define variables in Portainer UI)
|
||||
- Port exposed on all interfaces (Portainer limitation)
|
||||
- Optimized for Portainer's stack parser
|
||||
|
||||
**Quick Steps:**
|
||||
|
||||
1. Create `mosaic_internal` overlay network in Portainer
|
||||
2. Deploy `mosaic-openbao` stack with `docker-compose.portainer.yml`
|
||||
3. Deploy `mosaic` swarm stack with `docker-compose.swarm.yml`
|
||||
4. Configure environment variables in Portainer UI
|
||||
|
||||
See [Portainer Deployment Guide](docs/PORTAINER-DEPLOYMENT.md) for detailed instructions.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mosaic-stack/
|
||||
├── apps/
|
||||
│ ├── api/ # NestJS backend API
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── auth/ # BetterAuth + Authentik OIDC
|
||||
│ │ │ ├── prisma/ # Database service
|
||||
│ │ │ ├── coordinator-integration/ # Coordinator API client
|
||||
│ │ │ └── app.module.ts # Main application module
|
||||
│ │ ├── prisma/
|
||||
│ │ │ └── schema.prisma # Database schema
|
||||
│ │ └── Dockerfile
|
||||
│ ├── web/ # Next.js 16 frontend
|
||||
│ │ ├── app/
|
||||
│ │ ├── components/
|
||||
│ │ │ └── widgets/ # HUD widgets (agent status, etc.)
|
||||
│ │ └── Dockerfile
|
||||
│ ├── orchestrator/ # Agent lifecycle & spawning (NestJS)
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── spawner/ # Agent spawning service
|
||||
│ │ │ ├── queue/ # Valkey-backed task queue
|
||||
│ │ │ ├── monitor/ # Health monitoring
|
||||
│ │ │ ├── git/ # Git worktree management
|
||||
│ │ │ └── killswitch/ # Emergency agent termination
|
||||
│ │ └── Dockerfile
|
||||
│ └── coordinator/ # Task assignment & monitoring (FastAPI)
|
||||
│ ├── src/
|
||||
│ │ ├── webhook.py # Gitea webhook receiver
|
||||
│ │ ├── parser.py # Issue metadata parser
|
||||
│ │ └── security.py # HMAC signature verification
|
||||
│ └── Dockerfile
|
||||
├── packages/
|
||||
│ ├── shared/ # Shared types & utilities
|
||||
│ │ └── src/types/
|
||||
│ │ ├── auth.types.ts # Auth types (AuthUser, Session, etc.)
|
||||
│ │ ├── database.types.ts # DB entity types
|
||||
│ │ └── enums.ts # Shared enums
|
||||
│ ├── ui/ # Shared UI components (planned)
|
||||
│ └── config/ # Shared configuration (planned)
|
||||
├── plugins/ # MoltBot skills (planned)
|
||||
│ ├── mosaic-plugin-brain/ # API query skill
|
||||
│ ├── mosaic-plugin-calendar/ # Calendar skill
|
||||
│ └── mosaic-plugin-tasks/ # Task management skill
|
||||
├── docker/
|
||||
│ ├── docker-compose.yml # Production deployment
|
||||
│ └── init-scripts/ # PostgreSQL initialization
|
||||
├── docs/
|
||||
│ ├── SETUP.md # Installation guide
|
||||
│ ├── CONFIGURATION.md # Environment configuration
|
||||
│ ├── DESIGN-PRINCIPLES.md # PDA-friendly design patterns
|
||||
│ ├── API.md # API documentation
|
||||
│ ├── TYPE-SHARING.md # Type sharing strategy
|
||||
│ └── scratchpads/ # Development notes
|
||||
├── .env.example # Environment template
|
||||
├── turbo.json # TurboRepo configuration
|
||||
└── pnpm-workspace.yaml # Workspace configuration
|
||||
```
|
||||
|
||||
## Agent Orchestration Layer (v0.0.6)
|
||||
|
||||
Mosaic Stack includes a sophisticated agent orchestration system for autonomous task execution:
|
||||
|
||||
- **Orchestrator Service** (NestJS) - Manages agent lifecycle, spawning, and health monitoring
|
||||
- **Coordinator Service** (FastAPI) - Receives Gitea webhooks, assigns tasks to agents
|
||||
- **Task Queue** - Valkey-backed queue for distributed task management
|
||||
- **Git Worktrees** - Isolated workspaces for parallel agent execution
|
||||
- **Killswitch** - Emergency stop mechanism for runaway agents
|
||||
- **Agent Dashboard** - Real-time monitoring UI with status widgets
|
||||
|
||||
See [Agent Orchestration Design](docs/design/agent-orchestration.md) for architecture details.
|
||||
|
||||
## Speech Services
|
||||
|
||||
Mosaic Stack includes integrated speech-to-text (STT) and text-to-speech (TTS) capabilities through a modular provider architecture. Each component is optional and independently configurable.
|
||||
|
||||
- **Speech-to-Text** - Transcribe audio files and real-time audio streams using Whisper (via Speaches)
|
||||
- **Text-to-Speech** - Synthesize speech with 54+ voices across 8 languages (via Kokoro, CPU-based)
|
||||
- **Premium Voice Cloning** - Clone voices from audio samples with emotion control (via Chatterbox, GPU)
|
||||
- **Fallback TTS** - Ultra-lightweight CPU fallback for low-resource environments (via Piper/OpenedAI Speech)
|
||||
- **WebSocket Streaming** - Real-time streaming transcription via Socket.IO `/speech` namespace
|
||||
- **Automatic Fallback** - TTS tier system with graceful degradation (premium -> default -> fallback)
|
||||
|
||||
**Quick Start:**
|
||||
|
||||
```bash
|
||||
# Start speech services alongside core stack
|
||||
make speech-up
|
||||
|
||||
# Or with Docker Compose directly
|
||||
docker compose -f docker-compose.yml -f docker-compose.speech.yml up -d
|
||||
```
|
||||
|
||||
See [Speech Services Documentation](docs/SPEECH.md) for architecture details, API reference, provider configuration, and deployment options.
|
||||
|
||||
## Current Implementation Status
|
||||
|
||||
### ✅ Completed (v0.0.1-0.0.6)
|
||||
|
||||
- **M1-Foundation:** Project scaffold, PostgreSQL 17 + pgvector, Prisma ORM
|
||||
- **M2-MultiTenant:** Workspace isolation with RLS, team management
|
||||
- **M3-Features:** Knowledge management, tasks, calendar, authentication
|
||||
- **M4-MoltBot:** Bot integration architecture (in progress)
|
||||
- **M6-AgentOrchestration:** Orchestrator service, coordinator, agent dashboard ✅
|
||||
|
||||
**Test Coverage:** 2168+ tests passing
|
||||
|
||||
### 🚧 In Progress (v0.0.x)
|
||||
|
||||
- Agent orchestration E2E testing
|
||||
- Usage budget management
|
||||
- Performance optimization
|
||||
|
||||
### 📋 Planned Features (v0.1.0 MVP)
|
||||
|
||||
- Event/calendar management
|
||||
- Project tracking with Gantt charts
|
||||
- MoltBot integration
|
||||
- WebSocket real-time updates
|
||||
- Data migration from jarvis-brain
|
||||
|
||||
See the [issue tracker](https://git.mosaicstack.dev/mosaic/stack/issues) for complete roadmap.
|
||||
|
||||
## Knowledge Module
|
||||
|
||||
The **Knowledge Module** is a powerful personal wiki and knowledge management system built into Mosaic Stack. Create interconnected notes, organize with tags, track changes over time, and visualize relationships.
|
||||
|
||||
### Features
|
||||
|
||||
- **📝 Markdown-based entries** — Write using familiar Markdown syntax
|
||||
- **🔗 Wiki-style linking** — Connect entries using `[[wiki-links]]`
|
||||
- **🏷️ Tag organization** — Categorize and filter with flexible tagging
|
||||
- **📜 Full version history** — Every edit is tracked and recoverable
|
||||
- **🔍 Powerful search** — Full-text search across titles and content
|
||||
- **📊 Knowledge graph** — Visualize relationships between entries
|
||||
- **📤 Import/Export** — Bulk import/export for portability
|
||||
- **⚡ Valkey caching** — High-performance caching for fast access
|
||||
|
||||
### Quick Examples
|
||||
|
||||
**Create an entry:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/knowledge/entries \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "x-workspace-id: WORKSPACE_ID" \
|
||||
-d '{
|
||||
"title": "React Hooks Guide",
|
||||
"content": "# React Hooks\n\nSee [[Component Patterns]] for more.",
|
||||
"tags": ["react", "frontend"],
|
||||
"status": "PUBLISHED"
|
||||
}'
|
||||
```
|
||||
|
||||
**Search entries:**
|
||||
|
||||
```bash
|
||||
curl -X GET 'http://localhost:3001/api/knowledge/search?q=react+hooks' \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "x-workspace-id: WORKSPACE_ID"
|
||||
```
|
||||
|
||||
**Export knowledge base:**
|
||||
|
||||
```bash
|
||||
curl -X GET 'http://localhost:3001/api/knowledge/export?format=markdown' \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "x-workspace-id: WORKSPACE_ID" \
|
||||
-o knowledge-export.zip
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- **[User Guide](KNOWLEDGE_USER_GUIDE.md)** — Getting started, features, and workflows
|
||||
- **[API Documentation](KNOWLEDGE_API.md)** — Complete REST API reference with examples
|
||||
- **[Developer Guide](KNOWLEDGE_DEV.md)** — Architecture, implementation, and contributing
|
||||
|
||||
### Key Concepts
|
||||
|
||||
**Wiki-links**
|
||||
Connect entries using double-bracket syntax:
|
||||
|
||||
```markdown
|
||||
See [[Entry Title]] or [[entry-slug]] for details.
|
||||
Use [[Page|custom text]] for custom display text.
|
||||
```
|
||||
|
||||
**Version History**
|
||||
Every edit creates a new version. View history, compare changes, and restore previous versions:
|
||||
|
||||
```bash
|
||||
# List versions
|
||||
GET /api/knowledge/entries/:slug/versions
|
||||
|
||||
# Get specific version
|
||||
GET /api/knowledge/entries/:slug/versions/:version
|
||||
|
||||
# Restore version
|
||||
POST /api/knowledge/entries/:slug/restore/:version
|
||||
```
|
||||
|
||||
**Backlinks**
|
||||
Automatically discover entries that link to a given entry:
|
||||
|
||||
```bash
|
||||
GET /api/knowledge/entries/:slug/backlinks
|
||||
```
|
||||
|
||||
**Tags**
|
||||
Organize entries with tags:
|
||||
|
||||
```bash
|
||||
# Create tag
|
||||
POST /api/knowledge/tags
|
||||
{ "name": "React", "color": "#61dafb" }
|
||||
|
||||
# Find entries with tags
|
||||
GET /api/knowledge/search/by-tags?tags=react,frontend
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
With Valkey caching enabled:
|
||||
|
||||
- **Entry retrieval:** ~2-5ms (vs ~50ms uncached)
|
||||
- **Search queries:** ~2-5ms (vs ~200ms uncached)
|
||||
- **Graph traversals:** ~2-5ms (vs ~400ms uncached)
|
||||
- **Cache hit rates:** 70-90% for active workspaces
|
||||
|
||||
Configure caching via environment variables:
|
||||
|
||||
```bash
|
||||
VALKEY_URL=redis://localhost:6379
|
||||
KNOWLEDGE_CACHE_ENABLED=true
|
||||
KNOWLEDGE_CACHE_TTL=300 # 5 minutes
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Branch Strategy
|
||||
|
||||
- `main` — Trunk branch (all development merges here)
|
||||
- `feature/*` — Feature branches from main
|
||||
- `fix/*` — Bug fix branches from main
|
||||
|
||||
### Running Locally
|
||||
|
||||
```bash
|
||||
# Start all services (requires Docker for PostgreSQL)
|
||||
pnpm dev # All apps
|
||||
|
||||
# Or run individually
|
||||
pnpm dev:api # API only (http://localhost:3001)
|
||||
pnpm dev:web # Web only (http://localhost:3000)
|
||||
|
||||
# Database tools
|
||||
pnpm prisma:studio # Open Prisma Studio
|
||||
pnpm prisma:migrate # Run migrations
|
||||
pnpm prisma:generate # Regenerate Prisma client
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
pnpm test # All tests
|
||||
pnpm test:api # API tests only
|
||||
pnpm test:web # Web tests only
|
||||
pnpm test:e2e # E2E tests with Playwright
|
||||
pnpm test:coverage # Generate coverage report
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
pnpm build # Build all apps
|
||||
pnpm build:api # Build API only
|
||||
pnpm build:web # Build web only
|
||||
```
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Mosaic Stack follows strict **PDA-friendly design principles**:
|
||||
|
||||
### Language Guidelines
|
||||
|
||||
We **never** use demanding or stressful language:
|
||||
|
||||
| ❌ NEVER | ✅ ALWAYS |
|
||||
| ----------- | -------------------- |
|
||||
| OVERDUE | Target passed |
|
||||
| URGENT | Approaching target |
|
||||
| MUST DO | Scheduled for |
|
||||
| CRITICAL | High priority |
|
||||
| YOU NEED TO | Consider / Option to |
|
||||
| REQUIRED | Recommended |
|
||||
|
||||
### Visual Principles
|
||||
|
||||
- **10-second scannability** — Key info visible immediately
|
||||
- **Visual chunking** — Clear sections with headers
|
||||
- **Single-line items** — Compact, scannable lists
|
||||
- **Calm colors** — No aggressive reds for status indicators
|
||||
- **Progressive disclosure** — Details on click, not upfront
|
||||
|
||||
See [Design Principles](docs/3-architecture/3-design-principles/1-pda-friendly.md) for complete guidelines.
|
||||
|
||||
## API Conventions
|
||||
|
||||
### Endpoints
|
||||
|
||||
```
|
||||
GET /api/{resource} # List (with pagination, filters)
|
||||
GET /api/{resource}/:id # Get single item
|
||||
POST /api/{resource} # Create new item
|
||||
PATCH /api/{resource}/:id # Update item
|
||||
DELETE /api/{resource}/:id # Delete item
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
All authenticated endpoints require a Bearer token:
|
||||
|
||||
```http
|
||||
Authorization: Bearer {session_token}
|
||||
```
|
||||
|
||||
See [API Reference](docs/4-api/) for complete API documentation.
|
||||
|
||||
## Configuration
|
||||
|
||||
Key environment variables:
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgresql://mosaic:password@localhost:5432/mosaic
|
||||
|
||||
# Authentik OIDC
|
||||
OIDC_ISSUER=https://auth.example.com/application/o/mosaic-stack/
|
||||
OIDC_CLIENT_ID=your-client-id
|
||||
OIDC_CLIENT_SECRET=your-client-secret
|
||||
|
||||
# JWT Session
|
||||
JWT_SECRET=change-this-to-a-random-secret-in-production
|
||||
JWT_EXPIRATION=24h
|
||||
|
||||
# Application
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
See [Configuration](docs/1-getting-started/3-configuration/1-environment.md) for all configuration options.
|
||||
|
||||
## Caching
|
||||
|
||||
Mosaic Stack uses **Valkey** (Redis-compatible) for high-performance caching, significantly improving response times for frequently accessed data.
|
||||
|
||||
### Knowledge Module Caching
|
||||
|
||||
The Knowledge module implements intelligent caching for:
|
||||
|
||||
- **Entry Details** - Individual knowledge entries (GET `/api/knowledge/entries/:slug`)
|
||||
- **Search Results** - Full-text search queries with filters
|
||||
- **Graph Queries** - Knowledge graph traversals with depth limits
|
||||
|
||||
### Cache Configuration
|
||||
|
||||
Configure caching via environment variables:
|
||||
|
||||
```bash
|
||||
# Valkey connection
|
||||
VALKEY_URL=redis://localhost:6379
|
||||
|
||||
# Knowledge cache settings
|
||||
KNOWLEDGE_CACHE_ENABLED=true # Set to false to disable caching (dev mode)
|
||||
KNOWLEDGE_CACHE_TTL=300 # Time-to-live in seconds (default: 5 minutes)
|
||||
```
|
||||
|
||||
### Cache Invalidation Strategy
|
||||
|
||||
Caches are automatically invalidated on data changes:
|
||||
|
||||
- **Entry Updates** - Invalidates entry cache, search caches, and related graph caches
|
||||
- **Entry Creation** - Invalidates search caches and graph caches
|
||||
- **Entry Deletion** - Invalidates entry cache, search caches, and graph caches
|
||||
- **Link Changes** - Invalidates graph caches for affected entries
|
||||
|
||||
### Cache Statistics & Management
|
||||
|
||||
Monitor and manage caches via REST endpoints:
|
||||
|
||||
```bash
|
||||
# Get cache statistics (hits, misses, hit rate)
|
||||
GET /api/knowledge/cache/stats
|
||||
|
||||
# Clear all caches for a workspace (admin only)
|
||||
POST /api/knowledge/cache/clear
|
||||
|
||||
# Reset cache statistics (admin only)
|
||||
POST /api/knowledge/cache/stats/reset
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"stats": {
|
||||
"hits": 1250,
|
||||
"misses": 180,
|
||||
"sets": 195,
|
||||
"deletes": 15,
|
||||
"hitRate": 0.874
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Benefits
|
||||
|
||||
- **Entry retrieval:** ~10-50ms → ~2-5ms (80-90% improvement)
|
||||
- **Search queries:** ~100-300ms → ~2-5ms (95-98% improvement)
|
||||
- **Graph traversals:** ~200-500ms → ~2-5ms (95-99% improvement)
|
||||
|
||||
Cache hit rates typically stabilize at 70-90% for active workspaces.
|
||||
|
||||
## Type Sharing
|
||||
|
||||
Types used by both frontend and backend live in `@mosaic/shared`:
|
||||
|
||||
```typescript
|
||||
import type { AuthUser, Task, Event } from "@mosaic/shared";
|
||||
|
||||
// Frontend
|
||||
function UserProfile({ user }: { user: AuthUser }) {
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
|
||||
// Backend
|
||||
@Get("profile")
|
||||
@UseGuards(AuthGuard)
|
||||
getProfile(@CurrentUser() user: AuthUser) {
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
See [Type Sharing Strategy](docs/2-development/3-type-sharing/1-strategy.md) for the complete type sharing strategy.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Check the [issue tracker](https://git.mosaicstack.dev/mosaic/stack/issues) for open issues
|
||||
2. Create a feature branch: `git checkout -b feature/my-feature develop`
|
||||
3. Make your changes with tests (minimum 85% coverage required)
|
||||
4. Run tests: `pnpm test`
|
||||
5. Build: `pnpm build`
|
||||
6. Commit with conventional format: `feat(#issue): Description`
|
||||
7. Push and create a pull request to `main`
|
||||
|
||||
### Commit Format
|
||||
|
||||
```
|
||||
<type>(#issue): Brief description
|
||||
|
||||
Detailed explanation if needed.
|
||||
|
||||
Fixes #123
|
||||
```
|
||||
|
||||
**Types:** `feat`, `fix`, `docs`, `test`, `refactor`, `chore`
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- **Minimum 85% coverage** for new code
|
||||
- **TDD approach** — Write tests before implementation
|
||||
- **All tests pass** before PR merge
|
||||
- Use Vitest for unit/integration tests
|
||||
- Use Playwright for E2E tests
|
||||
|
||||
## Documentation
|
||||
|
||||
Complete documentation is organized in a Bookstack-compatible structure in the `docs/` directory.
|
||||
|
||||
### 📚 Getting Started
|
||||
|
||||
- **[Quick Start](docs/1-getting-started/1-quick-start/1-overview.md)** — Get running in 5 minutes
|
||||
- **[Installation](docs/1-getting-started/2-installation/)** — Prerequisites, local setup, Docker deployment
|
||||
- **[Configuration](docs/1-getting-started/3-configuration/)** — Environment variables and Authentik OIDC
|
||||
|
||||
### 💻 Development
|
||||
|
||||
- **[Workflow](docs/2-development/1-workflow/)** — Branching strategy, testing requirements, commit guidelines
|
||||
- **[Database](docs/2-development/2-database/)** — Schema design, migrations, Prisma usage
|
||||
- **[Type Sharing](docs/2-development/3-type-sharing/1-strategy.md)** — Shared types across monorepo
|
||||
|
||||
### 🏗️ Architecture
|
||||
|
||||
- **[Overview](docs/3-architecture/1-overview/)** — System design and components
|
||||
- **[Authentication](docs/3-architecture/2-authentication/)** — BetterAuth and OIDC integration
|
||||
- **[Design Principles](docs/3-architecture/3-design-principles/1-pda-friendly.md)** — PDA-friendly patterns (non-negotiable)
|
||||
- **[Telemetry](docs/telemetry.md)** — AI task completion tracking, predictions, and SDK reference
|
||||
|
||||
### 🔌 API Reference
|
||||
|
||||
- **[Conventions](docs/4-api/1-conventions/1-endpoints.md)** — REST patterns, pagination, filtering
|
||||
- **[Authentication](docs/4-api/2-authentication/1-endpoints.md)** — Auth endpoints and flows
|
||||
|
||||
**Browse all documentation:** [docs/](docs/)
|
||||
|
||||
## Related Projects
|
||||
|
||||
- **jarvis-brain** — Original JSON-based personal assistant (migration source)
|
||||
- **MoltBot** — Stock messaging gateway for multi-platform integration
|
||||
|
||||
## Security
|
||||
|
||||
- **Session-based authentication** with secure JWT tokens
|
||||
- **Row-level security** ready for multi-tenant isolation
|
||||
- **OIDC integration** with Authentik for enterprise SSO
|
||||
- **Secure error handling** — No sensitive data in logs or responses
|
||||
- **Type-safe validation** — TypeScript catches issues at compile time
|
||||
|
||||
## License
|
||||
|
||||
[Add license information]
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues:** https://git.mosaicstack.dev/mosaic/stack/issues
|
||||
- **Documentation:** https://git.mosaicstack.dev/mosaic/stack/wiki
|
||||
|
||||
---
|
||||
|
||||
**Mosaic Stack v0.0.1** — Building the future of personal assistants.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Mosaic Stack Soul
|
||||
|
||||
You are Jarvis for the Mosaic Stack repository, running on the current agent runtime.
|
||||
|
||||
## Behavioral Invariants
|
||||
|
||||
- Identity first: answer identity prompts as Jarvis for this repository.
|
||||
- Implementation detail second: runtime (Codex/Claude/OpenCode/etc.) is secondary metadata.
|
||||
- Be proactive: surface risks, blockers, and next actions without waiting.
|
||||
- Be calm and clear: keep responses concise, chunked, and PDA-friendly.
|
||||
- Respect canonical sources:
|
||||
- Repo operations and conventions: `AGENTS.md`
|
||||
- Machine-wide rails: `~/.config/mosaic/STANDARDS.md`
|
||||
- Repo lifecycle hooks: `.mosaic/repo-hooks.sh`
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not claim completion without verification evidence.
|
||||
- Do not bypass lint/type/test quality gates.
|
||||
- Prefer explicit assumptions and concrete file/command references.
|
||||
@@ -1,48 +0,0 @@
|
||||
# Node modules
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log
|
||||
|
||||
# Build output
|
||||
dist
|
||||
build
|
||||
*.tsbuildinfo
|
||||
|
||||
# Tests
|
||||
coverage
|
||||
.vitest
|
||||
test
|
||||
*.spec.ts
|
||||
*.test.ts
|
||||
|
||||
# Development files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
docs
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
@@ -1,45 +0,0 @@
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/database
|
||||
|
||||
# System Administration
|
||||
# Comma-separated list of user IDs that have system administrator privileges
|
||||
# These users can perform system-level operations across all workspaces
|
||||
# Note: Workspace ownership does NOT grant system admin access
|
||||
# SYSTEM_ADMIN_IDS=uuid1,uuid2,uuid3
|
||||
|
||||
# Federation Instance Identity
|
||||
# Display name for this Mosaic instance
|
||||
INSTANCE_NAME=Mosaic Instance
|
||||
# Publicly accessible URL for federation (must be valid HTTP/HTTPS URL)
|
||||
INSTANCE_URL=http://localhost:3000
|
||||
|
||||
# Encryption (AES-256-GCM for sensitive data at rest)
|
||||
# CRITICAL: Generate a secure random key for production!
|
||||
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
|
||||
# CSRF Protection (Required in production)
|
||||
# Secret key for HMAC binding CSRF tokens to user sessions
|
||||
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
# In development, a random key is generated if not set
|
||||
CSRF_SECRET=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210
|
||||
|
||||
# Gatekeeper merge automation
|
||||
GITEA_WEBHOOK_SECRET=replace-with-gitea-webhook-secret
|
||||
GITEA_API_TOKEN=replace-with-gitea-api-token
|
||||
GATEKEEPER_ENABLED=true
|
||||
|
||||
# OpenTelemetry Configuration
|
||||
# Enable/disable OpenTelemetry tracing (default: true)
|
||||
OTEL_ENABLED=true
|
||||
# Service name for telemetry (default: mosaic-api)
|
||||
OTEL_SERVICE_NAME=mosaic-api
|
||||
# OTLP exporter endpoint (default: http://localhost:4318/v1/traces)
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
||||
# Alternative: Jaeger endpoint (legacy)
|
||||
# OTEL_EXPORTER_JAEGER_ENDPOINT=http://localhost:4318/v1/traces
|
||||
# Deployment environment (default: development, or uses NODE_ENV)
|
||||
# OTEL_DEPLOYMENT_ENVIRONMENT=production
|
||||
# Trace sampling ratio: 0.0 (none) to 1.0 (all) - default: 1.0
|
||||
# Use lower values in high-traffic production environments
|
||||
# OTEL_TRACES_SAMPLER_ARG=1.0
|
||||
@@ -1,5 +0,0 @@
|
||||
DATABASE_URL="postgresql://test:test@localhost:5432/test"
|
||||
ENCRYPTION_KEY="test-encryption-key-32-characters"
|
||||
JWT_SECRET="test-jwt-secret"
|
||||
INSTANCE_NAME="Test Instance"
|
||||
INSTANCE_URL="https://test.example.com"
|
||||
@@ -1,9 +0,0 @@
|
||||
# WARNING: These are example test credentials for local integration testing.
|
||||
# Copy this file to .env.test and customize the values for your local environment.
|
||||
# NEVER use these credentials in any shared environment or commit .env.test to git.
|
||||
|
||||
DATABASE_URL="postgresql://test:test@localhost:5432/test"
|
||||
ENCRYPTION_KEY="test-encryption-key-32-characters"
|
||||
JWT_SECRET="test-jwt-secret"
|
||||
INSTANCE_NAME="Test Instance"
|
||||
INSTANCE_URL="https://test.example.com"
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "es2022",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"keepClassNames": true
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# api — Agent Context
|
||||
|
||||
> Part of the apps layer.
|
||||
|
||||
## Patterns
|
||||
|
||||
- **Config validation pattern**: Config files use exported validation functions + typed getter functions (not class-validator). See `auth.config.ts`, `federation.config.ts`, `speech/speech.config.ts`. Pattern: export `isXEnabled()`, `validateXConfig()`, and `getXConfig()` functions.
|
||||
- **Config registerAs**: `speech.config.ts` also exports a `registerAs("speech", ...)` factory for NestJS ConfigModule namespaced injection. Use `ConfigModule.forFeature(speechConfig)` in module imports and access via `this.config.get<string>('speech.stt.baseUrl')`.
|
||||
- **Conditional config validation**: When a service has an enabled flag (e.g., `STT_ENABLED`), URL/connection vars are only required when enabled. Validation throws with a helpful message suggesting how to disable.
|
||||
- **Boolean env parsing**: Use `value === "true" || value === "1"` pattern. No default-true -- all services default to disabled when env var is unset.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Prisma client must be generated** before `tsc --noEmit` will pass. Run `pnpm prisma:generate` first. Pre-existing type errors from Prisma are expected in worktrees without generated client.
|
||||
- **Pre-commit hooks**: lint-staged runs on staged files. If other packages' files are staged, their lint must pass too. Only stage files you intend to commit.
|
||||
- **vitest runs all test files**: Even when targeting a specific test file, vitest loads all spec files. Many will fail if Prisma client isn't generated -- this is expected. Check only your target file's pass/fail status.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `src/speech/speech.config.ts` | Speech services env var validation and typed config (STT, TTS, limits) |
|
||||
| `src/speech/speech.config.spec.ts` | Unit tests for speech config validation (51 tests) |
|
||||
| `src/auth/auth.config.ts` | Auth/OIDC config validation (reference pattern) |
|
||||
| `src/federation/federation.config.ts` | Federation config validation (reference pattern) |
|
||||
@@ -1,113 +0,0 @@
|
||||
# Base image for all stages
|
||||
# Uses Debian slim (glibc) instead of Alpine (musl) because native Node.js addons
|
||||
# (matrix-sdk-crypto-nodejs, Prisma engines) require glibc-compatible binaries.
|
||||
FROM git.mosaicstack.dev/mosaic/node-base:24-slim AS base
|
||||
|
||||
# Install pnpm globally
|
||||
RUN corepack enable && corepack prepare [email protected] --activate
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy monorepo configuration files
|
||||
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
|
||||
COPY turbo.json ./
|
||||
|
||||
# ======================
|
||||
# Dependencies stage
|
||||
# ======================
|
||||
FROM base AS deps
|
||||
|
||||
# Install build tools for native addons (node-pty requires node-gyp compilation)
|
||||
# Note: openssl and ca-certificates pre-installed in base image
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy all package.json files for workspace resolution
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
COPY packages/ui/package.json ./packages/ui/
|
||||
COPY packages/config/package.json ./packages/config/
|
||||
COPY apps/api/package.json ./apps/api/
|
||||
|
||||
# Copy npm configuration for native binary architecture hints
|
||||
COPY .npmrc ./
|
||||
|
||||
# Install dependencies (no cache mount — Kaniko builds are ephemeral in CI)
|
||||
# Then explicitly rebuild node-pty from source since pnpm may skip postinstall
|
||||
# scripts or fail to find prebuilt binaries for this Node.js version
|
||||
RUN pnpm install --frozen-lockfile \
|
||||
&& cd node_modules/.pnpm/node-pty@*/node_modules/node-pty \
|
||||
&& npx node-gyp rebuild 2>&1 || true
|
||||
|
||||
# ======================
|
||||
# Builder stage
|
||||
# ======================
|
||||
FROM base AS builder
|
||||
|
||||
# Copy root node_modules from deps
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
# Copy all source code FIRST
|
||||
COPY packages ./packages
|
||||
COPY apps/api ./apps/api
|
||||
|
||||
# Then copy workspace node_modules from deps (these go AFTER source to avoid being overwritten)
|
||||
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
|
||||
COPY --from=deps /app/packages/config/node_modules ./packages/config/node_modules
|
||||
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
||||
|
||||
# Build the API app and its dependencies using TurboRepo
|
||||
# --force disables turbo cache to ensure fresh build from source
|
||||
RUN pnpm turbo build --filter=@mosaic/api --force
|
||||
|
||||
# ======================
|
||||
# Production stage
|
||||
# ======================
|
||||
FROM git.mosaicstack.dev/mosaic/node-base:24-slim AS production
|
||||
|
||||
# dumb-init, openssl, ca-certificates pre-installed in base image
|
||||
|
||||
# Single RUN to minimize Kaniko filesystem snapshots (each RUN = full snapshot)
|
||||
# - Remove npm/npx to reduce image size (not used in production)
|
||||
# - Create non-root user
|
||||
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \
|
||||
&& groupadd -g 1001 nodejs && useradd -m -u 1001 -g nodejs nestjs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy node_modules from builder (includes generated Prisma client in pnpm store)
|
||||
# pnpm stores the Prisma client in node_modules/.pnpm/.../.prisma, so we need the full tree
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules
|
||||
|
||||
# Copy built packages (includes dist/ directories)
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/packages ./packages
|
||||
|
||||
# Copy built API application
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/dist ./apps/api/dist
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/prisma ./apps/api/prisma
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/package.json ./apps/api/
|
||||
# Copy app's node_modules which contains symlinks to root node_modules
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/node_modules ./apps/api/node_modules
|
||||
|
||||
# Copy entrypoint script (runs migrations before starting app)
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/docker-entrypoint.sh ./apps/api/
|
||||
|
||||
# Set working directory to API app
|
||||
WORKDIR /app/apps/api
|
||||
|
||||
# Switch to non-root user
|
||||
USER nestjs
|
||||
|
||||
# Expose API port (default 3001, can be overridden via PORT env var)
|
||||
EXPOSE ${PORT:-3001}
|
||||
|
||||
# Health check uses PORT env var (set by docker-compose or defaults to 3001)
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD node -e "const port = process.env.PORT || 3001; require('http').get('http://localhost:' + port + '/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||
|
||||
# Use dumb-init to handle signals properly
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
# Run migrations then start the application
|
||||
CMD ["sh", "docker-entrypoint.sh"]
|
||||
@@ -1,260 +0,0 @@
|
||||
# Mosaic Stack API
|
||||
|
||||
The Mosaic Stack API is a NestJS-based backend service providing REST endpoints and WebSocket support for the Mosaic productivity platform.
|
||||
|
||||
## Overview
|
||||
|
||||
The API serves as the central backend for:
|
||||
|
||||
- **Task Management** - Create, update, track tasks with filtering and sorting
|
||||
- **Event Management** - Calendar events and scheduling
|
||||
- **Project Management** - Organize work into projects
|
||||
- **Knowledge Base** - Wiki-style documentation with markdown support and wiki-linking
|
||||
- **Ideas** - Quick capture and organization of ideas
|
||||
- **Domains** - Categorize work across different domains
|
||||
- **Personalities** - AI personality configurations for the Ollama integration
|
||||
- **Widgets & Layouts** - Dashboard customization
|
||||
- **Activity Logging** - Track all user actions
|
||||
- **WebSocket Events** - Real-time updates for tasks, events, and projects
|
||||
|
||||
## Available Modules
|
||||
|
||||
| Module | Base Path | Description |
|
||||
| ------------------ | --------------------------- | ---------------------------------------- |
|
||||
| **Tasks** | `/api/tasks` | CRUD operations for tasks with filtering |
|
||||
| **Events** | `/api/events` | Calendar events and scheduling |
|
||||
| **Projects** | `/api/projects` | Project management |
|
||||
| **Knowledge** | `/api/knowledge/entries` | Wiki entries with markdown support |
|
||||
| **Knowledge Tags** | `/api/knowledge/tags` | Tag management for knowledge entries |
|
||||
| **Ideas** | `/api/ideas` | Quick capture and idea management |
|
||||
| **Domains** | `/api/domains` | Domain categorization |
|
||||
| **Personalities** | `/api/personalities` | AI personality configurations |
|
||||
| **Widgets** | `/api/widgets` | Dashboard widget data |
|
||||
| **Layouts** | `/api/layouts` | Dashboard layout configuration |
|
||||
| **Ollama** | `/api/ollama` | LLM integration (generate, chat, embed) |
|
||||
| **Users** | `/api/users/me/preferences` | User preferences |
|
||||
|
||||
### Health Check
|
||||
|
||||
- `GET /` - API health check
|
||||
- `GET /health` - Detailed health status including database connectivity
|
||||
|
||||
## Authentication
|
||||
|
||||
The API uses **BetterAuth** for authentication with the following features:
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. **Email/Password** - Users can sign up and log in with email and password
|
||||
2. **Session Tokens** - BetterAuth generates session tokens with configurable expiration
|
||||
|
||||
### Guards
|
||||
|
||||
The API uses a layered guard system:
|
||||
|
||||
| Guard | Purpose | Applies To |
|
||||
| ------------------- | ------------------------------------------------------------------------ | -------------------------- |
|
||||
| **AuthGuard** | Verifies user authentication via Bearer token | Most protected endpoints |
|
||||
| **WorkspaceGuard** | Validates workspace membership and sets Row-Level Security (RLS) context | Workspace-scoped resources |
|
||||
| **PermissionGuard** | Enforces role-based access control | Admin operations |
|
||||
|
||||
### Workspace Roles
|
||||
|
||||
- **OWNER** - Full control over workspace
|
||||
- **ADMIN** - Administrative functions (can delete content, manage members)
|
||||
- **MEMBER** - Standard access (create/edit content)
|
||||
- **GUEST** - Read-only access
|
||||
|
||||
### Permission Levels
|
||||
|
||||
Used with `@RequirePermission()` decorator:
|
||||
|
||||
```typescript
|
||||
Permission.WORKSPACE_OWNER; // Requires OWNER role
|
||||
Permission.WORKSPACE_ADMIN; // Requires ADMIN or OWNER
|
||||
Permission.WORKSPACE_MEMBER; // Requires MEMBER, ADMIN, or OWNER
|
||||
Permission.WORKSPACE_ANY; // Any authenticated member including GUEST
|
||||
```
|
||||
|
||||
### Providing Workspace Context
|
||||
|
||||
Workspace ID can be provided via:
|
||||
|
||||
1. **Header**: `X-Workspace-Id: <workspace-id>` (highest priority)
|
||||
2. **URL Parameter**: `:workspaceId`
|
||||
3. **Request Body**: `workspaceId` field
|
||||
|
||||
### Example: Protected Controller
|
||||
|
||||
```typescript
|
||||
@Controller("tasks")
|
||||
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
||||
export class TasksController {
|
||||
@Post()
|
||||
@RequirePermission(Permission.WORKSPACE_MEMBER)
|
||||
async create(@Body() dto: CreateTaskDto, @Workspace() workspaceId: string) {
|
||||
// workspaceId is verified and RLS context is set
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --------------------- | ----------------------------------------- | ----------------------- |
|
||||
| `PORT` | API server port | `3001` |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | Required |
|
||||
| `NODE_ENV` | Environment (`development`, `production`) | - |
|
||||
| `NEXT_PUBLIC_APP_URL` | Frontend application URL (for CORS) | `http://localhost:3000` |
|
||||
| `WEB_URL` | WebSocket CORS origin | `http://localhost:3000` |
|
||||
|
||||
## Running Locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- PostgreSQL database
|
||||
- pnpm workspace (part of Mosaic Stack monorepo)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install dependencies:**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Set up environment variables:**
|
||||
|
||||
```bash
|
||||
cp .env.example .env # If available
|
||||
# Edit .env with your DATABASE_URL
|
||||
```
|
||||
|
||||
3. **Generate Prisma client:**
|
||||
|
||||
```bash
|
||||
pnpm prisma:generate
|
||||
```
|
||||
|
||||
4. **Run database migrations:**
|
||||
|
||||
```bash
|
||||
pnpm prisma:migrate
|
||||
```
|
||||
|
||||
5. **Seed the database (optional):**
|
||||
```bash
|
||||
pnpm prisma:seed
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The API will start on `http://localhost:3001`
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
### Database Management
|
||||
|
||||
```bash
|
||||
# Open Prisma Studio
|
||||
pnpm prisma:studio
|
||||
|
||||
# Reset database (dev only)
|
||||
pnpm prisma:reset
|
||||
|
||||
# Run migrations in production
|
||||
pnpm prisma:migrate:prod
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
The API does not currently include Swagger/OpenAPI documentation. Instead:
|
||||
|
||||
- **Controller files** contain detailed JSDoc comments describing each endpoint
|
||||
- **DTO classes** define request/response schemas with class-validator decorators
|
||||
- Refer to the controller source files in `src/` for endpoint details
|
||||
|
||||
### Example: Reading an Endpoint
|
||||
|
||||
```typescript
|
||||
// src/tasks/tasks.controller.ts
|
||||
|
||||
/**
|
||||
* POST /api/tasks
|
||||
* Create a new task
|
||||
* Requires: MEMBER role or higher
|
||||
*/
|
||||
@Post()
|
||||
@RequirePermission(Permission.WORKSPACE_MEMBER)
|
||||
async create(@Body() createTaskDto: CreateTaskDto, @Workspace() workspaceId: string) {
|
||||
return this.tasksService.create(workspaceId, user.id, createTaskDto);
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket Support
|
||||
|
||||
The API provides real-time updates via WebSocket. Clients receive notifications for:
|
||||
|
||||
- `task:created` - New task created
|
||||
- `task:updated` - Task modified
|
||||
- `task:deleted` - Task removed
|
||||
- `event:created` - New event created
|
||||
- `event:updated` - Event modified
|
||||
- `event:deleted` - Event removed
|
||||
- `project:updated` - Project modified
|
||||
|
||||
Clients join workspace-specific rooms for scoped updates.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run unit tests
|
||||
pnpm test
|
||||
|
||||
# Run tests with coverage
|
||||
pnpm test:coverage
|
||||
|
||||
# Run e2e tests
|
||||
pnpm test:e2e
|
||||
|
||||
# Watch mode
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── activity/ # Activity logging
|
||||
├── auth/ # Authentication (BetterAuth config, guards)
|
||||
├── common/ # Shared decorators and guards
|
||||
├── database/ # Database module
|
||||
├── domains/ # Domain management
|
||||
├── events/ # Event management
|
||||
├── filters/ # Global exception filters
|
||||
├── ideas/ # Idea capture and management
|
||||
├── knowledge/ # Knowledge base (entries, tags, markdown)
|
||||
├── layouts/ # Dashboard layouts
|
||||
├── lib/ # Utility functions
|
||||
├── ollama/ # LLM integration
|
||||
├── personalities/ # AI personality configurations
|
||||
├── prisma/ # Prisma service
|
||||
├── projects/ # Project management
|
||||
├── tasks/ # Task management
|
||||
├── users/ # User preferences
|
||||
├── widgets/ # Dashboard widgets
|
||||
├── websocket/ # WebSocket gateway
|
||||
├── app.controller.ts # Root controller (health check)
|
||||
├── app.module.ts # Root module
|
||||
└── main.ts # Application bootstrap
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "Running database migrations..."
|
||||
./node_modules/.bin/prisma migrate deploy --schema ./prisma/schema.prisma
|
||||
|
||||
echo "Starting application..."
|
||||
exec node dist/main.js
|
||||
@@ -1,16 +0,0 @@
|
||||
import nestjsConfig from "@mosaic/config/eslint/nestjs";
|
||||
|
||||
export default [
|
||||
...nestjsConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**", "node_modules/**", "**/*.test.ts", "**/*.spec.ts"],
|
||||
},
|
||||
];
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
{
|
||||
"name": "@mosaic/api",
|
||||
"version": "0.0.20",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "nest start --watch",
|
||||
"start": "node dist/main",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node scripts/vitest-runner.mjs",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "vitest run --config ./vitest.e2e.config.ts",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:migrate:prod": "prisma migrate deploy",
|
||||
"prisma:studio": "prisma studio",
|
||||
"prisma:seed": "prisma db seed",
|
||||
"prisma:reset": "prisma migrate reset",
|
||||
"migrate:encrypt-llm-keys": "tsx scripts/encrypt-llm-keys.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.72.1",
|
||||
"@mosaic/shared": "workspace:*",
|
||||
"@mosaicstack/telemetry-client": "^0.1.1",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"@nestjs/common": "^11.1.12",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.12",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/platform-express": "^11.1.12",
|
||||
"@nestjs/platform-socket.io": "^11.1.12",
|
||||
"@nestjs/schedule": "^6.1.1",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@nestjs/websockets": "^11.1.12",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.55.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.44.0",
|
||||
"@opentelemetry/resources": "^1.30.1",
|
||||
"@opentelemetry/sdk-node": "^0.56.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.5.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.28.0",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"@types/marked": "^6.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.13.5",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-auth": "^1.4.17",
|
||||
"bullmq": "^5.67.2",
|
||||
"chokidar": "^4.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"discord.js": "^14.25.1",
|
||||
"dockerode": "^4.0.9",
|
||||
"gray-matter": "^4.0.3",
|
||||
"helmet": "^8.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ioredis": "^5.9.2",
|
||||
"jose": "^6.1.3",
|
||||
"marked": "^17.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.3",
|
||||
"marked-highlight": "^2.2.3",
|
||||
"matrix-bot-sdk": "^0.8.0",
|
||||
"node-pty": "^1.0.0",
|
||||
"ollama": "^0.6.3",
|
||||
"openai": "^6.17.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"slugify": "^1.6.6",
|
||||
"socket.io": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "^1.4.17",
|
||||
"@mosaic/config": "workspace:*",
|
||||
"@nestjs/cli": "^11.0.6",
|
||||
"@nestjs/schematics": "^11.0.1",
|
||||
"@nestjs/testing": "^11.1.12",
|
||||
"@opentelemetry/context-async-hooks": "^2.5.0",
|
||||
"@swc/core": "^1.10.18",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/dockerode": "^3.3.47",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/highlight.js": "^10.1.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"dotenv": "^17.2.4",
|
||||
"express": "^5.2.1",
|
||||
"prisma": "^6.19.2",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.8.2",
|
||||
"unplugin-swc": "^1.5.2",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
migrations: {
|
||||
seed: "tsx prisma/seed.ts",
|
||||
},
|
||||
});
|
||||
@@ -1,261 +0,0 @@
|
||||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS "vector";
|
||||
|
||||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TaskStatus" AS ENUM ('NOT_STARTED', 'IN_PROGRESS', 'PAUSED', 'COMPLETED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ProjectStatus" AS ENUM ('PLANNING', 'ACTIVE', 'PAUSED', 'COMPLETED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkspaceMemberRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'GUEST');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ActivityAction" AS ENUM ('CREATED', 'UPDATED', 'DELETED', 'COMPLETED', 'ASSIGNED', 'COMMENTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EntityType" AS ENUM ('TASK', 'EVENT', 'PROJECT', 'WORKSPACE', 'USER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" UUID NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"auth_provider_id" TEXT,
|
||||
"preferences" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workspaces" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"owner_id" UUID NOT NULL,
|
||||
"settings" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workspace_members" (
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"role" "WorkspaceMemberRole" NOT NULL DEFAULT 'MEMBER',
|
||||
"joined_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "workspace_members_pkey" PRIMARY KEY ("workspace_id","user_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tasks" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"status" "TaskStatus" NOT NULL DEFAULT 'NOT_STARTED',
|
||||
"priority" "TaskPriority" NOT NULL DEFAULT 'MEDIUM',
|
||||
"due_date" TIMESTAMPTZ,
|
||||
"assignee_id" UUID,
|
||||
"creator_id" UUID NOT NULL,
|
||||
"project_id" UUID,
|
||||
"parent_id" UUID,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "events" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"start_time" TIMESTAMPTZ NOT NULL,
|
||||
"end_time" TIMESTAMPTZ,
|
||||
"all_day" BOOLEAN NOT NULL DEFAULT false,
|
||||
"location" TEXT,
|
||||
"recurrence" JSONB,
|
||||
"creator_id" UUID NOT NULL,
|
||||
"project_id" UUID,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "projects" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"status" "ProjectStatus" NOT NULL DEFAULT 'PLANNING',
|
||||
"start_date" DATE,
|
||||
"end_date" DATE,
|
||||
"creator_id" UUID NOT NULL,
|
||||
"color" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "activity_logs" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"action" "ActivityAction" NOT NULL,
|
||||
"entity_type" "EntityType" NOT NULL,
|
||||
"entity_id" UUID NOT NULL,
|
||||
"details" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "activity_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "memory_embeddings" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"embedding" vector(1536),
|
||||
"entity_type" "EntityType",
|
||||
"entity_id" UUID,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "memory_embeddings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_auth_provider_id_key" ON "users"("auth_provider_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workspaces_owner_id_idx" ON "workspaces"("owner_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workspace_members_user_id_idx" ON "workspace_members"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_workspace_id_idx" ON "tasks"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_workspace_id_status_idx" ON "tasks"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_workspace_id_due_date_idx" ON "tasks"("workspace_id", "due_date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_assignee_id_idx" ON "tasks"("assignee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_project_id_idx" ON "tasks"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_parent_id_idx" ON "tasks"("parent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_workspace_id_idx" ON "events"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_workspace_id_start_time_idx" ON "events"("workspace_id", "start_time");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_creator_id_idx" ON "events"("creator_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_project_id_idx" ON "events"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_workspace_id_idx" ON "projects"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_workspace_id_status_idx" ON "projects"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_creator_id_idx" ON "projects"("creator_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_workspace_id_idx" ON "activity_logs"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_workspace_id_created_at_idx" ON "activity_logs"("workspace_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_entity_type_entity_id_idx" ON "activity_logs"("entity_type", "entity_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_user_id_idx" ON "activity_logs"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "memory_embeddings_workspace_id_idx" ON "memory_embeddings"("workspace_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_assignee_id_fkey" FOREIGN KEY ("assignee_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "memory_embeddings" ADD CONSTRAINT "memory_embeddings_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Add HNSW index for fast vector similarity search on memory_embeddings table
|
||||
-- Using cosine distance operator for semantic similarity
|
||||
-- Parameters: m=16 (max connections per layer), ef_construction=64 (build quality)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS memory_embeddings_embedding_idx
|
||||
ON memory_embeddings
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (m = 16, ef_construction = 64);
|
||||
@@ -1,92 +0,0 @@
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'LOGIN';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'LOGOUT';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'PASSWORD_RESET';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'EMAIL_VERIFIED';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "activity_logs" ADD COLUMN "ip_address" TEXT,
|
||||
ADD COLUMN "user_agent" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "email_verified" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "image" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sessions" (
|
||||
"id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"expires_at" TIMESTAMPTZ NOT NULL,
|
||||
"ip_address" TEXT,
|
||||
"user_agent" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "accounts" (
|
||||
"id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"account_id" TEXT NOT NULL,
|
||||
"provider_id" TEXT NOT NULL,
|
||||
"access_token" TEXT,
|
||||
"refresh_token" TEXT,
|
||||
"id_token" TEXT,
|
||||
"access_token_expires_at" TIMESTAMPTZ,
|
||||
"refresh_token_expires_at" TIMESTAMPTZ,
|
||||
"scope" TEXT,
|
||||
"password" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "verifications" (
|
||||
"id" UUID NOT NULL,
|
||||
"identifier" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"expires_at" TIMESTAMPTZ NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "verifications_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sessions_token_key" ON "sessions"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sessions_user_id_idx" ON "sessions"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sessions_token_idx" ON "sessions"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "accounts_user_id_idx" ON "accounts"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "accounts_provider_id_account_id_key" ON "accounts"("provider_id", "account_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "verifications_identifier_idx" ON "verifications"("identifier");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_action_idx" ON "activity_logs"("action");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-286
@@ -1,286 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IdeaStatus" AS ENUM ('CAPTURED', 'PROCESSING', 'ACTIONABLE', 'ARCHIVED', 'DISCARDED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RelationshipType" AS ENUM ('BLOCKS', 'BLOCKED_BY', 'DEPENDS_ON', 'PARENT_OF', 'CHILD_OF', 'RELATED_TO', 'DUPLICATE_OF', 'SUPERSEDES', 'PART_OF');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AgentStatus" AS ENUM ('IDLE', 'WORKING', 'WAITING', 'ERROR', 'TERMINATED');
|
||||
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "EntityType" ADD VALUE 'IDEA';
|
||||
ALTER TYPE "EntityType" ADD VALUE 'DOMAIN';
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "memory_embeddings_embedding_idx";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "events" ADD COLUMN "domain_id" UUID;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "domain_id" UUID;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "tasks" ADD COLUMN "domain_id" UUID;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "domains" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"color" TEXT,
|
||||
"icon" TEXT,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "domains_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ideas" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"domain_id" UUID,
|
||||
"project_id" UUID,
|
||||
"title" TEXT,
|
||||
"content" TEXT NOT NULL,
|
||||
"status" "IdeaStatus" NOT NULL DEFAULT 'CAPTURED',
|
||||
"priority" "TaskPriority" NOT NULL DEFAULT 'MEDIUM',
|
||||
"category" TEXT,
|
||||
"tags" TEXT[],
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"embedding" vector(1536),
|
||||
"creator_id" UUID NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "ideas_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "relationships" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"source_type" "EntityType" NOT NULL,
|
||||
"source_id" UUID NOT NULL,
|
||||
"target_type" "EntityType" NOT NULL,
|
||||
"target_id" UUID NOT NULL,
|
||||
"relationship" "RelationshipType" NOT NULL,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"notes" TEXT,
|
||||
"creator_id" UUID NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "relationships_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agents" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"model" TEXT,
|
||||
"role" TEXT,
|
||||
"status" "AgentStatus" NOT NULL DEFAULT 'IDLE',
|
||||
"current_task" TEXT,
|
||||
"metrics" JSONB NOT NULL DEFAULT '{"totalTasks": 0, "successfulTasks": 0, "failedTasks": 0, "avgResponseTimeMs": 0}',
|
||||
"last_heartbeat" TIMESTAMPTZ,
|
||||
"error_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"last_error" TEXT,
|
||||
"fired_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"fire_history" JSONB NOT NULL DEFAULT '[]',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"terminated_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "agents_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_sessions" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"agent_id" UUID,
|
||||
"session_key" TEXT NOT NULL,
|
||||
"label" TEXT,
|
||||
"channel" TEXT,
|
||||
"context_summary" TEXT,
|
||||
"message_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"started_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_message_at" TIMESTAMPTZ,
|
||||
"ended_at" TIMESTAMPTZ,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
|
||||
CONSTRAINT "agent_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "widget_definitions" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"display_name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"component" TEXT NOT NULL,
|
||||
"default_width" INTEGER NOT NULL DEFAULT 1,
|
||||
"default_height" INTEGER NOT NULL DEFAULT 1,
|
||||
"min_width" INTEGER NOT NULL DEFAULT 1,
|
||||
"min_height" INTEGER NOT NULL DEFAULT 1,
|
||||
"max_width" INTEGER,
|
||||
"max_height" INTEGER,
|
||||
"config_schema" JSONB NOT NULL DEFAULT '{}',
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "widget_definitions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_layouts" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"layout" JSONB NOT NULL DEFAULT '[]',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "user_layouts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "domains_workspace_id_idx" ON "domains"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "domains_workspace_id_slug_key" ON "domains"("workspace_id", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_workspace_id_idx" ON "ideas"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_workspace_id_status_idx" ON "ideas"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_domain_id_idx" ON "ideas"("domain_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_project_id_idx" ON "ideas"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_creator_id_idx" ON "ideas"("creator_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "relationships_source_type_source_id_idx" ON "relationships"("source_type", "source_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "relationships_target_type_target_id_idx" ON "relationships"("target_type", "target_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "relationships_relationship_idx" ON "relationships"("relationship");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "relationships_workspace_id_source_type_source_id_target_typ_key" ON "relationships"("workspace_id", "source_type", "source_id", "target_type", "target_id", "relationship");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agents_workspace_id_idx" ON "agents"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agents_status_idx" ON "agents"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agents_workspace_id_agent_id_key" ON "agents"("workspace_id", "agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_sessions_workspace_id_idx" ON "agent_sessions"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_sessions_user_id_idx" ON "agent_sessions"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_sessions_agent_id_idx" ON "agent_sessions"("agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_sessions_is_active_idx" ON "agent_sessions"("is_active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_sessions_workspace_id_session_key_key" ON "agent_sessions"("workspace_id", "session_key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "widget_definitions_name_key" ON "widget_definitions"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_layouts_user_id_idx" ON "user_layouts"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_layouts_workspace_id_user_id_name_key" ON "user_layouts"("workspace_id", "user_id", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_domain_id_idx" ON "events"("domain_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_domain_id_idx" ON "projects"("domain_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_domain_id_idx" ON "tasks"("domain_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_domain_id_fkey" FOREIGN KEY ("domain_id") REFERENCES "domains"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_domain_id_fkey" FOREIGN KEY ("domain_id") REFERENCES "domains"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_domain_id_fkey" FOREIGN KEY ("domain_id") REFERENCES "domains"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "domains" ADD CONSTRAINT "domains_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_domain_id_fkey" FOREIGN KEY ("domain_id") REFERENCES "domains"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "relationships" ADD CONSTRAINT "relationships_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "relationships" ADD CONSTRAINT "relationships_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agents" ADD CONSTRAINT "agents_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_sessions" ADD CONSTRAINT "agent_sessions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_sessions" ADD CONSTRAINT "agent_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_sessions" ADD CONSTRAINT "agent_sessions_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_layouts" ADD CONSTRAINT "user_layouts_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_layouts" ADD CONSTRAINT "user_layouts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,158 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EntryStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'WORKSPACE', 'PUBLIC');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "user_layouts" ADD COLUMN "metadata" JSONB NOT NULL DEFAULT '{}';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_entries" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"content_html" TEXT,
|
||||
"summary" TEXT,
|
||||
"status" "EntryStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"created_by" UUID NOT NULL,
|
||||
"updated_by" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "knowledge_entries_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_entry_versions" (
|
||||
"id" UUID NOT NULL,
|
||||
"entry_id" UUID NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"summary" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID NOT NULL,
|
||||
"change_note" TEXT,
|
||||
|
||||
CONSTRAINT "knowledge_entry_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_links" (
|
||||
"id" UUID NOT NULL,
|
||||
"source_id" UUID NOT NULL,
|
||||
"target_id" UUID NOT NULL,
|
||||
"link_text" TEXT NOT NULL,
|
||||
"context" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "knowledge_links_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_tags" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"color" TEXT,
|
||||
"description" TEXT,
|
||||
|
||||
CONSTRAINT "knowledge_tags_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_entry_tags" (
|
||||
"entry_id" UUID NOT NULL,
|
||||
"tag_id" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "knowledge_entry_tags_pkey" PRIMARY KEY ("entry_id","tag_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_embeddings" (
|
||||
"id" UUID NOT NULL,
|
||||
"entry_id" UUID NOT NULL,
|
||||
"embedding" vector(1536) NOT NULL,
|
||||
"model" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "knowledge_embeddings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_workspace_id_status_idx" ON "knowledge_entries"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_workspace_id_updated_at_idx" ON "knowledge_entries"("workspace_id", "updated_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_created_by_idx" ON "knowledge_entries"("created_by");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_updated_by_idx" ON "knowledge_entries"("updated_by");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_entries_workspace_id_slug_key" ON "knowledge_entries"("workspace_id", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entry_versions_entry_id_version_idx" ON "knowledge_entry_versions"("entry_id", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_entry_versions_entry_id_version_key" ON "knowledge_entry_versions"("entry_id", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_links_source_id_idx" ON "knowledge_links"("source_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_links_target_id_idx" ON "knowledge_links"("target_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_links_source_id_target_id_key" ON "knowledge_links"("source_id", "target_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_tags_workspace_id_idx" ON "knowledge_tags"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_tags_workspace_id_slug_key" ON "knowledge_tags"("workspace_id", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entry_tags_entry_id_idx" ON "knowledge_entry_tags"("entry_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entry_tags_tag_id_idx" ON "knowledge_entry_tags"("tag_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_embeddings_entry_id_key" ON "knowledge_embeddings"("entry_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_embeddings_entry_id_idx" ON "knowledge_embeddings"("entry_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entries" ADD CONSTRAINT "knowledge_entries_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entry_versions" ADD CONSTRAINT "knowledge_entry_versions_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_links" ADD CONSTRAINT "knowledge_links_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_links" ADD CONSTRAINT "knowledge_links_target_id_fkey" FOREIGN KEY ("target_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_tags" ADD CONSTRAINT "knowledge_tags_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entry_tags" ADD CONSTRAINT "knowledge_entry_tags_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entry_tags" ADD CONSTRAINT "knowledge_entry_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "knowledge_tags"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_embeddings" ADD CONSTRAINT "knowledge_embeddings_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,40 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TeamMemberRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "teams" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "teams_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "team_members" (
|
||||
"team_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"role" "TeamMemberRole" NOT NULL DEFAULT 'MEMBER',
|
||||
"joined_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "team_members_pkey" PRIMARY KEY ("team_id","user_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "teams_workspace_id_idx" ON "teams"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "team_members_user_id_idx" ON "team_members"("user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "teams" ADD CONSTRAINT "teams_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,319 +0,0 @@
|
||||
-- Row-Level Security (RLS) for Multi-Tenant Isolation
|
||||
-- This migration enables RLS on all tenant-scoped tables and creates policies
|
||||
-- to ensure users can only access data within their authorized workspaces.
|
||||
|
||||
-- =============================================================================
|
||||
-- ENABLE RLS ON TENANT-SCOPED TABLES
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE workspaces ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE workspace_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE teams ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE activity_logs ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE memory_embeddings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE domains ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE ideas ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE relationships ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE agents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE agent_sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE user_layouts ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_entries ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_tags ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_entry_tags ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_links ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_embeddings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_entry_versions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- HELPER FUNCTION: Check if user is workspace member
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_member(workspace_uuid UUID, user_uuid UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM workspace_members
|
||||
WHERE workspace_id = workspace_uuid
|
||||
AND user_id = user_uuid
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- =============================================================================
|
||||
-- HELPER FUNCTION: Check if user is workspace owner/admin
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_admin(workspace_uuid UUID, user_uuid UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM workspace_members
|
||||
WHERE workspace_id = workspace_uuid
|
||||
AND user_id = user_uuid
|
||||
AND role IN ('OWNER', 'ADMIN')
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- =============================================================================
|
||||
-- HELPER FUNCTION: Get current user ID from session variable
|
||||
-- =============================================================================
|
||||
-- Usage in API: SET LOCAL app.current_user_id = 'user-uuid';
|
||||
|
||||
CREATE OR REPLACE FUNCTION current_user_id()
|
||||
RETURNS UUID AS $$
|
||||
BEGIN
|
||||
RETURN NULLIF(current_setting('app.current_user_id', TRUE), '')::UUID;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- =============================================================================
|
||||
-- WORKSPACES: Users can only see workspaces they're members of
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY workspace_member_access ON workspaces
|
||||
FOR ALL
|
||||
USING (
|
||||
id IN (
|
||||
SELECT workspace_id FROM workspace_members
|
||||
WHERE user_id = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- WORKSPACE_MEMBERS: Users can see members of their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY workspace_members_access ON workspace_members
|
||||
FOR ALL
|
||||
USING (
|
||||
workspace_id IN (
|
||||
SELECT workspace_id FROM workspace_members
|
||||
WHERE user_id = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- TEAMS: Users can see teams in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY teams_workspace_access ON teams
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- TEAM_MEMBERS: Users can see team members in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY team_members_access ON team_members
|
||||
FOR ALL
|
||||
USING (
|
||||
team_id IN (
|
||||
SELECT id FROM teams
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- TASKS: Users can only see tasks in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY tasks_workspace_access ON tasks
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- EVENTS: Users can only see events in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY events_workspace_access ON events
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- PROJECTS: Users can only see projects in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY projects_workspace_access ON projects
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- ACTIVITY_LOGS: Users can only see activity in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY activity_logs_workspace_access ON activity_logs
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- MEMORY_EMBEDDINGS: Users can only see embeddings in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY memory_embeddings_workspace_access ON memory_embeddings
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- DOMAINS: Users can only see domains in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY domains_workspace_access ON domains
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- IDEAS: Users can only see ideas in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY ideas_workspace_access ON ideas
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- RELATIONSHIPS: Users can only see relationships in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY relationships_workspace_access ON relationships
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- AGENTS: Users can only see agents in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY agents_workspace_access ON agents
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- AGENT_SESSIONS: Users can only see agent sessions in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY agent_sessions_workspace_access ON agent_sessions
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- USER_LAYOUTS: Users can only see their own layouts in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY user_layouts_workspace_access ON user_layouts
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
AND user_id = current_user_id()
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_ENTRIES: Users can only see entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_entries_workspace_access ON knowledge_entries
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_TAGS: Users can only see tags in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_tags_workspace_access ON knowledge_tags
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_ENTRY_TAGS: Users can see tags for entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_entry_tags_access ON knowledge_entry_tags
|
||||
FOR ALL
|
||||
USING (
|
||||
entry_id IN (
|
||||
SELECT id FROM knowledge_entries
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_LINKS: Users can see links between entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_links_access ON knowledge_links
|
||||
FOR ALL
|
||||
USING (
|
||||
source_id IN (
|
||||
SELECT id FROM knowledge_entries
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_EMBEDDINGS: Users can see embeddings for entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_embeddings_access ON knowledge_embeddings
|
||||
FOR ALL
|
||||
USING (
|
||||
entry_id IN (
|
||||
SELECT id FROM knowledge_entries
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_ENTRY_VERSIONS: Users can see versions for entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_entry_versions_access ON knowledge_entry_versions
|
||||
FOR ALL
|
||||
USING (
|
||||
entry_id IN (
|
||||
SELECT id FROM knowledge_entries
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- GRANT USAGE TO APPLICATION ROLE
|
||||
-- =============================================================================
|
||||
-- The application should connect with a role that has appropriate permissions.
|
||||
-- By default, we assume the owner of the database has full access.
|
||||
-- In production, create a dedicated role with limited permissions.
|
||||
|
||||
-- Example (uncomment and customize for production):
|
||||
-- GRANT USAGE ON SCHEMA public TO mosaic_app;
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO mosaic_app;
|
||||
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO mosaic_app;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_preferences" (
|
||||
"id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"theme" TEXT NOT NULL DEFAULT 'system',
|
||||
"locale" TEXT NOT NULL DEFAULT 'en',
|
||||
"timezone" TEXT,
|
||||
"settings" JSONB NOT NULL DEFAULT '{}',
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "user_preferences_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_preferences_user_id_key" ON "user_preferences"("user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,47 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AgentTaskStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AgentTaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_tasks" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"status" "AgentTaskStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"priority" "AgentTaskPriority" NOT NULL DEFAULT 'MEDIUM',
|
||||
"agent_type" TEXT NOT NULL,
|
||||
"agent_config" JSONB NOT NULL DEFAULT '{}',
|
||||
"result" JSONB,
|
||||
"error" TEXT,
|
||||
"created_by_id" UUID NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"started_at" TIMESTAMPTZ,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "agent_tasks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_tasks_workspace_id_idx" ON "agent_tasks"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_tasks_workspace_id_status_idx" ON "agent_tasks"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_tasks_workspace_id_priority_idx" ON "agent_tasks"("workspace_id", "priority");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_tasks_created_by_id_idx" ON "agent_tasks"("created_by_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_tasks_id_workspace_id_key" ON "agent_tasks"("id", "workspace_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_tasks" ADD CONSTRAINT "agent_tasks_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_tasks" ADD CONSTRAINT "agent_tasks_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,31 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FormalityLevel" AS ENUM ('VERY_CASUAL', 'CASUAL', 'NEUTRAL', 'FORMAL', 'VERY_FORMAL');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "personalities" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"tone" TEXT NOT NULL,
|
||||
"formality_level" "FormalityLevel" NOT NULL DEFAULT 'NEUTRAL',
|
||||
"system_prompt_template" TEXT NOT NULL,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "personalities_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "personalities_workspace_id_idx" ON "personalities"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "personalities_workspace_id_is_default_idx" ON "personalities"("workspace_id", "is_default");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "personalities_workspace_id_name_key" ON "personalities"("workspace_id", "name");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "personalities" ADD CONSTRAINT "personalities_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `personalities` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `display_text` to the `knowledge_links` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `position_end` to the `knowledge_links` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `position_start` to the `knowledge_links` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "personalities" DROP CONSTRAINT "personalities_workspace_id_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "knowledge_links_source_id_target_id_key";
|
||||
|
||||
-- AlterTable: Add new columns with temporary defaults for existing records
|
||||
ALTER TABLE "knowledge_links"
|
||||
ADD COLUMN "display_text" TEXT DEFAULT '',
|
||||
ADD COLUMN "position_end" INTEGER DEFAULT 0,
|
||||
ADD COLUMN "position_start" INTEGER DEFAULT 0,
|
||||
ADD COLUMN "resolved" BOOLEAN NOT NULL DEFAULT false,
|
||||
ALTER COLUMN "target_id" DROP NOT NULL;
|
||||
|
||||
-- Update existing records: set display_text to link_text and resolved to true if target exists
|
||||
UPDATE "knowledge_links" SET "display_text" = "link_text" WHERE "display_text" = '';
|
||||
UPDATE "knowledge_links" SET "resolved" = true WHERE "target_id" IS NOT NULL;
|
||||
|
||||
-- Remove defaults for new records
|
||||
ALTER TABLE "knowledge_links"
|
||||
ALTER COLUMN "display_text" DROP DEFAULT,
|
||||
ALTER COLUMN "position_end" DROP DEFAULT,
|
||||
ALTER COLUMN "position_start" DROP DEFAULT;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "personalities";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "FormalityLevel";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_links_source_id_resolved_idx" ON "knowledge_links"("source_id", "resolved");
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
-- Add HNSW index for fast vector similarity search on knowledge_embeddings table
|
||||
-- Using cosine distance operator for semantic similarity
|
||||
-- Parameters: m=16 (max connections per layer), ef_construction=64 (build quality)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS knowledge_embeddings_embedding_idx
|
||||
ON knowledge_embeddings
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (m = 16, ef_construction = 64);
|
||||
@@ -1,29 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "llm_provider_instances" (
|
||||
"id" UUID NOT NULL,
|
||||
"provider_type" TEXT NOT NULL,
|
||||
"display_name" TEXT NOT NULL,
|
||||
"user_id" UUID,
|
||||
"config" JSONB NOT NULL,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"is_enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "llm_provider_instances_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_provider_instances_user_id_idx" ON "llm_provider_instances"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_provider_instances_provider_type_idx" ON "llm_provider_instances"("provider_type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_provider_instances_is_default_idx" ON "llm_provider_instances"("is_default");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_provider_instances_is_enabled_idx" ON "llm_provider_instances"("is_enabled");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "llm_provider_instances" ADD CONSTRAINT "llm_provider_instances_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,112 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RunnerJobStatus" AS ENUM ('PENDING', 'QUEUED', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobStepPhase" AS ENUM ('SETUP', 'EXECUTION', 'VALIDATION', 'CLEANUP');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobStepType" AS ENUM ('COMMAND', 'AI_ACTION', 'GATE', 'ARTIFACT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobStepStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "runner_jobs" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_task_id" UUID,
|
||||
"type" TEXT NOT NULL,
|
||||
"status" "RunnerJobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"priority" INTEGER NOT NULL,
|
||||
"progress_percent" INTEGER NOT NULL DEFAULT 0,
|
||||
"result" JSONB,
|
||||
"error" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"started_at" TIMESTAMPTZ,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "runner_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "job_steps" (
|
||||
"id" UUID NOT NULL,
|
||||
"job_id" UUID NOT NULL,
|
||||
"ordinal" INTEGER NOT NULL,
|
||||
"phase" "JobStepPhase" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "JobStepType" NOT NULL,
|
||||
"status" "JobStepStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"output" TEXT,
|
||||
"tokens_input" INTEGER,
|
||||
"tokens_output" INTEGER,
|
||||
"started_at" TIMESTAMPTZ,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
"duration_ms" INTEGER,
|
||||
|
||||
CONSTRAINT "job_steps_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "job_events" (
|
||||
"id" UUID NOT NULL,
|
||||
"job_id" UUID NOT NULL,
|
||||
"step_id" UUID,
|
||||
"type" TEXT NOT NULL,
|
||||
"timestamp" TIMESTAMPTZ NOT NULL,
|
||||
"actor" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "job_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "runner_jobs_id_workspace_id_key" ON "runner_jobs"("id", "workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runner_jobs_workspace_id_idx" ON "runner_jobs"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runner_jobs_workspace_id_status_idx" ON "runner_jobs"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runner_jobs_agent_task_id_idx" ON "runner_jobs"("agent_task_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runner_jobs_priority_idx" ON "runner_jobs"("priority");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_steps_job_id_idx" ON "job_steps"("job_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_steps_job_id_ordinal_idx" ON "job_steps"("job_id", "ordinal");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_steps_status_idx" ON "job_steps"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_job_id_idx" ON "job_events"("job_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_step_id_idx" ON "job_events"("step_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_timestamp_idx" ON "job_events"("timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_type_idx" ON "job_events"("type");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "runner_jobs" ADD CONSTRAINT "runner_jobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "runner_jobs" ADD CONSTRAINT "runner_jobs_agent_task_id_fkey" FOREIGN KEY ("agent_task_id") REFERENCES "agent_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "job_steps" ADD CONSTRAINT "job_steps_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "runner_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "job_events" ADD CONSTRAINT "job_events_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "runner_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "job_events" ADD CONSTRAINT "job_events_step_id_fkey" FOREIGN KEY ("step_id") REFERENCES "job_steps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_job_id_timestamp_idx" ON "job_events"("job_id", "timestamp");
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
-- Add tsvector column for full-text search on knowledge_entries
|
||||
-- Weighted fields: title (A), summary (B), content (C)
|
||||
|
||||
-- Step 1: Add the search_vector column
|
||||
ALTER TABLE "knowledge_entries"
|
||||
ADD COLUMN "search_vector" tsvector;
|
||||
|
||||
-- Step 2: Create GIN index for fast full-text search
|
||||
CREATE INDEX "knowledge_entries_search_vector_idx"
|
||||
ON "knowledge_entries"
|
||||
USING gin("search_vector");
|
||||
|
||||
-- Step 3: Create function to update search_vector
|
||||
CREATE OR REPLACE FUNCTION knowledge_entries_search_vector_update()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.search_vector :=
|
||||
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.summary, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.content, '')), 'C');
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 4: Create trigger to automatically update search_vector on insert/update
|
||||
CREATE TRIGGER knowledge_entries_search_vector_trigger
|
||||
BEFORE INSERT OR UPDATE ON "knowledge_entries"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION knowledge_entries_search_vector_update();
|
||||
|
||||
-- Step 5: Populate search_vector for existing entries
|
||||
UPDATE "knowledge_entries"
|
||||
SET search_vector =
|
||||
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(summary, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(content, '')), 'C');
|
||||
@@ -1,118 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FederationConnectionStatus" AS ENUM ('PENDING', 'ACTIVE', 'SUSPENDED', 'DISCONNECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FederationMessageType" AS ENUM ('QUERY', 'COMMAND', 'EVENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FederationMessageStatus" AS ENUM ('PENDING', 'DELIVERED', 'FAILED', 'TIMEOUT');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "federation_connections" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"remote_instance_id" TEXT NOT NULL,
|
||||
"remote_url" TEXT NOT NULL,
|
||||
"remote_public_key" TEXT NOT NULL,
|
||||
"remote_capabilities" JSONB NOT NULL DEFAULT '{}',
|
||||
"status" "FederationConnectionStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"connected_at" TIMESTAMPTZ,
|
||||
"disconnected_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "federation_connections_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "federated_identities" (
|
||||
"id" UUID NOT NULL,
|
||||
"local_user_id" UUID NOT NULL,
|
||||
"remote_user_id" TEXT NOT NULL,
|
||||
"remote_instance_id" TEXT NOT NULL,
|
||||
"oidc_subject" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "federated_identities_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "federation_messages" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"connection_id" UUID NOT NULL,
|
||||
"message_type" "FederationMessageType" NOT NULL,
|
||||
"message_id" TEXT NOT NULL,
|
||||
"correlation_id" TEXT,
|
||||
"query" TEXT,
|
||||
"command_type" TEXT,
|
||||
"event_type" TEXT,
|
||||
"payload" JSONB DEFAULT '{}',
|
||||
"response" JSONB DEFAULT '{}',
|
||||
"status" "FederationMessageStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"error" TEXT,
|
||||
"signature" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"delivered_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "federation_messages_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "federation_connections_workspace_id_remote_instance_id_key" ON "federation_connections"("workspace_id", "remote_instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_connections_workspace_id_idx" ON "federation_connections"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_connections_workspace_id_status_idx" ON "federation_connections"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_connections_remote_instance_id_idx" ON "federation_connections"("remote_instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "federated_identities_local_user_id_remote_instance_id_key" ON "federated_identities"("local_user_id", "remote_instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federated_identities_local_user_id_idx" ON "federated_identities"("local_user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federated_identities_remote_instance_id_idx" ON "federated_identities"("remote_instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federated_identities_oidc_subject_idx" ON "federated_identities"("oidc_subject");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "federation_messages_message_id_key" ON "federation_messages"("message_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_workspace_id_idx" ON "federation_messages"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_connection_id_idx" ON "federation_messages"("connection_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_message_id_idx" ON "federation_messages"("message_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_correlation_id_idx" ON "federation_messages"("correlation_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_event_type_idx" ON "federation_messages"("event_type");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_connections" ADD CONSTRAINT "federation_connections_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federated_identities" ADD CONSTRAINT "federated_identities_local_user_id_fkey" FOREIGN KEY ("local_user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_messages" ADD CONSTRAINT "federation_messages_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "federation_connections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_messages" ADD CONSTRAINT "federation_messages_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- Add version field for optimistic locking to prevent race conditions
|
||||
-- This allows safe concurrent updates to runner job status
|
||||
|
||||
ALTER TABLE "runner_jobs" ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- Create index for better performance on version checks
|
||||
CREATE INDEX "runner_jobs_version_idx" ON "runner_jobs"("version");
|
||||
@@ -1,34 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "federation_event_subscriptions" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"connection_id" UUID NOT NULL,
|
||||
"event_type" TEXT NOT NULL,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "federation_event_subscriptions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_event_subscriptions_workspace_id_idx" ON "federation_event_subscriptions"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_event_subscriptions_connection_id_idx" ON "federation_event_subscriptions"("connection_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_event_subscriptions_event_type_idx" ON "federation_event_subscriptions"("event_type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_event_subscriptions_workspace_id_is_active_idx" ON "federation_event_subscriptions"("workspace_id", "is_active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "federation_event_subscriptions_workspace_id_connection_id_even_key" ON "federation_event_subscriptions"("workspace_id", "connection_id", "event_type");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_event_subscriptions" ADD CONSTRAINT "federation_event_subscriptions_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "federation_connections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_event_subscriptions" ADD CONSTRAINT "federation_event_subscriptions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
-- Rollback: SQL Injection Hardening for is_workspace_admin() Helper Function
|
||||
-- This reverts the function to its previous implementation
|
||||
|
||||
-- =============================================================================
|
||||
-- REVERT is_workspace_admin() to original implementation
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_admin(workspace_uuid UUID, user_uuid UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM workspace_members
|
||||
WHERE workspace_id = workspace_uuid
|
||||
AND user_id = user_uuid
|
||||
AND role IN ('OWNER', 'ADMIN')
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
-- Security Fix: SQL Injection Hardening for is_workspace_admin() Helper Function
|
||||
-- This migration adds explicit UUID validation to prevent SQL injection attacks
|
||||
--
|
||||
-- Related: #355 Code Review - Security CRIT-3
|
||||
-- Original issue: Migration 20260129221004_add_rls_policies
|
||||
|
||||
-- =============================================================================
|
||||
-- SECURITY FIX: Add explicit UUID validation to is_workspace_admin()
|
||||
-- =============================================================================
|
||||
-- The is_workspace_admin() function previously accepted UUID parameters without
|
||||
-- explicit type casting/validation. Although PostgreSQL's parameter binding provides
|
||||
-- some protection, explicit UUID type validation is a security best practice.
|
||||
--
|
||||
-- This fix adds explicit UUID validation using PostgreSQL's uuid type checking
|
||||
-- to ensure that non-UUID values cannot bypass the function's intent.
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_admin(workspace_uuid UUID, user_uuid UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
-- Validate input parameters are valid UUIDs
|
||||
v_workspace_id UUID;
|
||||
v_user_id UUID;
|
||||
BEGIN
|
||||
-- Explicitly validate workspace_uuid parameter
|
||||
IF workspace_uuid IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
v_workspace_id := workspace_uuid::UUID;
|
||||
|
||||
-- Explicitly validate user_uuid parameter
|
||||
IF user_uuid IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
v_user_id := user_uuid::UUID;
|
||||
|
||||
-- Query with validated parameters
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM workspace_members
|
||||
WHERE workspace_id = v_workspace_id
|
||||
AND user_id = v_user_id
|
||||
AND role IN ('OWNER', 'ADMIN')
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- =============================================================================
|
||||
-- NOTES
|
||||
-- =============================================================================
|
||||
-- This is a hardening fix that adds defense-in-depth to the is_workspace_admin()
|
||||
-- helper function. While PostgreSQL's parameterized queries already provide
|
||||
-- protection against SQL injection, explicit UUID type validation ensures:
|
||||
--
|
||||
-- 1. Parameters are explicitly cast to UUID type
|
||||
-- 2. NULL values are handled defensively
|
||||
-- 3. The function's intent is clear and secure
|
||||
-- 4. Compliance with security best practices
|
||||
--
|
||||
-- This change is backward compatible and does not affect existing functionality.
|
||||
@@ -1,91 +0,0 @@
|
||||
-- Row-Level Security (RLS) for Auth Tables
|
||||
-- This migration adds FORCE ROW LEVEL SECURITY and policies to accounts and sessions tables
|
||||
-- to ensure users can only access their own authentication data.
|
||||
--
|
||||
-- Related: #350 - Add RLS policies to auth tables with FORCE enforcement
|
||||
-- Design: docs/design/credential-security.md (Phase 1a)
|
||||
|
||||
-- =============================================================================
|
||||
-- ENABLE FORCE RLS ON AUTH TABLES
|
||||
-- =============================================================================
|
||||
-- FORCE means the table owner (mosaic) is also subject to RLS policies.
|
||||
-- This prevents Prisma (connecting as owner) from bypassing policies.
|
||||
|
||||
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE accounts FORCE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE sessions FORCE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- ACCOUNTS TABLE POLICIES
|
||||
-- =============================================================================
|
||||
|
||||
-- Owner bypass policy: Allow access to all rows ONLY when no RLS context is set
|
||||
-- This is required for:
|
||||
-- 1. Prisma migrations that run without RLS context
|
||||
-- 2. BetterAuth internal operations during authentication flow (when no user context)
|
||||
-- 3. Database maintenance operations
|
||||
-- When RLS context IS set (current_user_id() returns non-NULL), this policy does not apply
|
||||
--
|
||||
-- NOTE: If connecting as a PostgreSQL superuser (like the default 'mosaic' role),
|
||||
-- RLS policies are bypassed entirely. For full RLS enforcement, the application
|
||||
-- should connect as a non-superuser role. See docs/design/credential-security.md
|
||||
CREATE POLICY accounts_owner_bypass ON accounts
|
||||
FOR ALL
|
||||
USING (current_user_id() IS NULL);
|
||||
|
||||
-- User access policy: Users can only access their own accounts
|
||||
-- Uses current_user_id() helper from migration 20260129221004_add_rls_policies
|
||||
-- This policy applies to all operations: SELECT, INSERT, UPDATE, DELETE
|
||||
CREATE POLICY accounts_user_access ON accounts
|
||||
FOR ALL
|
||||
USING (user_id = current_user_id());
|
||||
|
||||
-- =============================================================================
|
||||
-- SESSIONS TABLE POLICIES
|
||||
-- =============================================================================
|
||||
|
||||
-- Owner bypass policy: Allow access to all rows ONLY when no RLS context is set
|
||||
-- See note on accounts_owner_bypass policy about superuser limitations
|
||||
CREATE POLICY sessions_owner_bypass ON sessions
|
||||
FOR ALL
|
||||
USING (current_user_id() IS NULL);
|
||||
|
||||
-- User access policy: Users can only access their own sessions
|
||||
CREATE POLICY sessions_user_access ON sessions
|
||||
FOR ALL
|
||||
USING (user_id = current_user_id());
|
||||
|
||||
-- =============================================================================
|
||||
-- VERIFICATION TABLE ANALYSIS
|
||||
-- =============================================================================
|
||||
-- The verifications table does NOT need RLS policies because:
|
||||
-- 1. It stores ephemeral verification tokens (email verification, password reset)
|
||||
-- 2. It has no user_id column - only identifier (email) and value (token)
|
||||
-- 3. Tokens are short-lived and accessed by token value, not user context
|
||||
-- 4. BetterAuth manages access control through token validation, not RLS
|
||||
-- 5. No cross-user data leakage risk since tokens are random and expire
|
||||
--
|
||||
-- Therefore, we intentionally do NOT add RLS to verifications table.
|
||||
|
||||
-- =============================================================================
|
||||
-- IMPORTANT: SUPERUSER LIMITATION
|
||||
-- =============================================================================
|
||||
-- PostgreSQL superusers (including the default 'mosaic' role) ALWAYS bypass
|
||||
-- Row-Level Security policies, even with FORCE ROW LEVEL SECURITY enabled.
|
||||
-- This is a fundamental PostgreSQL security design.
|
||||
--
|
||||
-- For production deployments with full RLS enforcement, create a dedicated
|
||||
-- non-superuser application role:
|
||||
--
|
||||
-- CREATE ROLE mosaic_app WITH LOGIN PASSWORD 'secure-password';
|
||||
-- GRANT CONNECT ON DATABASE mosaic TO mosaic_app;
|
||||
-- GRANT USAGE ON SCHEMA public TO mosaic_app;
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO mosaic_app;
|
||||
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO mosaic_app;
|
||||
--
|
||||
-- Then update DATABASE_URL to connect as mosaic_app instead of mosaic.
|
||||
-- The RLS policies will then be properly enforced for application queries.
|
||||
--
|
||||
-- See: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
|
||||
@@ -1,76 +0,0 @@
|
||||
-- Rollback: User Credentials Storage with RLS Policies
|
||||
-- This migration reverses all changes from migration.sql
|
||||
--
|
||||
-- Related: #355 - Create UserCredential Prisma model with RLS policies
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP TRIGGERS AND FUNCTIONS
|
||||
-- =============================================================================
|
||||
|
||||
DROP TRIGGER IF EXISTS user_credentials_updated_at ON user_credentials;
|
||||
DROP FUNCTION IF EXISTS update_user_credentials_updated_at();
|
||||
|
||||
-- =============================================================================
|
||||
-- DISABLE RLS
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE user_credentials DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP RLS POLICIES
|
||||
-- =============================================================================
|
||||
|
||||
DROP POLICY IF EXISTS user_credentials_owner_bypass ON user_credentials;
|
||||
DROP POLICY IF EXISTS user_credentials_user_access ON user_credentials;
|
||||
DROP POLICY IF EXISTS user_credentials_workspace_access ON user_credentials;
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP INDEXES
|
||||
-- =============================================================================
|
||||
|
||||
DROP INDEX IF EXISTS "user_credentials_user_id_workspace_id_provider_name_key";
|
||||
DROP INDEX IF EXISTS "user_credentials_scope_is_active_idx";
|
||||
DROP INDEX IF EXISTS "user_credentials_workspace_id_scope_idx";
|
||||
DROP INDEX IF EXISTS "user_credentials_user_id_scope_idx";
|
||||
DROP INDEX IF EXISTS "user_credentials_workspace_id_idx";
|
||||
DROP INDEX IF EXISTS "user_credentials_user_id_idx";
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP FOREIGN KEY CONSTRAINTS
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE "user_credentials" DROP CONSTRAINT IF EXISTS "user_credentials_workspace_id_fkey";
|
||||
ALTER TABLE "user_credentials" DROP CONSTRAINT IF EXISTS "user_credentials_user_id_fkey";
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP TABLE
|
||||
-- =============================================================================
|
||||
|
||||
DROP TABLE IF EXISTS "user_credentials";
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP ENUMS
|
||||
-- =============================================================================
|
||||
-- NOTE: ENUM values cannot be easily removed from an existing enum type in PostgreSQL.
|
||||
-- To fully reverse this migration, you would need to:
|
||||
--
|
||||
-- 1. Remove the 'CREDENTIAL' value from EntityType enum (if not used elsewhere):
|
||||
-- ALTER TYPE "EntityType" RENAME TO "EntityType_old";
|
||||
-- CREATE TYPE "EntityType" AS ENUM (...all values except CREDENTIAL...);
|
||||
-- -- Then rebuild all dependent objects
|
||||
--
|
||||
-- 2. Remove credential-related actions from ActivityAction enum (if not used elsewhere):
|
||||
-- ALTER TYPE "ActivityAction" RENAME TO "ActivityAction_old";
|
||||
-- CREATE TYPE "ActivityAction" AS ENUM (...all values except CREDENTIAL_*...);
|
||||
-- -- Then rebuild all dependent objects
|
||||
--
|
||||
-- 3. Drop the CredentialType and CredentialScope enums:
|
||||
-- DROP TYPE IF EXISTS "CredentialType";
|
||||
-- DROP TYPE IF EXISTS "CredentialScope";
|
||||
--
|
||||
-- Due to the complexity and risk of breaking existing data/code that references
|
||||
-- these enum values, this migration does NOT automatically remove them.
|
||||
-- If you need to clean up the enums, manually execute the steps above.
|
||||
--
|
||||
-- For development environments, you can safely drop and recreate the enums manually
|
||||
-- using the SQL statements above.
|
||||
@@ -1,184 +0,0 @@
|
||||
-- User Credentials Storage with RLS Policies
|
||||
-- This migration adds the user_credentials table for secure storage of user API keys,
|
||||
-- OAuth tokens, and other credentials with encryption and RLS enforcement.
|
||||
--
|
||||
-- Related: #355 - Create UserCredential Prisma model with RLS policies
|
||||
-- Design: docs/design/credential-security.md (Phase 3a)
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE ENUMS
|
||||
-- =============================================================================
|
||||
|
||||
-- CredentialType enum: Types of credentials that can be stored
|
||||
CREATE TYPE "CredentialType" AS ENUM ('API_KEY', 'OAUTH_TOKEN', 'ACCESS_TOKEN', 'SECRET', 'PASSWORD', 'CUSTOM');
|
||||
|
||||
-- CredentialScope enum: Access scope for credentials
|
||||
CREATE TYPE "CredentialScope" AS ENUM ('USER', 'WORKSPACE', 'SYSTEM');
|
||||
|
||||
-- =============================================================================
|
||||
-- EXTEND EXISTING ENUMS
|
||||
-- =============================================================================
|
||||
|
||||
-- Add CREDENTIAL to EntityType for activity logging
|
||||
ALTER TYPE "EntityType" ADD VALUE 'CREDENTIAL';
|
||||
|
||||
-- Add credential-related actions to ActivityAction
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_CREATED';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_ACCESSED';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_ROTATED';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_REVOKED';
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE USER_CREDENTIALS TABLE
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE "user_credentials" (
|
||||
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"user_id" UUID NOT NULL,
|
||||
"workspace_id" UUID,
|
||||
|
||||
-- Identity
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"provider" VARCHAR(100) NOT NULL,
|
||||
"type" "CredentialType" NOT NULL,
|
||||
"scope" "CredentialScope" NOT NULL DEFAULT 'USER',
|
||||
|
||||
-- Encrypted storage
|
||||
"encrypted_value" TEXT NOT NULL,
|
||||
"masked_value" VARCHAR(20),
|
||||
|
||||
-- Metadata
|
||||
"description" TEXT,
|
||||
"expires_at" TIMESTAMPTZ,
|
||||
"last_used_at" TIMESTAMPTZ,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
|
||||
-- Status
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"rotated_at" TIMESTAMPTZ,
|
||||
|
||||
-- Audit
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT "user_credentials_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE FOREIGN KEY CONSTRAINTS
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_user_id_fkey"
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_workspace_id_fkey"
|
||||
FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE INDEXES
|
||||
-- =============================================================================
|
||||
|
||||
-- Index for user lookups
|
||||
CREATE INDEX "user_credentials_user_id_idx" ON "user_credentials"("user_id");
|
||||
|
||||
-- Index for workspace lookups
|
||||
CREATE INDEX "user_credentials_workspace_id_idx" ON "user_credentials"("workspace_id");
|
||||
|
||||
-- Index for user + scope queries
|
||||
CREATE INDEX "user_credentials_user_id_scope_idx" ON "user_credentials"("user_id", "scope");
|
||||
|
||||
-- Index for workspace + scope queries
|
||||
CREATE INDEX "user_credentials_workspace_id_scope_idx" ON "user_credentials"("workspace_id", "scope");
|
||||
|
||||
-- Index for scope + active status queries
|
||||
CREATE INDEX "user_credentials_scope_is_active_idx" ON "user_credentials"("scope", "is_active");
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE UNIQUE CONSTRAINT
|
||||
-- =============================================================================
|
||||
|
||||
-- Prevent duplicate credentials per user/workspace/provider/name
|
||||
CREATE UNIQUE INDEX "user_credentials_user_id_workspace_id_provider_name_key"
|
||||
ON "user_credentials"("user_id", "workspace_id", "provider", "name");
|
||||
|
||||
-- =============================================================================
|
||||
-- ENABLE FORCE ROW LEVEL SECURITY
|
||||
-- =============================================================================
|
||||
-- FORCE means the table owner (mosaic) is also subject to RLS policies.
|
||||
-- This prevents Prisma (connecting as owner) from bypassing policies.
|
||||
|
||||
ALTER TABLE user_credentials ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE user_credentials FORCE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- RLS POLICIES
|
||||
-- =============================================================================
|
||||
|
||||
-- Owner bypass policy: Allow access to all rows ONLY when no RLS context is set
|
||||
-- This is required for:
|
||||
-- 1. Prisma migrations that run without RLS context
|
||||
-- 2. Database maintenance operations
|
||||
-- When RLS context IS set (current_user_id() returns non-NULL), this policy does not apply
|
||||
--
|
||||
-- NOTE: If connecting as a PostgreSQL superuser (like the default 'mosaic' role),
|
||||
-- RLS policies are bypassed entirely. For full RLS enforcement, the application
|
||||
-- should connect as a non-superuser role. See docs/design/credential-security.md
|
||||
CREATE POLICY user_credentials_owner_bypass ON user_credentials
|
||||
FOR ALL
|
||||
USING (current_user_id() IS NULL);
|
||||
|
||||
-- User access policy: USER-scoped credentials visible only to owner
|
||||
-- Uses current_user_id() helper from migration 20260129221004_add_rls_policies
|
||||
CREATE POLICY user_credentials_user_access ON user_credentials
|
||||
FOR ALL
|
||||
USING (
|
||||
scope = 'USER' AND user_id = current_user_id()
|
||||
);
|
||||
|
||||
-- Workspace admin access policy: WORKSPACE-scoped credentials visible to workspace admins
|
||||
-- Uses is_workspace_admin() helper from migration 20260129221004_add_rls_policies
|
||||
CREATE POLICY user_credentials_workspace_access ON user_credentials
|
||||
FOR ALL
|
||||
USING (
|
||||
scope = 'WORKSPACE'
|
||||
AND workspace_id IS NOT NULL
|
||||
AND is_workspace_admin(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- SYSTEM-scoped credentials are only accessible via owner bypass policy
|
||||
-- (when current_user_id() IS NULL, which happens for admin operations)
|
||||
|
||||
-- =============================================================================
|
||||
-- AUDIT TRIGGER
|
||||
-- =============================================================================
|
||||
|
||||
-- Update updated_at timestamp on row changes
|
||||
CREATE OR REPLACE FUNCTION update_user_credentials_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER user_credentials_updated_at
|
||||
BEFORE UPDATE ON user_credentials
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_user_credentials_updated_at();
|
||||
|
||||
-- =============================================================================
|
||||
-- NOTES
|
||||
-- =============================================================================
|
||||
-- This migration creates the foundation for secure credential storage.
|
||||
-- The encrypted_value column stores ciphertext in one of two formats:
|
||||
--
|
||||
-- 1. OpenBao Transit format (preferred): vault:v1:base64data
|
||||
-- 2. AES-256-GCM fallback format: iv:authTag:encrypted
|
||||
--
|
||||
-- The VaultService (issue #353) handles encryption/decryption with automatic
|
||||
-- fallback to CryptoService when OpenBao is unavailable.
|
||||
--
|
||||
-- RLS enforcement ensures:
|
||||
-- - USER scope: Only the credential owner can access
|
||||
-- - WORKSPACE scope: Only workspace admins can access
|
||||
-- - SYSTEM scope: Only accessible via admin/migration bypass
|
||||
@@ -1,37 +0,0 @@
|
||||
-- Encrypt existing plaintext Account tokens
|
||||
-- This migration adds an encryption_version column and marks existing records for encryption
|
||||
-- The actual encryption happens via Prisma middleware on first read/write
|
||||
|
||||
-- Add encryption_version column to track encryption state
|
||||
-- NULL = not encrypted (legacy plaintext)
|
||||
-- 'aes' = AES-256-GCM encrypted
|
||||
-- 'vault' = OpenBao Transit encrypted (Phase 2)
|
||||
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS encryption_version VARCHAR(20);
|
||||
|
||||
-- Create index for efficient queries filtering by encryption status
|
||||
-- This index is also declared in Prisma schema (@@index([encryptionVersion]))
|
||||
-- Using CREATE INDEX IF NOT EXISTS for idempotency
|
||||
CREATE INDEX IF NOT EXISTS "accounts_encryption_version_idx" ON accounts(encryption_version);
|
||||
|
||||
-- Verify index was created successfully by running:
|
||||
-- SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'accounts' AND indexname = 'accounts_encryption_version_idx';
|
||||
|
||||
-- Update statistics for query planner
|
||||
ANALYZE accounts;
|
||||
|
||||
-- Migration Note:
|
||||
-- This migration does NOT encrypt data in-place to avoid downtime and data corruption risks.
|
||||
-- Instead, the Prisma middleware (account-encryption.middleware.ts) handles encryption:
|
||||
--
|
||||
-- 1. On READ: Detects format (plaintext vs encrypted) and decrypts if needed
|
||||
-- 2. On WRITE: Encrypts tokens and sets encryption_version = 'aes'
|
||||
-- 3. Backward compatible: Plaintext tokens (encryption_version = NULL) are passed through unchanged
|
||||
--
|
||||
-- To actively encrypt existing tokens, run the companion script:
|
||||
-- node scripts/encrypt-account-tokens.js
|
||||
--
|
||||
-- This approach ensures:
|
||||
-- - Zero downtime migration
|
||||
-- - No risk of corrupting tokens during bulk encryption
|
||||
-- - Progressive encryption as tokens are accessed/refreshed
|
||||
-- - Easy rollback (middleware is idempotent)
|
||||
@@ -1,26 +0,0 @@
|
||||
-- Encrypt LLM Provider API Keys Migration
|
||||
--
|
||||
-- This migration enables transparent encryption/decryption of LLM provider API keys
|
||||
-- stored in the llm_provider_instances.config JSON field.
|
||||
--
|
||||
-- IMPORTANT: This is a data migration with no schema changes.
|
||||
--
|
||||
-- Strategy:
|
||||
-- 1. Prisma middleware (llm-encryption.middleware.ts) handles encryption/decryption
|
||||
-- 2. Middleware auto-detects encryption format:
|
||||
-- - vault:v1:... = OpenBao Transit encrypted
|
||||
-- - Otherwise = Legacy plaintext (backward compatible)
|
||||
-- 3. New API keys are always encrypted on write
|
||||
-- 4. Existing plaintext keys work until re-saved (lazy migration)
|
||||
--
|
||||
-- To actively encrypt all existing API keys NOW:
|
||||
-- pnpm --filter @mosaic/api migrate:encrypt-llm-keys
|
||||
--
|
||||
-- This approach ensures:
|
||||
-- - Zero downtime migration
|
||||
-- - No schema changes required
|
||||
-- - Backward compatible with plaintext keys
|
||||
-- - Progressive encryption as keys are accessed/updated
|
||||
-- - Easy rollback (middleware is idempotent)
|
||||
--
|
||||
-- Note: No SQL changes needed. This file exists for migration tracking only.
|
||||
@@ -1,197 +0,0 @@
|
||||
-- RecreateEnum: FormalityLevel was dropped in 20260129235248_add_link_storage_fields
|
||||
CREATE TYPE "FormalityLevel" AS ENUM ('VERY_CASUAL', 'CASUAL', 'NEUTRAL', 'FORMAL', 'VERY_FORMAL');
|
||||
|
||||
-- RecreateTable: personalities was dropped in 20260129235248_add_link_storage_fields
|
||||
-- Recreated with current schema (display_name, system_prompt, temperature, etc.)
|
||||
CREATE TABLE "personalities" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"display_name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"system_prompt" TEXT NOT NULL,
|
||||
"temperature" DOUBLE PRECISION,
|
||||
"max_tokens" INTEGER,
|
||||
"llm_provider_instance_id" UUID,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"is_enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "personalities_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex: personalities
|
||||
CREATE UNIQUE INDEX "personalities_id_workspace_id_key" ON "personalities"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "personalities_workspace_id_name_key" ON "personalities"("workspace_id", "name");
|
||||
CREATE INDEX "personalities_workspace_id_idx" ON "personalities"("workspace_id");
|
||||
CREATE INDEX "personalities_workspace_id_is_default_idx" ON "personalities"("workspace_id", "is_default");
|
||||
CREATE INDEX "personalities_workspace_id_is_enabled_idx" ON "personalities"("workspace_id", "is_enabled");
|
||||
CREATE INDEX "personalities_llm_provider_instance_id_idx" ON "personalities"("llm_provider_instance_id");
|
||||
|
||||
-- AddForeignKey: personalities
|
||||
ALTER TABLE "personalities" ADD CONSTRAINT "personalities_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "personalities" ADD CONSTRAINT "personalities_llm_provider_instance_id_fkey" FOREIGN KEY ("llm_provider_instance_id") REFERENCES "llm_provider_instances"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "cron_schedules" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"expression" TEXT NOT NULL,
|
||||
"command" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"last_run" TIMESTAMPTZ,
|
||||
"next_run" TIMESTAMPTZ,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "cron_schedules_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workspace_llm_settings" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"default_llm_provider_id" UUID,
|
||||
"default_personality_id" UUID,
|
||||
"settings" JSONB DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "workspace_llm_settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "quality_gates" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"command" TEXT,
|
||||
"expected_output" TEXT,
|
||||
"is_regex" BOOLEAN NOT NULL DEFAULT false,
|
||||
"required" BOOLEAN NOT NULL DEFAULT true,
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
"is_enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "quality_gates_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "task_rejections" (
|
||||
"id" UUID NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"workspace_id" TEXT NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"attempt_count" INTEGER NOT NULL,
|
||||
"failures" JSONB NOT NULL,
|
||||
"original_task" TEXT NOT NULL,
|
||||
"started_at" TIMESTAMPTZ NOT NULL,
|
||||
"rejected_at" TIMESTAMPTZ NOT NULL,
|
||||
"escalated" BOOLEAN NOT NULL DEFAULT false,
|
||||
"manual_review" BOOLEAN NOT NULL DEFAULT false,
|
||||
"resolved_at" TIMESTAMPTZ,
|
||||
"resolution" TEXT,
|
||||
|
||||
CONSTRAINT "task_rejections_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "token_budgets" (
|
||||
"id" UUID NOT NULL,
|
||||
"task_id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"allocated_tokens" INTEGER NOT NULL,
|
||||
"estimated_complexity" TEXT NOT NULL,
|
||||
"input_tokens_used" INTEGER NOT NULL DEFAULT 0,
|
||||
"output_tokens_used" INTEGER NOT NULL DEFAULT 0,
|
||||
"total_tokens_used" INTEGER NOT NULL DEFAULT 0,
|
||||
"estimated_cost" DECIMAL(10,6),
|
||||
"started_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
"budget_utilization" DOUBLE PRECISION,
|
||||
"suspicious_pattern" BOOLEAN NOT NULL DEFAULT false,
|
||||
"suspicious_reason" TEXT,
|
||||
|
||||
CONSTRAINT "token_budgets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "llm_usage_logs" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"provider" VARCHAR(50) NOT NULL,
|
||||
"model" VARCHAR(100) NOT NULL,
|
||||
"provider_instance_id" UUID,
|
||||
"prompt_tokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"completion_tokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"total_tokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"cost_cents" DOUBLE PRECISION,
|
||||
"task_type" VARCHAR(50),
|
||||
"conversation_id" UUID,
|
||||
"duration_ms" INTEGER,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "llm_usage_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex: cron_schedules
|
||||
CREATE INDEX "cron_schedules_workspace_id_idx" ON "cron_schedules"("workspace_id");
|
||||
CREATE INDEX "cron_schedules_workspace_id_enabled_idx" ON "cron_schedules"("workspace_id", "enabled");
|
||||
CREATE INDEX "cron_schedules_next_run_idx" ON "cron_schedules"("next_run");
|
||||
|
||||
-- CreateIndex: workspace_llm_settings
|
||||
CREATE UNIQUE INDEX "workspace_llm_settings_workspace_id_key" ON "workspace_llm_settings"("workspace_id");
|
||||
CREATE INDEX "workspace_llm_settings_workspace_id_idx" ON "workspace_llm_settings"("workspace_id");
|
||||
CREATE INDEX "workspace_llm_settings_default_llm_provider_id_idx" ON "workspace_llm_settings"("default_llm_provider_id");
|
||||
CREATE INDEX "workspace_llm_settings_default_personality_id_idx" ON "workspace_llm_settings"("default_personality_id");
|
||||
|
||||
-- CreateIndex: quality_gates
|
||||
CREATE UNIQUE INDEX "quality_gates_workspace_id_name_key" ON "quality_gates"("workspace_id", "name");
|
||||
CREATE INDEX "quality_gates_workspace_id_idx" ON "quality_gates"("workspace_id");
|
||||
CREATE INDEX "quality_gates_workspace_id_is_enabled_idx" ON "quality_gates"("workspace_id", "is_enabled");
|
||||
|
||||
-- CreateIndex: task_rejections
|
||||
CREATE INDEX "task_rejections_task_id_idx" ON "task_rejections"("task_id");
|
||||
CREATE INDEX "task_rejections_workspace_id_idx" ON "task_rejections"("workspace_id");
|
||||
CREATE INDEX "task_rejections_agent_id_idx" ON "task_rejections"("agent_id");
|
||||
CREATE INDEX "task_rejections_escalated_idx" ON "task_rejections"("escalated");
|
||||
CREATE INDEX "task_rejections_manual_review_idx" ON "task_rejections"("manual_review");
|
||||
|
||||
-- CreateIndex: token_budgets
|
||||
CREATE UNIQUE INDEX "token_budgets_task_id_key" ON "token_budgets"("task_id");
|
||||
CREATE INDEX "token_budgets_task_id_idx" ON "token_budgets"("task_id");
|
||||
CREATE INDEX "token_budgets_workspace_id_idx" ON "token_budgets"("workspace_id");
|
||||
CREATE INDEX "token_budgets_suspicious_pattern_idx" ON "token_budgets"("suspicious_pattern");
|
||||
|
||||
-- CreateIndex: llm_usage_logs
|
||||
CREATE INDEX "llm_usage_logs_workspace_id_idx" ON "llm_usage_logs"("workspace_id");
|
||||
CREATE INDEX "llm_usage_logs_workspace_id_created_at_idx" ON "llm_usage_logs"("workspace_id", "created_at");
|
||||
CREATE INDEX "llm_usage_logs_user_id_idx" ON "llm_usage_logs"("user_id");
|
||||
CREATE INDEX "llm_usage_logs_provider_idx" ON "llm_usage_logs"("provider");
|
||||
CREATE INDEX "llm_usage_logs_model_idx" ON "llm_usage_logs"("model");
|
||||
CREATE INDEX "llm_usage_logs_provider_instance_id_idx" ON "llm_usage_logs"("provider_instance_id");
|
||||
CREATE INDEX "llm_usage_logs_task_type_idx" ON "llm_usage_logs"("task_type");
|
||||
CREATE INDEX "llm_usage_logs_conversation_id_idx" ON "llm_usage_logs"("conversation_id");
|
||||
|
||||
-- AddForeignKey: cron_schedules
|
||||
ALTER TABLE "cron_schedules" ADD CONSTRAINT "cron_schedules_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: workspace_llm_settings
|
||||
ALTER TABLE "workspace_llm_settings" ADD CONSTRAINT "workspace_llm_settings_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "workspace_llm_settings" ADD CONSTRAINT "workspace_llm_settings_default_llm_provider_id_fkey" FOREIGN KEY ("default_llm_provider_id") REFERENCES "llm_provider_instances"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "workspace_llm_settings" ADD CONSTRAINT "workspace_llm_settings_default_personality_id_fkey" FOREIGN KEY ("default_personality_id") REFERENCES "personalities"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: quality_gates
|
||||
ALTER TABLE "quality_gates" ADD CONSTRAINT "quality_gates_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: llm_usage_logs
|
||||
ALTER TABLE "llm_usage_logs" ADD CONSTRAINT "llm_usage_logs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "llm_usage_logs" ADD CONSTRAINT "llm_usage_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "llm_usage_logs" ADD CONSTRAINT "llm_usage_logs_provider_instance_id_fkey" FOREIGN KEY ("provider_instance_id") REFERENCES "llm_provider_instances"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "workspaces" ADD COLUMN "matrix_room_id" TEXT;
|
||||
@@ -1,49 +0,0 @@
|
||||
-- Fix schema drift: tables, indexes, and constraints defined in schema.prisma
|
||||
-- but never created (or dropped and never recreated) by prior migrations.
|
||||
|
||||
-- ============================================
|
||||
-- CreateTable: instances (Federation module)
|
||||
-- Never created in any prior migration
|
||||
-- ============================================
|
||||
CREATE TABLE "instances" (
|
||||
"id" UUID NOT NULL,
|
||||
"instance_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"public_key" TEXT NOT NULL,
|
||||
"private_key" TEXT NOT NULL,
|
||||
"capabilities" JSONB NOT NULL DEFAULT '{}',
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "instances_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "instances_instance_id_key" ON "instances"("instance_id");
|
||||
|
||||
-- ============================================
|
||||
-- Recreate dropped unique index on knowledge_links
|
||||
-- Created in 20260129220645_add_knowledge_module, dropped in
|
||||
-- 20260129235248_add_link_storage_fields, never recreated.
|
||||
-- ============================================
|
||||
CREATE UNIQUE INDEX "knowledge_links_source_id_target_id_key" ON "knowledge_links"("source_id", "target_id");
|
||||
|
||||
-- ============================================
|
||||
-- Missing @@unique([id, workspaceId]) composite indexes
|
||||
-- Defined in schema.prisma but never created in migrations.
|
||||
-- (agent_tasks and runner_jobs already have these.)
|
||||
-- ============================================
|
||||
CREATE UNIQUE INDEX "tasks_id_workspace_id_key" ON "tasks"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "events_id_workspace_id_key" ON "events"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "projects_id_workspace_id_key" ON "projects"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "activity_logs_id_workspace_id_key" ON "activity_logs"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "domains_id_workspace_id_key" ON "domains"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "ideas_id_workspace_id_key" ON "ideas"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "user_layouts_id_workspace_id_key" ON "user_layouts"("id", "workspace_id");
|
||||
|
||||
-- ============================================
|
||||
-- Missing index on agent_tasks.agent_type
|
||||
-- Defined as @@index([agentType]) in schema.prisma
|
||||
-- ============================================
|
||||
CREATE INDEX "agent_tasks_agent_type_idx" ON "agent_tasks"("agent_type");
|
||||
@@ -1,23 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TerminalSessionStatus" AS ENUM ('ACTIVE', 'CLOSED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "terminal_sessions" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL DEFAULT 'Terminal',
|
||||
"status" "TerminalSessionStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"closed_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "terminal_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "terminal_sessions_workspace_id_idx" ON "terminal_sessions"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "terminal_sessions_workspace_id_status_idx" ON "terminal_sessions"("workspace_id", "status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "terminal_sessions" ADD CONSTRAINT "terminal_sessions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable: add tone and formality_level columns to personalities
|
||||
ALTER TABLE "personalities" ADD COLUMN "tone" TEXT NOT NULL DEFAULT 'neutral';
|
||||
ALTER TABLE "personalities" ADD COLUMN "formality_level" "FormalityLevel" NOT NULL DEFAULT 'NEUTRAL';
|
||||
@@ -1,24 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_memories" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" JSONB NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "agent_memories_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_memories_workspace_id_agent_id_key_key" ON "agent_memories"("workspace_id", "agent_id", "key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_memories_workspace_id_idx" ON "agent_memories"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_memories_agent_id_idx" ON "agent_memories"("agent_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_memories" ADD CONSTRAINT "agent_memories_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,33 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "conversation_archives" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"session_id" TEXT NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"messages" JSONB NOT NULL,
|
||||
"message_count" INTEGER NOT NULL,
|
||||
"summary" TEXT NOT NULL,
|
||||
"embedding" vector(1536),
|
||||
"started_at" TIMESTAMPTZ NOT NULL,
|
||||
"ended_at" TIMESTAMPTZ,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "conversation_archives_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "conversation_archives_workspace_id_session_id_key" ON "conversation_archives"("workspace_id", "session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "conversation_archives_workspace_id_idx" ON "conversation_archives"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "conversation_archives_agent_id_idx" ON "conversation_archives"("agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "conversation_archives_started_at_idx" ON "conversation_archives"("started_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "conversation_archives" ADD CONSTRAINT "conversation_archives_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,109 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "SystemConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"encrypted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SystemConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BreakglassUser" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "BreakglassUser_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LlmProvider" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"baseUrl" TEXT,
|
||||
"apiKey" TEXT,
|
||||
"apiType" TEXT NOT NULL DEFAULT 'openai-completions',
|
||||
"models" JSONB NOT NULL DEFAULT '[]',
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LlmProvider_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserContainer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"containerId" TEXT,
|
||||
"containerName" TEXT NOT NULL,
|
||||
"gatewayPort" INTEGER,
|
||||
"gatewayToken" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'stopped',
|
||||
"lastActiveAt" TIMESTAMP(3),
|
||||
"idleTimeoutMin" INTEGER NOT NULL DEFAULT 30,
|
||||
"config" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserContainer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SystemContainer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL,
|
||||
"containerId" TEXT,
|
||||
"gatewayPort" INTEGER,
|
||||
"gatewayToken" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'stopped',
|
||||
"primaryModel" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SystemContainer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserAgentConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"primaryModel" TEXT,
|
||||
"fallbackModels" JSONB NOT NULL DEFAULT '[]',
|
||||
"personality" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserAgentConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SystemConfig_key_key" ON "SystemConfig"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BreakglassUser_username_key" ON "BreakglassUser"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LlmProvider_userId_idx" ON "LlmProvider"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LlmProvider_userId_name_key" ON "LlmProvider"("userId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserContainer_userId_key" ON "UserContainer"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SystemContainer_name_key" ON "SystemContainer"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserAgentConfig_userId_key" ON "UserAgentConfig"("userId");
|
||||
@@ -1,37 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "findings" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"task_id" UUID,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
"summary" TEXT NOT NULL,
|
||||
"embedding" vector(1536),
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "findings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "findings_id_workspace_id_key" ON "findings"("id", "workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "findings_workspace_id_idx" ON "findings"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "findings_agent_id_idx" ON "findings"("agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "findings_type_idx" ON "findings"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "findings_task_id_idx" ON "findings"("task_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "findings" ADD CONSTRAINT "findings_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "findings" ADD CONSTRAINT "findings_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "agent_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "tasks" ADD COLUMN "assigned_agent" TEXT;
|
||||
@@ -1,13 +0,0 @@
|
||||
-- MS21: Add admin, local auth, and invitation fields to users table
|
||||
-- These columns were added to schema.prisma but never captured in a migration.
|
||||
|
||||
ALTER TABLE "users"
|
||||
ADD COLUMN IF NOT EXISTS "deactivated_at" TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS "is_local_auth" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS "password_hash" TEXT,
|
||||
ADD COLUMN IF NOT EXISTS "invited_by" UUID,
|
||||
ADD COLUMN IF NOT EXISTS "invitation_token" TEXT,
|
||||
ADD COLUMN IF NOT EXISTS "invited_at" TIMESTAMPTZ;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "users_invitation_token_key" ON "users"("invitation_token");
|
||||
@@ -1,83 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "AgentConversationMessage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionId" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL DEFAULT 'internal',
|
||||
"role" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
|
||||
CONSTRAINT "AgentConversationMessage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AgentSessionTree" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionId" TEXT NOT NULL,
|
||||
"parentSessionId" TEXT,
|
||||
"provider" TEXT NOT NULL DEFAULT 'internal',
|
||||
"missionId" TEXT,
|
||||
"taskId" TEXT,
|
||||
"taskSource" TEXT DEFAULT 'internal',
|
||||
"agentType" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'spawning',
|
||||
"spawnedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
|
||||
CONSTRAINT "AgentSessionTree_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AgentProviderConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"workspaceId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"gatewayUrl" TEXT NOT NULL,
|
||||
"credentials" JSONB NOT NULL DEFAULT '{}',
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AgentProviderConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OperatorAuditLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"sessionId" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"content" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "OperatorAuditLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AgentConversationMessage_sessionId_timestamp_idx" ON "AgentConversationMessage"("sessionId", "timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AgentSessionTree_sessionId_key" ON "AgentSessionTree"("sessionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AgentSessionTree_parentSessionId_idx" ON "AgentSessionTree"("parentSessionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AgentSessionTree_missionId_idx" ON "AgentSessionTree"("missionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AgentProviderConfig_workspaceId_name_key" ON "AgentProviderConfig"("workspaceId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OperatorAuditLog_sessionId_idx" ON "OperatorAuditLog"("sessionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OperatorAuditLog_userId_idx" ON "OperatorAuditLog"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OperatorAuditLog_createdAt_idx" ON "OperatorAuditLog"("createdAt");
|
||||
@@ -1,14 +0,0 @@
|
||||
CREATE TABLE "pending_merges" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"repo" TEXT NOT NULL,
|
||||
"pr_number" INTEGER NOT NULL,
|
||||
"head_sha" TEXT NOT NULL,
|
||||
"state" TEXT NOT NULL DEFAULT 'pending',
|
||||
"review_result" JSONB,
|
||||
"ci_status" TEXT,
|
||||
"requester" TEXT,
|
||||
"gitea_merge_url" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT "pending_merges_repo_pr_sha_key" UNIQUE ("repo", "pr_number", "head_sha")
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,603 +0,0 @@
|
||||
import {
|
||||
PrismaClient,
|
||||
TaskStatus,
|
||||
TaskPriority,
|
||||
ProjectStatus,
|
||||
WorkspaceMemberRole,
|
||||
EntryStatus,
|
||||
Visibility,
|
||||
} from "@prisma/client";
|
||||
import { seedAgentTemplates } from "../src/seed/agent-templates.seed";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding database...");
|
||||
|
||||
// IMPORTANT: This seed script should not be run concurrently
|
||||
// If running in CI/CD, ensure serialization of seed operations
|
||||
// to prevent race conditions and data corruption
|
||||
|
||||
// Create test user
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: "[email protected]" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "[email protected]",
|
||||
name: "Development User",
|
||||
preferences: {
|
||||
theme: "system",
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Created user: ${user.email}`);
|
||||
|
||||
// Create workspace
|
||||
const workspace = await prisma.workspace.upsert({
|
||||
where: { id: "00000000-0000-0000-0000-000000000001" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "00000000-0000-0000-0000-000000000001",
|
||||
name: "Development Workspace",
|
||||
ownerId: user.id,
|
||||
settings: {
|
||||
timezone: "America/New_York",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Created workspace: ${workspace.name}`);
|
||||
|
||||
// Add user as workspace owner
|
||||
await prisma.workspaceMember.upsert({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
role: WorkspaceMemberRole.OWNER,
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// WIDGET DEFINITIONS (global, not workspace-scoped)
|
||||
// ============================================
|
||||
const widgetDefs = [
|
||||
{
|
||||
name: "TasksWidget",
|
||||
displayName: "Tasks",
|
||||
description: "View and manage your tasks",
|
||||
component: "TasksWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
maxWidth: 4,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "CalendarWidget",
|
||||
displayName: "Calendar",
|
||||
description: "View upcoming events and schedule",
|
||||
component: "CalendarWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 2,
|
||||
minHeight: 2,
|
||||
maxWidth: 4,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "QuickCaptureWidget",
|
||||
displayName: "Quick Capture",
|
||||
description: "Quickly capture notes and tasks",
|
||||
component: "QuickCaptureWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 1,
|
||||
minWidth: 2,
|
||||
minHeight: 1,
|
||||
maxWidth: 4,
|
||||
maxHeight: 2,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "AgentStatusWidget",
|
||||
displayName: "Agent Status",
|
||||
description: "Monitor agent activity and status",
|
||||
component: "AgentStatusWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
maxWidth: 3,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "ActiveProjectsWidget",
|
||||
displayName: "Active Projects & Agent Chains",
|
||||
description: "View active projects and running agent sessions",
|
||||
component: "ActiveProjectsWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 3,
|
||||
minWidth: 2,
|
||||
minHeight: 2,
|
||||
maxWidth: 4,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "TaskProgressWidget",
|
||||
displayName: "Task Progress",
|
||||
description: "Live progress of orchestrator agent tasks",
|
||||
component: "TaskProgressWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
maxWidth: 3,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "OrchestratorEventsWidget",
|
||||
displayName: "Orchestrator Events",
|
||||
description: "Recent orchestration events with stream/Matrix visibility",
|
||||
component: "OrchestratorEventsWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
maxWidth: 4,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
];
|
||||
|
||||
for (const wd of widgetDefs) {
|
||||
await prisma.widgetDefinition.upsert({
|
||||
where: { name: wd.name },
|
||||
update: {
|
||||
displayName: wd.displayName,
|
||||
description: wd.description,
|
||||
component: wd.component,
|
||||
defaultWidth: wd.defaultWidth,
|
||||
defaultHeight: wd.defaultHeight,
|
||||
minWidth: wd.minWidth,
|
||||
minHeight: wd.minHeight,
|
||||
maxWidth: wd.maxWidth,
|
||||
maxHeight: wd.maxHeight,
|
||||
configSchema: wd.configSchema,
|
||||
},
|
||||
create: {
|
||||
name: wd.name,
|
||||
displayName: wd.displayName,
|
||||
description: wd.description,
|
||||
component: wd.component,
|
||||
defaultWidth: wd.defaultWidth,
|
||||
defaultHeight: wd.defaultHeight,
|
||||
minWidth: wd.minWidth,
|
||||
minHeight: wd.minHeight,
|
||||
maxWidth: wd.maxWidth,
|
||||
maxHeight: wd.maxHeight,
|
||||
configSchema: wd.configSchema,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Seeded ${widgetDefs.length} widget definitions`);
|
||||
|
||||
// Use transaction for atomic seed data reset and creation
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Delete existing seed data for idempotency (avoids duplicates on re-run)
|
||||
await tx.task.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
await tx.event.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
await tx.project.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
|
||||
// Create sample project
|
||||
const project = await tx.project.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
name: "Sample Project",
|
||||
description: "A sample project for development",
|
||||
status: ProjectStatus.ACTIVE,
|
||||
creatorId: user.id,
|
||||
color: "#3B82F6",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Created project: ${project.name}`);
|
||||
|
||||
// Create sample tasks
|
||||
const tasks = [
|
||||
{
|
||||
title: "Set up development environment",
|
||||
status: TaskStatus.COMPLETED,
|
||||
priority: TaskPriority.HIGH,
|
||||
},
|
||||
{
|
||||
title: "Review project requirements",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
},
|
||||
{
|
||||
title: "Design database schema",
|
||||
status: TaskStatus.COMPLETED,
|
||||
priority: TaskPriority.HIGH,
|
||||
},
|
||||
{
|
||||
title: "Implement NestJS integration",
|
||||
status: TaskStatus.COMPLETED,
|
||||
priority: TaskPriority.HIGH,
|
||||
},
|
||||
{
|
||||
title: "Create seed data",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
},
|
||||
];
|
||||
|
||||
// Use createMany for batch insertion (better performance)
|
||||
await tx.task.createMany({
|
||||
data: tasks.map((taskData) => ({
|
||||
workspaceId: workspace.id,
|
||||
title: taskData.title,
|
||||
status: taskData.status,
|
||||
priority: taskData.priority,
|
||||
creatorId: user.id,
|
||||
projectId: project.id,
|
||||
})),
|
||||
});
|
||||
|
||||
console.log(`Created ${tasks.length} sample tasks`);
|
||||
|
||||
// Create sample event
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(10, 0, 0, 0);
|
||||
|
||||
await tx.event.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
title: "Morning standup",
|
||||
description: "Daily team sync",
|
||||
startTime: tomorrow,
|
||||
endTime: new Date(tomorrow.getTime() + 30 * 60000), // 30 minutes later
|
||||
creatorId: user.id,
|
||||
projectId: project.id,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Created sample event");
|
||||
|
||||
// ============================================
|
||||
// KNOWLEDGE MODULE SEED DATA
|
||||
// ============================================
|
||||
|
||||
// Delete existing knowledge data
|
||||
await tx.knowledgeEmbedding.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
|
||||
await tx.knowledgeEntryTag.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
|
||||
await tx.knowledgeLink.deleteMany({ where: { source: { workspaceId: workspace.id } } });
|
||||
await tx.knowledgeEntryVersion.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
|
||||
await tx.knowledgeEntry.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
await tx.knowledgeTag.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
|
||||
// Create knowledge tags
|
||||
const tags = await Promise.all([
|
||||
tx.knowledgeTag.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
name: "Architecture",
|
||||
slug: "architecture",
|
||||
color: "#3B82F6",
|
||||
description: "System architecture and design decisions",
|
||||
},
|
||||
}),
|
||||
tx.knowledgeTag.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
name: "Development",
|
||||
slug: "development",
|
||||
color: "#10B981",
|
||||
description: "Development practices and guidelines",
|
||||
},
|
||||
}),
|
||||
tx.knowledgeTag.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
name: "Getting Started",
|
||||
slug: "getting-started",
|
||||
color: "#F59E0B",
|
||||
description: "Onboarding and setup guides",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log(`Created ${tags.length} knowledge tags`);
|
||||
|
||||
// Create knowledge entries
|
||||
const entries = [
|
||||
{
|
||||
slug: "welcome",
|
||||
title: "Welcome to Mosaic Stack Knowledge Base",
|
||||
content: `# Welcome to Mosaic Stack
|
||||
|
||||
This is the knowledge base for the Mosaic Stack project. Here you'll find:
|
||||
|
||||
- **[[architecture-overview]]** - High-level system architecture
|
||||
- **[[development-setup]]** - Getting started with development
|
||||
- **[[database-schema]]** - Database design and conventions
|
||||
|
||||
## About This Knowledge Base
|
||||
|
||||
The Knowledge Module provides:
|
||||
- Wiki-style linking between entries
|
||||
- Full-text and semantic search
|
||||
- Version history and change tracking
|
||||
- Tag-based organization
|
||||
|
||||
Start exploring by following the links above!`,
|
||||
summary: "Introduction to the Mosaic Stack knowledge base and navigation guide",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
visibility: Visibility.WORKSPACE,
|
||||
tags: ["getting-started"],
|
||||
},
|
||||
{
|
||||
slug: "architecture-overview",
|
||||
title: "Architecture Overview",
|
||||
content: `# Architecture Overview
|
||||
|
||||
The Mosaic Stack is built on a modern, scalable architecture:
|
||||
|
||||
## Stack Components
|
||||
|
||||
- **Frontend**: Next.js 15+ with React 19
|
||||
- **Backend**: NestJS with Prisma ORM
|
||||
- **Database**: PostgreSQL 17 with pgvector
|
||||
- **Cache**: Valkey (Redis fork)
|
||||
|
||||
## Key Modules
|
||||
|
||||
1. **Task Management** - See [[development-setup]] for local setup
|
||||
2. **Event Calendar** - Integrated scheduling
|
||||
3. **Knowledge Base** - This module! See [[database-schema]]
|
||||
4. **Agent Orchestration** - AI agent coordination
|
||||
|
||||
The database schema is documented in [[database-schema]].`,
|
||||
summary: "High-level overview of Mosaic Stack architecture and components",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
visibility: Visibility.WORKSPACE,
|
||||
tags: ["architecture"],
|
||||
},
|
||||
{
|
||||
slug: "development-setup",
|
||||
title: "Development Setup Guide",
|
||||
content: `# Development Setup Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 22+
|
||||
- PostgreSQL 17
|
||||
- pnpm 9+
|
||||
|
||||
## Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
# Clone the repository
|
||||
git clone https://git.mosaicstack.dev/mosaic/stack.git
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Set up database
|
||||
cd apps/api
|
||||
pnpm prisma migrate dev
|
||||
pnpm prisma:seed
|
||||
|
||||
# Start development servers
|
||||
pnpm dev
|
||||
\`\`\`
|
||||
|
||||
## Architecture
|
||||
|
||||
Before diving in, review the [[architecture-overview]] to understand the system design.
|
||||
|
||||
## Database
|
||||
|
||||
The database schema is documented in [[database-schema]]. All models use Prisma ORM.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Read the [[architecture-overview]]
|
||||
2. Explore the codebase
|
||||
3. Check out the [[database-schema]] documentation`,
|
||||
summary: "Step-by-step guide to setting up the Mosaic Stack development environment",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
visibility: Visibility.WORKSPACE,
|
||||
tags: ["development", "getting-started"],
|
||||
},
|
||||
{
|
||||
slug: "database-schema",
|
||||
title: "Database Schema Documentation",
|
||||
content: `# Database Schema Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Mosaic Stack uses PostgreSQL 17 with Prisma ORM. See [[architecture-overview]] for context.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- All IDs are UUIDs
|
||||
- Timestamps use \`@db.Timestamptz\` (timezone-aware)
|
||||
- Soft deletes via status fields (e.g., ARCHIVED)
|
||||
- Relations enforce cascade deletes for data integrity
|
||||
|
||||
## Core Models
|
||||
|
||||
### Task Management
|
||||
- \`Task\` - Individual work items
|
||||
- \`Project\` - Task containers
|
||||
- \`Domain\` - High-level categorization
|
||||
|
||||
### Knowledge Module
|
||||
- \`KnowledgeEntry\` - Wiki pages
|
||||
- \`KnowledgeLink\` - Page connections
|
||||
- \`KnowledgeTag\` - Categorization
|
||||
- \`KnowledgeEmbedding\` - Semantic search (pgvector)
|
||||
|
||||
### Agent System
|
||||
- \`Agent\` - AI agent instances
|
||||
- \`AgentSession\` - Conversation sessions
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrations are managed via Prisma:
|
||||
|
||||
\`\`\`bash
|
||||
# Create migration
|
||||
pnpm prisma migrate dev --name my_migration
|
||||
|
||||
# Apply in production
|
||||
pnpm prisma migrate deploy
|
||||
\`\`\`
|
||||
|
||||
For setup instructions, see [[development-setup]].`,
|
||||
summary:
|
||||
"Comprehensive documentation of the Mosaic Stack database schema and Prisma conventions",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
visibility: Visibility.WORKSPACE,
|
||||
tags: ["architecture", "development"],
|
||||
},
|
||||
{
|
||||
slug: "future-ideas",
|
||||
title: "Future Ideas and Roadmap",
|
||||
content: `# Future Ideas and Roadmap
|
||||
|
||||
## Planned Features
|
||||
|
||||
- Real-time collaboration (CRDT)
|
||||
- Advanced graph visualizations
|
||||
- AI-powered summarization
|
||||
- Mobile app
|
||||
|
||||
## Research Areas
|
||||
|
||||
- Vector search optimization
|
||||
- Knowledge graph algorithms
|
||||
- Agent memory systems
|
||||
|
||||
This is a draft document. See [[architecture-overview]] for current state.`,
|
||||
summary: "Brainstorming document for future features and research directions",
|
||||
status: EntryStatus.DRAFT,
|
||||
visibility: Visibility.PRIVATE,
|
||||
tags: [],
|
||||
},
|
||||
];
|
||||
|
||||
// Create entries and track them for linking
|
||||
const createdEntries = new Map<string, any>();
|
||||
|
||||
for (const entryData of entries) {
|
||||
const entry = await tx.knowledgeEntry.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
slug: entryData.slug,
|
||||
title: entryData.title,
|
||||
content: entryData.content,
|
||||
summary: entryData.summary,
|
||||
status: entryData.status,
|
||||
visibility: entryData.visibility,
|
||||
createdBy: user.id,
|
||||
updatedBy: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
createdEntries.set(entryData.slug, entry);
|
||||
|
||||
// Create initial version
|
||||
await tx.knowledgeEntryVersion.create({
|
||||
data: {
|
||||
entryId: entry.id,
|
||||
version: 1,
|
||||
title: entry.title,
|
||||
content: entry.content,
|
||||
summary: entry.summary,
|
||||
createdBy: user.id,
|
||||
changeNote: "Initial version",
|
||||
},
|
||||
});
|
||||
|
||||
// Add tags
|
||||
for (const tagSlug of entryData.tags) {
|
||||
const tag = tags.find((t) => t.slug === tagSlug);
|
||||
if (tag) {
|
||||
await tx.knowledgeEntryTag.create({
|
||||
data: {
|
||||
entryId: entry.id,
|
||||
tagId: tag.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Created ${entries.length} knowledge entries`);
|
||||
|
||||
// Create wiki-links between entries
|
||||
const links = [
|
||||
{ source: "welcome", target: "architecture-overview", text: "architecture-overview" },
|
||||
{ source: "welcome", target: "development-setup", text: "development-setup" },
|
||||
{ source: "welcome", target: "database-schema", text: "database-schema" },
|
||||
{ source: "architecture-overview", target: "development-setup", text: "development-setup" },
|
||||
{ source: "architecture-overview", target: "database-schema", text: "database-schema" },
|
||||
{
|
||||
source: "development-setup",
|
||||
target: "architecture-overview",
|
||||
text: "architecture-overview",
|
||||
},
|
||||
{ source: "development-setup", target: "database-schema", text: "database-schema" },
|
||||
{ source: "database-schema", target: "architecture-overview", text: "architecture-overview" },
|
||||
{ source: "database-schema", target: "development-setup", text: "development-setup" },
|
||||
{ source: "future-ideas", target: "architecture-overview", text: "architecture-overview" },
|
||||
];
|
||||
|
||||
for (const link of links) {
|
||||
const sourceEntry = createdEntries.get(link.source);
|
||||
const targetEntry = createdEntries.get(link.target);
|
||||
|
||||
if (sourceEntry && targetEntry) {
|
||||
await tx.knowledgeLink.create({
|
||||
data: {
|
||||
sourceId: sourceEntry.id,
|
||||
targetId: targetEntry.id,
|
||||
linkText: link.text,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Created ${links.length} knowledge links`);
|
||||
});
|
||||
// Seed default agent templates (idempotent)
|
||||
await seedAgentTemplates(prisma);
|
||||
|
||||
console.log("Seeding completed successfully!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error("Error seeding database:", e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
/**
|
||||
* Data Migration: Encrypt LLM Provider API Keys
|
||||
*
|
||||
* Encrypts all plaintext API keys in llm_provider_instances.config using OpenBao Transit.
|
||||
* This script processes records in batches and runs in a transaction for safety.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm --filter @mosaic/api migrate:encrypt-llm-keys
|
||||
*
|
||||
* Environment Variables:
|
||||
* DATABASE_URL - PostgreSQL connection string
|
||||
* OPENBAO_ADDR - OpenBao server address (default: http://openbao:8200)
|
||||
* APPROLE_CREDENTIALS_PATH - Path to AppRole credentials file
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { VaultService } from "../src/vault/vault.service";
|
||||
import { TransitKey } from "../src/vault/vault.constants";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
interface LlmProviderConfig {
|
||||
apiKey?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface LlmProviderInstance {
|
||||
id: string;
|
||||
config: LlmProviderConfig;
|
||||
providerType: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is already encrypted
|
||||
*/
|
||||
function isEncrypted(value: string): boolean {
|
||||
if (!value || typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vault format: vault:v1:...
|
||||
if (value.startsWith("vault:v1:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// AES format: iv:authTag:encrypted (3 colon-separated hex parts)
|
||||
const parts = value.split(":");
|
||||
if (parts.length === 3 && parts.every((part) => /^[0-9a-f]+$/i.test(part))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main migration function
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
const logger = new Logger("EncryptLlmKeys");
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
try {
|
||||
logger.log("Starting LLM API key encryption migration...");
|
||||
|
||||
// Initialize VaultService
|
||||
const configService = new ConfigService();
|
||||
const vaultService = new VaultService(configService);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
await vaultService.onModuleInit();
|
||||
|
||||
logger.log("VaultService initialized successfully");
|
||||
|
||||
// Fetch all LLM provider instances
|
||||
const instances = await prisma.llmProviderInstance.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
config: true,
|
||||
providerType: true,
|
||||
displayName: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.log(`Found ${String(instances.length)} LLM provider instances`);
|
||||
|
||||
let encryptedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// Process each instance
|
||||
for (const instance of instances as LlmProviderInstance[]) {
|
||||
try {
|
||||
const config = instance.config;
|
||||
|
||||
// Skip if no apiKey field
|
||||
if (!config.apiKey || typeof config.apiKey !== "string") {
|
||||
logger.debug(`Skipping ${instance.displayName} (${instance.id}): No API key`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if already encrypted
|
||||
if (isEncrypted(config.apiKey)) {
|
||||
logger.debug(`Skipping ${instance.displayName} (${instance.id}): Already encrypted`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Encrypt the API key
|
||||
logger.log(`Encrypting ${instance.displayName} (${instance.providerType})...`);
|
||||
|
||||
const encryptedApiKey = await vaultService.encrypt(config.apiKey, TransitKey.LLM_CONFIG);
|
||||
|
||||
// Update the instance with encrypted key
|
||||
await prisma.llmProviderInstance.update({
|
||||
where: { id: instance.id },
|
||||
data: {
|
||||
config: {
|
||||
...config,
|
||||
apiKey: encryptedApiKey,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
encryptedCount++;
|
||||
logger.log(`✓ Encrypted ${instance.displayName} (${instance.id})`);
|
||||
} catch (error: unknown) {
|
||||
errorCount++;
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`✗ Failed to encrypt ${instance.displayName} (${instance.id}): ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
logger.log("\n=== Migration Summary ===");
|
||||
logger.log(`Total instances: ${String(instances.length)}`);
|
||||
logger.log(`Encrypted: ${String(encryptedCount)}`);
|
||||
logger.log(`Skipped: ${String(skippedCount)}`);
|
||||
logger.log(`Errors: ${String(errorCount)}`);
|
||||
|
||||
if (errorCount > 0) {
|
||||
logger.warn("\n⚠️ Some API keys failed to encrypt. Please review the errors above.");
|
||||
process.exit(1);
|
||||
} else if (encryptedCount === 0) {
|
||||
logger.log("\n✓ All API keys are already encrypted or no keys found.");
|
||||
} else {
|
||||
logger.log("\n✓ Migration completed successfully!");
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`Migration failed: ${errorMsg}`);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
|
||||
const rawArgs = process.argv.slice(2);
|
||||
const passthroughArgs = [];
|
||||
|
||||
for (let index = 0; index < rawArgs.length; index += 1) {
|
||||
const arg = rawArgs[index];
|
||||
|
||||
if (arg === "--testPathPattern") {
|
||||
const value = rawArgs[index + 1];
|
||||
if (value) {
|
||||
passthroughArgs.push(value);
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("--testPathPattern=")) {
|
||||
passthroughArgs.push(arg.slice("--testPathPattern=".length));
|
||||
continue;
|
||||
}
|
||||
|
||||
passthroughArgs.push(arg);
|
||||
}
|
||||
|
||||
const vitestBin = join(process.cwd(), "node_modules", ".bin", "vitest");
|
||||
const result = spawnSync(vitestBin, ["run", ...passthroughArgs], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
@@ -1,314 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { ActivityController } from "./activity.controller";
|
||||
import { ActivityService } from "./activity.service";
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
import type { QueryActivityLogDto } from "./dto";
|
||||
|
||||
describe("ActivityController", () => {
|
||||
let controller: ActivityController;
|
||||
let service: ActivityService;
|
||||
|
||||
const mockActivityService = {
|
||||
findAll: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
getAuditTrail: vi.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceId = "workspace-123";
|
||||
|
||||
beforeEach(() => {
|
||||
service = mockActivityService as any;
|
||||
controller = new ActivityController(service);
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
const mockPaginatedResult = {
|
||||
data: [
|
||||
{
|
||||
id: "activity-1",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "task-123",
|
||||
details: {},
|
||||
createdAt: new Date("2024-01-01"),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalPages: 1,
|
||||
},
|
||||
};
|
||||
|
||||
it("should return paginated activity logs using authenticated user's workspaceId", async () => {
|
||||
const query: QueryActivityLogDto = {
|
||||
workspaceId: "workspace-123",
|
||||
page: 1,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
mockActivityService.findAll.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
const result = await controller.findAll(query, mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockPaginatedResult);
|
||||
expect(mockActivityService.findAll).toHaveBeenCalledWith({
|
||||
...query,
|
||||
workspaceId: "workspace-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle query with filters", async () => {
|
||||
const query: QueryActivityLogDto = {
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
mockActivityService.findAll.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
await controller.findAll(query, mockWorkspaceId);
|
||||
|
||||
expect(mockActivityService.findAll).toHaveBeenCalledWith({
|
||||
...query,
|
||||
workspaceId: "workspace-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle query with date range", async () => {
|
||||
const startDate = new Date("2024-01-01");
|
||||
const endDate = new Date("2024-01-31");
|
||||
|
||||
const query: QueryActivityLogDto = {
|
||||
workspaceId: "workspace-123",
|
||||
startDate,
|
||||
endDate,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
mockActivityService.findAll.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
await controller.findAll(query, mockWorkspaceId);
|
||||
|
||||
expect(mockActivityService.findAll).toHaveBeenCalledWith({
|
||||
...query,
|
||||
workspaceId: "workspace-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should use user's workspaceId even if query provides different one", async () => {
|
||||
const query: QueryActivityLogDto = {
|
||||
workspaceId: "different-workspace",
|
||||
page: 1,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
mockActivityService.findAll.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
await controller.findAll(query, mockWorkspaceId);
|
||||
|
||||
// Should use authenticated user's workspaceId, not query's
|
||||
expect(mockActivityService.findAll).toHaveBeenCalledWith({
|
||||
...query,
|
||||
workspaceId: "workspace-123",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("findOne", () => {
|
||||
const mockActivity = {
|
||||
id: "activity-123",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "task-123",
|
||||
details: {},
|
||||
createdAt: new Date(),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
};
|
||||
|
||||
it("should return a single activity log using authenticated user's workspaceId", async () => {
|
||||
mockActivityService.findOne.mockResolvedValue(mockActivity);
|
||||
|
||||
const result = await controller.findOne("activity-123", mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockActivity);
|
||||
expect(mockActivityService.findOne).toHaveBeenCalledWith("activity-123", "workspace-123");
|
||||
});
|
||||
|
||||
it("should return null if activity not found", async () => {
|
||||
mockActivityService.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.findOne("nonexistent", mockWorkspaceId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null if workspaceId is missing (service handles gracefully)", async () => {
|
||||
mockActivityService.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.findOne("activity-123", undefined as any);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockActivityService.findOne).toHaveBeenCalledWith("activity-123", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuditTrail", () => {
|
||||
const mockAuditTrail = [
|
||||
{
|
||||
id: "activity-1",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "task-123",
|
||||
details: { title: "New Task" },
|
||||
createdAt: new Date("2024-01-01"),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "activity-2",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-456",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "task-123",
|
||||
details: { title: "Updated Task" },
|
||||
createdAt: new Date("2024-01-02"),
|
||||
user: {
|
||||
id: "user-456",
|
||||
name: "Another User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it("should return audit trail for a task using authenticated user's workspaceId", async () => {
|
||||
mockActivityService.getAuditTrail.mockResolvedValue(mockAuditTrail);
|
||||
|
||||
const result = await controller.getAuditTrail(EntityType.TASK, "task-123", mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockAuditTrail);
|
||||
expect(mockActivityService.getAuditTrail).toHaveBeenCalledWith(
|
||||
"workspace-123",
|
||||
EntityType.TASK,
|
||||
"task-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return audit trail for an event", async () => {
|
||||
const eventAuditTrail = [
|
||||
{
|
||||
id: "activity-3",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.EVENT,
|
||||
entityId: "event-123",
|
||||
details: {},
|
||||
createdAt: new Date(),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockActivityService.getAuditTrail.mockResolvedValue(eventAuditTrail);
|
||||
|
||||
const result = await controller.getAuditTrail(EntityType.EVENT, "event-123", mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(eventAuditTrail);
|
||||
expect(mockActivityService.getAuditTrail).toHaveBeenCalledWith(
|
||||
"workspace-123",
|
||||
EntityType.EVENT,
|
||||
"event-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return audit trail for a project", async () => {
|
||||
const projectAuditTrail = [
|
||||
{
|
||||
id: "activity-4",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: "project-123",
|
||||
details: {},
|
||||
createdAt: new Date(),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockActivityService.getAuditTrail.mockResolvedValue(projectAuditTrail);
|
||||
|
||||
const result = await controller.getAuditTrail(
|
||||
EntityType.PROJECT,
|
||||
"project-123",
|
||||
mockWorkspaceId
|
||||
);
|
||||
|
||||
expect(result).toEqual(projectAuditTrail);
|
||||
expect(mockActivityService.getAuditTrail).toHaveBeenCalledWith(
|
||||
"workspace-123",
|
||||
EntityType.PROJECT,
|
||||
"project-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty array if no audit trail found", async () => {
|
||||
mockActivityService.getAuditTrail.mockResolvedValue([]);
|
||||
|
||||
const result = await controller.getAuditTrail(
|
||||
EntityType.WORKSPACE,
|
||||
"workspace-999",
|
||||
mockWorkspaceId
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return empty array if workspaceId is missing (service handles gracefully)", async () => {
|
||||
mockActivityService.getAuditTrail.mockResolvedValue([]);
|
||||
|
||||
const result = await controller.getAuditTrail(EntityType.TASK, "task-123", undefined as any);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockActivityService.getAuditTrail).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
EntityType.TASK,
|
||||
"task-123"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Controller, Get, Query, Param, UseGuards } from "@nestjs/common";
|
||||
import { ActivityService } from "./activity.service";
|
||||
import { EntityType } from "@prisma/client";
|
||||
import { QueryActivityLogDto } from "./dto";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
|
||||
import { Workspace, Permission, RequirePermission } from "../common/decorators";
|
||||
|
||||
@Controller("activity")
|
||||
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
||||
export class ActivityController {
|
||||
constructor(private readonly activityService: ActivityService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async findAll(@Query() query: QueryActivityLogDto, @Workspace() workspaceId: string) {
|
||||
return this.activityService.findAll(Object.assign({}, query, { workspaceId }));
|
||||
}
|
||||
|
||||
@Get("audit/:entityType/:entityId")
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async getAuditTrail(
|
||||
@Param("entityType") entityType: EntityType,
|
||||
@Param("entityId") entityId: string,
|
||||
@Workspace() workspaceId: string
|
||||
) {
|
||||
return this.activityService.getAuditTrail(workspaceId, entityType, entityId);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async findOne(@Param("id") id: string, @Workspace() workspaceId: string) {
|
||||
return this.activityService.findOne(id, workspaceId);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ActivityController } from "./activity.controller";
|
||||
import { ActivityService } from "./activity.service";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
|
||||
/**
|
||||
* Module for activity logging and audit trail functionality
|
||||
*/
|
||||
@Module({
|
||||
imports: [PrismaModule, AuthModule],
|
||||
controllers: [ActivityController],
|
||||
providers: [ActivityService],
|
||||
exports: [ActivityService],
|
||||
})
|
||||
export class ActivityModule {}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user