Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddb1554e5b | ||
|
|
b017e66e17 | ||
|
|
172368612c | ||
|
|
1387231e57 | ||
|
|
0292392e64 | ||
|
|
594b8d711c | ||
|
|
3c1ffd2c2d | ||
|
|
4ebb123ba3 | ||
|
|
bb5cecb348 | ||
|
|
88f55d9135 | ||
|
|
e7e1bd26eb | ||
|
|
5d86b8fa93 | ||
|
|
35ea464661 | ||
|
|
e87ecdb3e5 | ||
|
|
a947db7bfd | ||
|
|
a35ea62ab1 | ||
|
|
f6c93dcb5c | ||
|
|
7f0408d417 | ||
|
|
cfd2a19bd7 | ||
|
|
d2a9e26395 | ||
|
|
0734b1f3a5 | ||
|
|
ce6420f3de | ||
|
|
81f58b15c8 | ||
|
|
0c2113f710 | ||
|
|
900a506c1f | ||
|
|
c3d29e796a | ||
|
|
c2365ae519 |
@@ -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
|
||||
+19
-501
@@ -1,506 +1,24 @@
|
||||
# ==============================================
|
||||
# Mosaic Stack Environment Configuration
|
||||
# ==============================================
|
||||
# Copy this file to .env and customize for your environment
|
||||
|
||||
# ======================
|
||||
# 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
|
||||
|
||||
# ======================
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# ======================
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# ======================
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# ======================
|
||||
# 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=
|
||||
|
||||
# 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=
|
||||
|
||||
# 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
|
||||
|
||||
# ======================
|
||||
# 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
|
||||
|
||||
# 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_API_KEY=sk-...
|
||||
|
||||
# ======================
|
||||
# Application Environment
|
||||
# ======================
|
||||
NODE_ENV=development
|
||||
|
||||
# ======================
|
||||
# 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.
|
||||
# Non-secret runtime settings for the mosaic-poc-agent container.
|
||||
# Copy to .env if you want to override the defaults in compose.yaml.
|
||||
#
|
||||
# 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
|
||||
# NEVER put credentials in this file. Authentication is supplied at
|
||||
# runtime only, via one of the two documented paths:
|
||||
# 1. read-only mounted pi auth file (default: ~/.pi/agent/auth.json,
|
||||
# override the host path with PI_AUTH_FILE)
|
||||
# 2. provider API key environment variable (ZAI_API_KEY or
|
||||
# ANTHROPIC_API_KEY), passed through by compose.yaml when set
|
||||
|
||||
# ======================
|
||||
# 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
|
||||
# Model provider (built-in pi provider name)
|
||||
PI_PROVIDER=zai
|
||||
|
||||
# Domain configuration for Traefik routing
|
||||
MOSAIC_API_DOMAIN=api.mosaic.local
|
||||
MOSAIC_WEB_DOMAIN=mosaic.local
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.local
|
||||
# Model ID within the provider
|
||||
PI_MODEL=glm-5.3-flash
|
||||
|
||||
# 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
|
||||
# Optional: alternative host path of the pi credential file mounted
|
||||
# read-only at /home/node/.pi/agent/auth.json in the container
|
||||
#PI_AUTH_FILE=/home/jwoltje/.pi/agent/auth.json
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Repository configuration
|
||||
GITEA_REPO_OWNER=mosaic
|
||||
GITEA_REPO_NAME=stack
|
||||
|
||||
# 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
|
||||
|
||||
# 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 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# Optional: documented env-var auth alternative (secret! set in your
|
||||
# shell or a gitignored .env, never commit)
|
||||
#ZAI_API_KEY=
|
||||
#ANTHROPIC_API_KEY=
|
||||
|
||||
@@ -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
-68
@@ -1,71 +1,8 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnpm-store
|
||||
# build/deps
|
||||
node_modules/
|
||||
|
||||
# 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
|
||||
# runtime credentials — never commit, never copy into the image
|
||||
.env
|
||||
.env.local
|
||||
.env.test
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.bak.*
|
||||
*.bak
|
||||
secrets/
|
||||
|
||||
# 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
|
||||
# generated runtime state lives in /home/jwoltje/.mosaic-dev (outside this project)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
# Repo Mosaic Linkage
|
||||
|
||||
This repository is attached to the machine-wide Mosaic framework.
|
||||
|
||||
## Load Order for Agents
|
||||
|
||||
1. `~/.config/mosaic/STANDARDS.md`
|
||||
2. `AGENTS.md` (this repository)
|
||||
3. `.mosaic/repo-hooks.sh` (repo-specific automation hooks)
|
||||
|
||||
## Purpose
|
||||
|
||||
- Keep universal standards in `~/.config/mosaic`
|
||||
- Keep repo-specific behavior in this repo
|
||||
- Avoid copying large runtime configs into each project
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"transport": "matrix",
|
||||
"matrix": {
|
||||
"control_room_id": "",
|
||||
"workspace_id": "",
|
||||
"homeserver_url": "",
|
||||
"access_token": "",
|
||||
"bot_user_id": ""
|
||||
},
|
||||
"worker": {
|
||||
"runtime": "codex",
|
||||
"command_template": "bash scripts/agent/orchestrator-worker.sh {task_file}",
|
||||
"timeout_seconds": 7200,
|
||||
"max_attempts": 1
|
||||
},
|
||||
"quality_gates": ["pnpm lint", "pnpm typecheck", "pnpm test"]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"mission_id": "ms22-p2-named-agent-fleet-20260304",
|
||||
"name": "MS22-P2 Named Agent Fleet",
|
||||
"description": "",
|
||||
"project_path": "/home/jwoltje/src/mosaic-stack",
|
||||
"created_at": "2026-03-05T01:53:28Z",
|
||||
"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": ""
|
||||
}
|
||||
],
|
||||
"sessions": []
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
enabled: false
|
||||
template: ""
|
||||
|
||||
# Set enabled: true and choose one template:
|
||||
# - typescript-node
|
||||
# - typescript-nextjs
|
||||
# - monorepo
|
||||
#
|
||||
# Apply manually:
|
||||
# ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target <repo>
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Repo-specific hooks used by scripts/agent/*.sh for Mosaic Stack.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
mosaic_hook_session_end() {
|
||||
echo "[mosaic-stack] Working tree summary:"
|
||||
git status --short | sed 's/^/[mosaic-stack] /' || true
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/
|
||||
supportedArchitectures[libc][]=glibc
|
||||
supportedArchitectures[cpu][]=x64
|
||||
@@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
coverage
|
||||
pnpm-lock.yaml
|
||||
@@ -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
|
||||
@@ -1,383 +0,0 @@
|
||||
# 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)
|
||||
|
||||
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
|
||||
|
||||
steps:
|
||||
# ─── Install (once) ─────────────────────────────────────────
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
|
||||
# ─── 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
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm turbo typecheck
|
||||
depends_on:
|
||||
- prisma-generate
|
||||
|
||||
# ─── Prisma Migrate (test DB) ──────────────────────────────
|
||||
prisma-migrate:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm --filter "@mosaic/api" prisma migrate deploy
|
||||
depends_on:
|
||||
- prisma-generate
|
||||
|
||||
# ─── 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
|
||||
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
|
||||
depends_on:
|
||||
- prisma-migrate
|
||||
|
||||
# ─── Build (all packages) ──────────────────────────────────
|
||||
build:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
NODE_ENV: "production"
|
||||
<<: *turbo_env
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm turbo build
|
||||
depends_on:
|
||||
- lint
|
||||
- typecheck
|
||||
- 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
|
||||
|
||||
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 +0,0 @@
|
||||
# Mosaic Stack — Agent Guidelines
|
||||
|
||||
## 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)
|
||||
|
||||
## Runtime Contract
|
||||
|
||||
- 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/`.
|
||||
|
||||
## Session Lifecycle
|
||||
|
||||
```bash
|
||||
bash scripts/agent/session-start.sh
|
||||
bash scripts/agent/critical.sh
|
||||
bash scripts/agent/session-end.sh
|
||||
```
|
||||
|
||||
Optional:
|
||||
|
||||
```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`
|
||||
@@ -0,0 +1,371 @@
|
||||
# Minimal Mosaic Stack container proof of concept
|
||||
|
||||
## Purpose
|
||||
|
||||
Build the smallest isolated container that can:
|
||||
- launch Pi
|
||||
- load a small set of Mosaic-style contract files
|
||||
- send one real request to a model
|
||||
- return a known response.
|
||||
|
||||
This is a standalone experiment. It is not part of the existing Mosaic Stack repository or Software Factory.
|
||||
|
||||
## Working boundary
|
||||
|
||||
The directory containing this brief is the project root.
|
||||
|
||||
### Do not read, copy, mount, import, or modify anything from:
|
||||
- `/home/jwoltje/.mosaic`
|
||||
- `/home/jwoltje/.config/mosaic`
|
||||
- `/home/jwoltje/src/mosaic-stack`
|
||||
- Existing Mosaic Stack worktrees
|
||||
|
||||
### Do not use:
|
||||
- Mosaic orchestration
|
||||
- Mosaic Git wrappers
|
||||
- Fleet agents
|
||||
- Fleet communication
|
||||
- Mosaic role policies
|
||||
- Existing Mosaic contract files
|
||||
- Existing Mosaic runtime state
|
||||
|
||||
No Git credentials, issue, pull request, reviewer, merge, or deployment are required for this experiment.
|
||||
|
||||
Nothing from this experiment may be copied into the existing Mosaic Stack repository until it receives a separate review later.
|
||||
|
||||
## Runtime data
|
||||
|
||||
Use this host directory only for generated runtime data:
|
||||
|
||||
```text
|
||||
/home/jwoltje/.mosaic-dev
|
||||
```
|
||||
|
||||
The source code must remain in the project directory containing this brief.
|
||||
|
||||
Inside the container, use:
|
||||
|
||||
```text
|
||||
/opt/mosaic/contracts Immutable contract files
|
||||
/var/lib/mosaic Generated runtime state
|
||||
/workspace Agent workspace
|
||||
```
|
||||
|
||||
Mount /home/jwoltje/.mosaic-dev at /var/lib/mosaic.
|
||||
|
||||
### Required proof
|
||||
|
||||
The finished experiment must prove one path:
|
||||
|
||||
1. Build one container image.
|
||||
2. Start one Pi agent inside the container.
|
||||
3. Load four local contract files from /opt/mosaic/contracts.
|
||||
4. Send a request that does not contain the expected response.
|
||||
5. Receive MOSAIC_HELLO_OK from the agent.
|
||||
6. Exit successfully when the response matches.
|
||||
7. Exit nonzero when the response does not match.
|
||||
|
||||
This is the entire required functional result.
|
||||
|
||||
### Required discovery
|
||||
|
||||
Before writing the runtime command:
|
||||
|
||||
1. Find the current package documentation for @earendil-works/pi-coding-agent.
|
||||
2. Determine the current package version.
|
||||
3. Determine the supported noninteractive command.
|
||||
4. Determine how Pi accepts a custom system prompt or system prompt file.
|
||||
5. Determine Pi's documented container authentication method.
|
||||
6. Record the commands and findings in BUILD-LOG.md.
|
||||
|
||||
Do not guess CLI flags, authentication paths, or SDK methods.
|
||||
|
||||
Pin the selected Pi package version in the project. Do not install an unversioned package during each container start.
|
||||
|
||||
Prefer the Pi CLI. Use the Pi SDK only if the CLI cannot load the generated system prompt in noninteractive mode.
|
||||
|
||||
### Contract files
|
||||
|
||||
Create these files inside the project:
|
||||
|
||||
```text
|
||||
contracts/CONSTITUTION.md
|
||||
contracts/STANDARDS.md
|
||||
contracts/SOUL.md
|
||||
contracts/USER.md
|
||||
```
|
||||
|
||||
Use these exact contents.
|
||||
|
||||
### contracts/CONSTITUTION.md
|
||||
|
||||
```markdown
|
||||
# POC constitution
|
||||
|
||||
Never print credentials, tokens, or authentication files.
|
||||
|
||||
Follow the loaded system instructions before the user request.
|
||||
```
|
||||
|
||||
### contracts/STANDARDS.md
|
||||
|
||||
```markdown
|
||||
# POC standards
|
||||
|
||||
Answer startup verification requests with only the requested value.
|
||||
Do not add explanation or formatting.
|
||||
```
|
||||
|
||||
### contracts/SOUL.md
|
||||
|
||||
```markdown
|
||||
# POC identity
|
||||
|
||||
Your name is mosaic-poc-agent.
|
||||
|
||||
Your startup marker is MOSAIC_HELLO_OK.
|
||||
|
||||
When asked for your startup marker, return only the marker.
|
||||
```
|
||||
|
||||
### contracts/USER.md
|
||||
|
||||
```markdown
|
||||
# POC user
|
||||
|
||||
This is an isolated local runtime test.
|
||||
```
|
||||
|
||||
Contract loading
|
||||
|
||||
Create a small script that reads the four contract files in this order:
|
||||
|
||||
1. CONSTITUTION.md
|
||||
2. STANDARDS.md
|
||||
3. SOUL.md
|
||||
4. USER.md
|
||||
|
||||
Join them with clear file separators.
|
||||
|
||||
Write the generated system prompt to:
|
||||
|
||||
```text
|
||||
/var/lib/mosaic/system-prompt.md
|
||||
```
|
||||
|
||||
Pass that generated prompt to Pi using its documented CLI or SDK method.
|
||||
|
||||
Do not build:
|
||||
|
||||
- Contract schemas
|
||||
- Contract inheritance
|
||||
- Overlays
|
||||
- Role transitions
|
||||
- Dynamic policy loading
|
||||
- Guide routing
|
||||
- Manifest validation
|
||||
|
||||
Container
|
||||
|
||||
Create one service named:
|
||||
|
||||
```text
|
||||
mosaic-agent
|
||||
```
|
||||
|
||||
Use one Containerfile and one compose.yaml.
|
||||
|
||||
Requirements:
|
||||
|
||||
- Use a maintained Node.js base image.
|
||||
- Run as a non-root user.
|
||||
- Install a pinned Pi package version.
|
||||
- Copy the local contract fixtures into /opt/mosaic/contracts.
|
||||
- Do not copy credentials into the image.
|
||||
- Do not mount the Docker socket.
|
||||
- Do not mount either live Mosaic directory.
|
||||
- Do not add a database, web server, queue, or second container.
|
||||
- The container may run as a one-shot command. It does not need to remain running.
|
||||
|
||||
### Authentication
|
||||
|
||||
Use Pi's documented authentication mechanism.
|
||||
|
||||
Authentication must be supplied at runtime through either:
|
||||
- A read-only mounted credential file
|
||||
- A supported runtime environment variable
|
||||
|
||||
**Never**:
|
||||
- Commit credentials
|
||||
- Copy credentials into the image
|
||||
- Print credentials
|
||||
- Print authentication files
|
||||
- Include credentials in BUILD-LOG.md
|
||||
- Store credentials under the project directory
|
||||
|
||||
Provide .env.example only for non-secret settings such as model or provider names.
|
||||
|
||||
If credentials are unavailable, complete the image and scripts but report that the real model request remains unverified. Do not fake the response.
|
||||
|
||||
### Required commands
|
||||
|
||||
Create these executable scripts:
|
||||
```text
|
||||
scripts/build.sh
|
||||
scripts/hello.sh
|
||||
scripts/verify.sh
|
||||
scripts/reset.sh
|
||||
```
|
||||
|
||||
### scripts/build.sh
|
||||
|
||||
Build the container image using Docker Compose.
|
||||
|
||||
### scripts/hello.sh
|
||||
|
||||
Run the mosaic-agent service as a one-shot container.
|
||||
|
||||
Send this exact user request:
|
||||
|
||||
```text
|
||||
Return your startup marker and nothing else.
|
||||
```
|
||||
|
||||
The request must not contain MOSAIC_HELLO_OK.
|
||||
|
||||
Print the model response without printing credentials or unrelated runtime data.
|
||||
|
||||
### scripts/verify.sh
|
||||
|
||||
Run the complete test.
|
||||
|
||||
**It must**:
|
||||
|
||||
1. Build or confirm the image is built.
|
||||
2. Run the agent request.
|
||||
3. Remove surrounding whitespace from the response.
|
||||
4. Compare the response with MOSAIC_HELLO_OK.
|
||||
5. Exit 0 only when they match exactly.
|
||||
6. Exit nonzero with a clear error when they do not match.
|
||||
|
||||
### scripts/reset.sh
|
||||
|
||||
Delete generated POC state only when all checks pass:
|
||||
1. The resolved path is exactly /home/jwoltje/.mosaic-dev.
|
||||
2. The path is not a symbolic link.
|
||||
3. The directory contains a .mosaic-poc-root ownership marker created by this project.
|
||||
|
||||
Refuse to delete anything if a check fails.
|
||||
|
||||
## Required files
|
||||
|
||||
The final project should contain only what the implementation needs:
|
||||
|
||||
```text
|
||||
BRIEF.md
|
||||
BUILD-LOG.md
|
||||
README.md
|
||||
LAYERS.md
|
||||
Containerfile
|
||||
compose.yaml
|
||||
package.json
|
||||
package-lock.json
|
||||
.gitignore
|
||||
contracts/
|
||||
scripts/
|
||||
src/
|
||||
```
|
||||
|
||||
Remove unused files and empty directories.
|
||||
|
||||
Build log
|
||||
|
||||
Create BUILD-LOG.md.
|
||||
|
||||
Treat it as append-only.
|
||||
|
||||
Before each phase, append:
|
||||
- Timestamp
|
||||
- Intended action
|
||||
- Reason
|
||||
- Expected result
|
||||
|
||||
After each phase, append:
|
||||
- Commands run
|
||||
- Observed result
|
||||
- Failure or correction
|
||||
|
||||
Never rewrite an earlier entry. Add a correction as a new entry.
|
||||
|
||||
Do not record credentials.
|
||||
|
||||
Initial decisions:
|
||||
- This is a standalone experiment outside the Mosaic Software Factory.
|
||||
- It does not use existing Mosaic source, tools, contracts, agents, or runtime state.
|
||||
- The first proof uses one Pi agent and four small local contract files.
|
||||
- The only required model result is MOSAIC_HELLO_OK.
|
||||
- Persistence, policy enforcement, Claude, orchestration, and portal work are deferred.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
The experiment passes when:
|
||||
1. scripts/build.sh exits 0.
|
||||
2. The image contains the four local contract files.
|
||||
3. The image contains no credentials.
|
||||
4. The container has no mounts from ~/.mosaic or ~/.config/mosaic.
|
||||
5. scripts/hello.sh performs a real model request.
|
||||
6. The request does not contain the expected marker.
|
||||
7. The agent returns exactly MOSAIC_HELLO_OK.
|
||||
8. scripts/verify.sh exits 0.
|
||||
9. Changing the expected value makes scripts/verify.sh exit nonzero.
|
||||
10. scripts/reset.sh refuses unsafe paths.
|
||||
11. Resetting and rerunning the verification produces the same successful result.
|
||||
|
||||
## Deferred layers
|
||||
|
||||
Document these in LAYERS.md. Do not implement them.
|
||||
|
||||
- L0: Container builds and returns MOSAIC_HELLO_OK.
|
||||
- L1: Persist and resume a named Pi session.
|
||||
- L2: Add a fixed tool permission policy.
|
||||
- L3: Load full versioned contract bundles.
|
||||
- L4: Add Claude as a second runtime.
|
||||
- L5: Add multiple agents and communication.
|
||||
- L6: Add orchestration, knowledge storage, and portal features.
|
||||
|
||||
## Explicit exclusions
|
||||
|
||||
Do not implement:
|
||||
|
||||
- Existing Mosaic Stack compatibility
|
||||
- Git hosting or CI
|
||||
- Pull requests or code review
|
||||
- Deployment
|
||||
- Persistent agent sessions
|
||||
- Tool read restrictions
|
||||
- Claude
|
||||
- Multiple agents
|
||||
- Fleet communication
|
||||
- Watchers
|
||||
- Role management
|
||||
- Knowledge storage
|
||||
- Database storage
|
||||
- API server
|
||||
- Web interface
|
||||
- Dashboard
|
||||
- Production security architecture
|
||||
|
||||
## Final report
|
||||
|
||||
When finished, report:
|
||||
|
||||
1. Files created.
|
||||
2. Pi package version.
|
||||
3. Exact build command.
|
||||
4. Exact verification command.
|
||||
5. Verification output with credentials removed.
|
||||
6. Whether the real model request passed.
|
||||
7. Any remaining failure.
|
||||
8. Anything implemented beyond this brief.
|
||||
|
||||
Do not describe the experiment as production-ready.
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
# BUILD-LOG
|
||||
|
||||
Append-only build log for the Minimal Mosaic Stack container proof of concept.
|
||||
Each phase records the plan before it runs and the observed result after it runs.
|
||||
No credentials are recorded in this file.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Pi package discovery
|
||||
|
||||
### Entry 1.1 — before
|
||||
|
||||
- Timestamp: 2026-02-02 (session start, local)
|
||||
- Intended action: Locate the current package documentation for `@earendil-works/pi-coding-agent`, determine the current version, the supported noninteractive command, the custom system prompt mechanism, and the documented container authentication method.
|
||||
- Reason: The brief forbids guessing CLI flags, authentication paths, or SDK methods; all runtime commands must be derived from the package documentation.
|
||||
- Expected result: Documented answers for all five discovery questions, recorded below, with the Pi package version pinned in the project.
|
||||
|
||||
### Entry 1.2 — after
|
||||
|
||||
- Timestamp: 2026-02-02
|
||||
- Commands run:
|
||||
- Read `README.md` of the locally installed `@earendil-works/pi-coding-agent` package
|
||||
- Read `docs/containerization.md`, excerpts of `docs/models.md` and `docs/providers.md`
|
||||
- `grep '"version"' .../pi-coding-agent/package.json`
|
||||
- `npm view @earendil-works/pi-coding-agent version`
|
||||
- Inspected host auth store structure (keys only, values never printed)
|
||||
- Observed result:
|
||||
1. **Package documentation**: full docs ship inside the installed package (`README.md` plus a `docs/` directory including `docs/containerization.md`).
|
||||
2. **Current package version**: `0.84.4` — the locally installed version and the npm registry latest are identical. Selected version to pin: `0.84.4`.
|
||||
3. **Supported noninteractive command**: `pi -p` / `pi --print` — "Print response and exit". Documented in the CLI Reference. Print mode also merges piped stdin into the initial prompt (not used here).
|
||||
4. **Custom system prompt**: documented CLI flag `--system-prompt <text>` — "Replace default prompt (context files and skills still appended)". Because the generated contract prompt must fully control behavior, the run also passes the documented discovery-off switches (`--no-context-files`, `--no-skills`, `--no-extensions`, `--no-prompt-templates`, `--no-themes`) so nothing is appended from ambient files. (`APPEND_SYSTEM.md` / `.pi/SYSTEM.md` file conventions exist but the CLI flag is the documented noninteractive path.)
|
||||
5. **Documented container authentication**: `docs/containerization.md` documents the "Plain Docker" pattern: run the whole `pi` process in the container and pass provider API keys at runtime via environment variables (example: `docker run -e ANTHROPIC_API_KEY ...`). The documented alternative credential store is `auth.json` written by `/login` under the pi agent directory (`~/.pi/agent/auth.json`).
|
||||
- Decision: supply the credential at runtime as a **read-only mounted credential file** (host `auth.json`, which contains a static API-key entry for the built-in `zai` provider) mounted at the container pi agent directory, and additionally allow the documented environment-variable path (`ZAI_API_KEY` / `ANTHROPIC_API_KEY`) as an alternative. Provider and model are non-secret settings supplied via `.env` (`PI_PROVIDER=zai`, `PI_MODEL=glm-5.3-flash`).
|
||||
- Failure or correction: none. Host check confirmed no API-key environment variables are exported on the host, so the read-only auth.json mount is the working runtime credential path for this experiment.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Project scaffold
|
||||
|
||||
### Entry 2.1 — before
|
||||
|
||||
- Timestamp: 2026-02-02
|
||||
- Intended action: Create the contract fixtures (exact brief contents), the contract loader (`src/load-contracts.sh`), the one-shot agent runner (`src/run-agent.sh`), the four required scripts (`scripts/build.sh`, `hello.sh`, `verify.sh`, `reset.sh`), `Containerfile`, `compose.yaml`, pinned `package.json` + `package-lock.json`, `.gitignore`, `README.md`, `LAYERS.md`.
|
||||
- Reason: Implement exactly the file set the brief requires, with no extra machinery (no schemas, overlays, manifests, or policy loading).
|
||||
- Expected result: A complete project whose only remaining unknown is whether the pinned image builds and the real model request returns `MOSAIC_HELLO_OK`.
|
||||
|
||||
### Entry 2.2 — after
|
||||
|
||||
- Timestamp: 2026-02-02
|
||||
- Commands run: file creation; `npm install --package-lock-only --ignore-scripts` to generate the lockfile from the pinned dependency.
|
||||
- Observed result: All files created; `package-lock.json` pins `@earendil-works/[email protected]` (exact, no range).
|
||||
- Failure or correction: none.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Container image build
|
||||
|
||||
### Entry 3.1 — before
|
||||
|
||||
- Timestamp: 2026-02-02
|
||||
- Intended action: Run `scripts/build.sh` (Docker Compose build) to produce image `mosaic-poc-agent:0.84.4` from `node:24-bookworm-slim` with the pinned Pi, the four contract fixtures at `/opt/mosaic/contracts`, and a non-root user (uid/gid 1000).
|
||||
- Reason: Phase 1 of the required proof path; `node:24-bookworm-slim` is the maintained base image used in Pi's own documented containerization example.
|
||||
- Expected result: `docker compose build` exits 0 and the image contains the contracts, the runner scripts, and the pinned `pi` binary, with no credentials baked in.
|
||||
|
||||
### Entry 3.2 — after
|
||||
|
||||
- Timestamp: 2026-02-02
|
||||
- Commands run: `scripts/build.sh`; `docker run --rm mosaic-poc-agent:0.84.4 --version`; `id` via `--entrypoint`; contract listing; credential file scan.
|
||||
- Observed result:
|
||||
- Build exit 0; image tagged `mosaic-poc-agent:0.84.4`.
|
||||
- `pi --version` inside the image reports `0.84.4` (and this run also executed the contract loader successfully, writing `/var/lib/mosaic/system-prompt.md`).
|
||||
- Container user is `uid=1000(node) gid=1000(node)` — non-root.
|
||||
- All four contract files present at `/opt/mosaic/contracts` with read-only permissions (0555).
|
||||
- Credential scan: no `auth.json` or other auth files exist in the image; `/home/node/.pi/agent/` is empty in the image.
|
||||
- Failure or correction:
|
||||
1. First build failed: Docker Compose expects `Dockerfile` by default; fixed by setting `build.dockerfile: Containerfile` in `compose.yaml`.
|
||||
2. Second build failed: `useradd` exit 4 (uid 1000 already exists) because the maintained node image ships a `node` user at uid/gid 1000. Fixed by reusing the built-in `node` user (same 1000:1000 host mapping) instead of creating a duplicate `mosaic` user; container paths updated from `/home/mosaic/...` to `/home/node/...` in `Containerfile`, `compose.yaml`, `README.md`, `.env.example`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Runtime verification
|
||||
|
||||
### Entry 4.1 — before
|
||||
|
||||
- Timestamp: 2026-02-02
|
||||
- Intended action: Run `scripts/hello.sh` (one-shot request: "Return your startup marker and nothing else."), then `scripts/verify.sh` (exact-match gate against `MOSAIC_HELLO_OK`), then the negative test (`EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh` must exit nonzero), then the `scripts/reset.sh` safety tests and a final rerun after reset.
|
||||
- Reason: Phases 2–7 of the required proof path plus acceptance criteria 5–11.
|
||||
- Expected result: hello prints only the marker; verify exits 0; negative test exits nonzero; reset refuses unsafe paths and succeeds on the real path; rerun after reset reproduces the success.
|
||||
|
||||
### Entry 4.2 — after
|
||||
|
||||
- Timestamp: 2026-02-02
|
||||
- Commands run: `scripts/hello.sh`; `scripts/verify.sh`; `EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh`; `scripts/reset.sh` (refusal tests: missing marker, symlink with canary file, then real reset, then missing dir); `scripts/build.sh && scripts/verify.sh` after reset; `docker compose config` mount inspection.
|
||||
- Observed result:
|
||||
- `hello.sh`: stdout exactly `MOSAIC_HELLO_OK` — a real model request (provider `zai`, model `glm-5.3-flash`, auth via the read-only mounted auth.json credential file). The request string contains no marker.
|
||||
- `verify.sh`: `PASS: response matches expected marker`, exit 0.
|
||||
- Negative test: `FAIL: response does not match expected marker` (expected `MOSAIC_NOT_OK`, actual `MOSAIC_HELLO_OK`), exit 1.
|
||||
- `reset.sh` refusal tests: missing marker → exit 1, nothing deleted; symlink (with canary file at the target) → exit 1, canary survived; real path with marker → removed, exit 0; missing dir → "nothing to remove", exit 0.
|
||||
- Rerun after reset: build + verify → PASS, exit 0 (criterion 11).
|
||||
- Resolved compose mounts: only `/home/jwoltje/.mosaic-dev → /var/lib/mosaic` (rw) and `~/.pi/agent/auth.json → /home/node/.pi/agent/auth.json` (read-only). No `~/.mosaic` or `~/.config/mosaic` mounts, no Docker socket.
|
||||
- Failure or correction:
|
||||
1. First hello run: the contract loader's status line was printed on stdout, mixing runtime data into the model response stream and contaminating the exact-match capture. Fixed by sending the loader's status message to stderr (`src/load-contracts.sh`), rebuilt the image, reran: stdout is exactly the model response.
|
||||
- Credential check: no credential material appears in this log, in hello/verify output, or in the image (image scan found no auth files).
|
||||
|
||||
## Result
|
||||
|
||||
All 11 acceptance criteria demonstrated. The real model request passed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Configuration-driven Hello World (M1)
|
||||
|
||||
### Entry 5.1 — before
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Intended action: Make the container POC configuration-driven. Baseline committed and tagged `poc-container-hello-v0`. Milestone M1 tracked in Gitea (issues #1-#4): (T1) config module with idempotent bootstrap and strict v1 validation; (T2) wire scripts and compose to config.json with fail-closed behavior; (T3) sandboxed config selftests; (T4) E2E verification and documentation.
|
||||
- Reason: Per docs/plans/2026-09-02_atomic-mosaic-foundation.md — config.json must be the sole discovery entry point; updates and runs must never corrupt or invent configuration.
|
||||
- Expected result: All M1 acceptance criteria pass; Hello World reproducible from configuration alone.
|
||||
|
||||
### Entry 5.2 — after
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Commands run: `scripts/test-config.sh` (20 cases); fail-closed checks (compose without launcher env, verify/reset with missing config); `scripts/bootstrap.sh`; config-driven `scripts/hello.sh`, `scripts/verify.sh`, negative marker test, sandboxed reset symlink refusal (canary survived), real reset + bootstrap + build + verify; config checksum comparison across the entire flow.
|
||||
- Observed result:
|
||||
- Config selftests: 20 passed, 0 failed.
|
||||
- Fail-closed confirmed: compose exits 1 without launcher env; verify/reset exit 1 on missing config before any mutation.
|
||||
- Bootstrap created `~/.config/mosaic-dev/config.json` exclusively; second run validated without rewriting (content + mtime unchanged).
|
||||
- Config-driven hello/verify returned exactly `MOSAIC_HELLO_OK`; verify exit 0; negative marker test exit 1.
|
||||
- Reset refused symlinked dataRoot; canary file survived; real reset removed only the configured data root.
|
||||
- config.json checksum unchanged across hello/verify/reset/bootstrap/build/verify.
|
||||
- Failure or correction:
|
||||
1. Selftest harness bug: `cfg` helper invoked without a body for the symlink case (`$2: unbound variable`). Fixed in the harness; product code unaffected.
|
||||
2. E2E rerun-after-reset failure: `verify.sh` did not ensure the configured data root existed before the container mount. With the data root absent, Docker auto-created the host path as root:root, and the container's uid-1000 user could not write the generated system prompt. Fixed by calling `bootstrap_runtime_dir` in `verify.sh`; also hardened it to fail with a clear message when the data root exists but is not writable (root-owned leftover). Clean-slate E2E rerun: all steps green.
|
||||
- Credential check: no credential material in config, scripts, logs, or test output.
|
||||
|
||||
## Result (M1)
|
||||
|
||||
Configuration-driven Hello World verified. `main` merged with M1 and tagged `config-hello-v1`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Mission and task abstraction (M2)
|
||||
|
||||
### Entry 6.1 — before
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Intended action: Add the first mission/task layer, host-side only (Gitea milestone M2, issues #6-#9): strict v1 schemas for missions and tasks, a task runner executing through the proven config-driven container path, immutable write-once run records under `<dataRoot>/runs/`, `expectExact` gating, timeouts, selftests, fixtures, and docs.
|
||||
- Reason: The foundation plan's following layer — mission (objective + directives), task (bounded unit), run (one attempt), result (immutable evidence) — must exist as data and records before any policy or multi-agent work.
|
||||
- Expected result: `scripts/run-task.sh tasks/hello-marker.json` succeeds with exactly `MOSAIC_HELLO_OK`; wrong expectations fail; every run leaves an immutable record; configuration remains untouched.
|
||||
|
||||
### Entry 6.2 — after
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Commands run: `scripts/test-task.sh` (18 cases incl. live runs); `scripts/run-task.sh validate` / `run` on committed fixtures; `node scripts/mosaic-task.mjs list`; config checksum comparison across runs.
|
||||
- Observed result:
|
||||
- Selftests: 18 passed, 0 failed (schema negatives; live exact-marker success; wrong expectExact fails; distinct run dirs; result.json contents; list).
|
||||
- Fixture run: status `succeeded`, response exactly `MOSAIC_HELLO_OK`, mission snapshot recorded.
|
||||
- Run records written once under `<dataRoot>/runs/r-<utcstamp>-<rand>/`; reruns never clobber.
|
||||
- Failure or correction: none this phase.
|
||||
- Credential check: no credential material in task data, run records, or logs.
|
||||
|
||||
## Result (M2)
|
||||
|
||||
Mission/task layer verified end-to-end. `main` merged with M2 and tagged `mission-task-v1`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Release model and safe updates (M3)
|
||||
|
||||
### Entry 7.1 — before
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Intended action: Add the release substrate (Gitea milestone M3, issues #10-#13): RELEASE file single-sources the version (0.0.X line per owner direction), image tags derive from it, scripts/release.sh provides package/activate/rollback/status, activation is health-gated by the M2 task runner, pointer + append-only log under <dataRoot>/state/.
|
||||
- Reason: The owner's top invariant — updates must never corrupt a working installation — needs a mechanism, not a convention: gate-then-flip with recorded history and rollback.
|
||||
- Expected result: Update, refusal, and rollback drills all green with config checksums unchanged.
|
||||
|
||||
### Entry 7.2 — after
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Commands run: scripts/test-release.sh (14 cases); recorded drills: update (0.0.3 -> 0.0.4 package+activate+verify), fault-injected refusal, rollback to 0.0.3.
|
||||
- Observed result:
|
||||
- Selftests: 14 passed, 0 failed.
|
||||
- Update drill: packaged and activated r0.0.4 after exact-marker health gate; verify green under the new tag; config checksum unchanged.
|
||||
- Refusal drill: health-gate fault injection -> activation refused (exit 1), pointer untouched, refusal appended to the log.
|
||||
- Rollback drill: health-gated rollback to r0.0.3; pointer restored; log records package/activate/refused/rollback history append-only.
|
||||
- Failure or correction:
|
||||
1. release.sh initially failed with missing state/ directory (no mkdir before pointer/log writes); fixed.
|
||||
2. Selftest harness mutated the repo RELEASE and restored the mutated copy (mv-back bug) plus a second trap replacing the first; fixed with inline backup restore and one self-healing exit trap. Product code unaffected.
|
||||
- Credential check: no credential material in release state, logs, or drills.
|
||||
|
||||
## Result (M3)
|
||||
|
||||
Release model and safe updates verified by drills. `main` merged with M3 and tagged `release-model-v1`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Runtime adapter seam (M4)
|
||||
|
||||
### Entry 8.1 — before
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Intended action: Formalize the harness boundary (Gitea milestone M4, issues #16-#19): documented adapter contract under /opt/mosaic/adapters/<name>/adapter.sh; run-agent.sh becomes a dispatcher; pi extracted unchanged; deterministic mock adapter for provider-free seam tests; config gains optional execution.adapter (default pi, configVersion unchanged); mission directives gain their sanctioned injection point via the run snapshot; RELEASE bumps to 0.0.5 with a health-gated activation.
|
||||
- Reason: Future harnesses (Claude, Codex, OpenCode) must be additive — one directory each — and mission content needs a single sanctioned path into the runtime.
|
||||
- Expected result: All suites green including new deterministic seam cases; 0.0.5 activated by health gate; mission-bearing run recorded.
|
||||
|
||||
### Entry 8.2 — after
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Commands run: scripts/test-config.sh; scripts/test-task.sh; scripts/test-release.sh; manual seam drills (mock verbatim, unknown/traversal adapter refusal); mission injection checks; release package + activate for 0.0.5.
|
||||
- Observed result:
|
||||
- Config suite 24/24 (adapter default/validation/env export).
|
||||
- Task suite 24/24 including deterministic mock cases (gate pass, expect-mismatch with reason, unknown adapter fail-closed) and mission injection asserted by prompt content.
|
||||
- Release suite 14/14; image mosaic-poc-agent:0.84.4-r0.0.5 packaged and activated via exact-marker health gate.
|
||||
- Mission directives now flow: task -> run snapshot -> container env -> generated prompt MISSION (runtime) section.
|
||||
- Failure or correction:
|
||||
1. Selection authority settled: load_config always exports MOSAIC_ADAPTER from config; environment overrides for scripts are therefore not a supported selection path (by design).
|
||||
2. Selftest harness: three authoring defects fixed (helpers used before definition; one config file reused across cases leaking adapter state; a static mission fixture asserted against distinctive seam directives; plus an accidentally duplicated live block removed).
|
||||
- Credential check: no credential material in adapters, prompts, run records, or logs.
|
||||
|
||||
## Result (M4)
|
||||
|
||||
Adapter seam verified; harness boundary is now additive by construction. `main` merged with M4 and tagged `adapter-seam-v1`.
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# CLAUDE Compatibility Pointer
|
||||
|
||||
This file exists so Claude Code sessions load Mosaic standards.
|
||||
|
||||
## MANDATORY — Read Before Any Response
|
||||
|
||||
BEFORE responding to any user message, READ `~/.config/mosaic/AGENTS.md`.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Minimal Mosaic Stack POC agent image.
|
||||
# Base: maintained Node.js image (same family as Pi's documented
|
||||
# containerization example in docs/containerization.md).
|
||||
FROM node:24-bookworm-slim
|
||||
|
||||
# Tools Pi's documented container image expects (bash, CA certs, git, ripgrep).
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Non-root user: the maintained node image ships a 'node' user at
|
||||
# uid/gid 1000, which matches the host user that owns the runtime
|
||||
# state directory mounted at /var/lib/mosaic. It is reused as-is.
|
||||
|
||||
# Pinned Pi install: package.json pins the exact version and
|
||||
# package-lock.json is installed with npm ci. No unversioned installs.
|
||||
WORKDIR /opt/app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
# Immutable contract fixtures (required location), runtime scripts, and
|
||||
# runtime adapters.
|
||||
COPY contracts /opt/mosaic/contracts
|
||||
COPY src /opt/mosaic/src
|
||||
COPY adapters /opt/mosaic/adapters
|
||||
RUN chmod 0555 /opt/mosaic/contracts /opt/mosaic/contracts/* \
|
||||
&& chmod 0555 /opt/mosaic/src /opt/mosaic/src/*.sh \
|
||||
&& chmod 0555 /opt/mosaic/adapters /opt/mosaic/adapters/*/adapter.sh
|
||||
|
||||
# Writable state, workspace, and pi agent directory (auth.json is
|
||||
# bind-mounted read-only at runtime; nothing is copied into the image).
|
||||
RUN mkdir -p /var/lib/mosaic /workspace /home/node/.pi/agent \
|
||||
&& chown -R node:node /var/lib/mosaic /workspace /home/node /opt/app
|
||||
|
||||
USER node
|
||||
WORKDIR /workspace
|
||||
ENV HOME=/home/node \
|
||||
PATH="/opt/app/node_modules/.bin:${PATH}" \
|
||||
PI_OFFLINE=1
|
||||
|
||||
# One-shot agent: args form the user request (default is the startup
|
||||
# verification request defined in compose.yaml).
|
||||
ENTRYPOINT ["/opt/mosaic/src/run-agent.sh"]
|
||||
@@ -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)
|
||||
@@ -0,0 +1,50 @@
|
||||
# LAYERS
|
||||
|
||||
Deferred capability layers for the Mosaic experiment. Only L0 is implemented by
|
||||
this proof of concept; everything below it is documented here and deliberately
|
||||
not implemented (see BRIEF.md, "Explicit exclusions").
|
||||
|
||||
## L0 — Implemented: container returns MOSAIC_HELLO_OK
|
||||
|
||||
One image (`mosaic-poc-agent:0.84.4`, built on `node:24-bookworm-slim`, non-root,
|
||||
pinned Pi) runs one Pi agent one-shot. Four immutable local contract files are
|
||||
loaded in fixed order into the generated system prompt
|
||||
(`/var/lib/mosaic/system-prompt.md`). One real model request is sent
|
||||
noninteractively; the response must equal `MOSAIC_HELLO_OK` exactly or the
|
||||
verification exits nonzero. Authentication is supplied at runtime only
|
||||
(read-only mounted pi auth file, or a provider API key environment variable).
|
||||
|
||||
## L1 — Deferred: persist and resume a named Pi session
|
||||
|
||||
Keep a named Pi session across container runs (`--name`, session storage under
|
||||
`/var/lib/mosaic`), resume it with the documented session flags, and verify
|
||||
state survives a container restart.
|
||||
|
||||
## L2 — Deferred: fixed tool permission policy
|
||||
|
||||
Add a fixed allow/deny policy for Pi tools (e.g. restricting built-in tools via
|
||||
documented `--tools` / `--exclude-tools` or an extension-based permission gate),
|
||||
so contract files can constrain what the agent may do, not just what it says.
|
||||
|
||||
## L3 — Deferred: load full versioned contract bundles
|
||||
|
||||
Replace the four static fixtures with versioned contract bundles: bundle
|
||||
manifests, contract versions, and deterministic ordering/hashing, loaded from
|
||||
an immutable bundle artifact instead of files copied at image build time.
|
||||
|
||||
## L4 — Deferred: Claude as a second runtime
|
||||
|
||||
Add a second runtime (Claude) alongside the Pi agent in the same container
|
||||
stack, behind the same contract-loading path, to compare behavior across
|
||||
runtimes.
|
||||
|
||||
## L5 — Deferred: multiple agents and communication
|
||||
|
||||
Run several named agents with defined roles and a communication channel between
|
||||
them (message passing or shared state under `/var/lib/mosaic`).
|
||||
|
||||
## L6 — Deferred: orchestration, knowledge storage, and portal features
|
||||
|
||||
Fleet-level orchestration, knowledge storage, monitoring, and portal UI on top
|
||||
of L1-L5. This is where the existing Mosaic Stack concepts would be re-evaluated
|
||||
from first principles.
|
||||
@@ -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 +1,191 @@
|
||||
# Mosaic Stack
|
||||
# Minimal Mosaic Stack container POC
|
||||
|
||||
Multi-tenant personal assistant platform with PostgreSQL backend, Authentik SSO, and MoltBot integration.
|
||||
Standalone experiment, not part of the Mosaic Stack repository or Software Factory.
|
||||
|
||||
## Overview
|
||||
One container image runs one Pi coding agent with four immutable local contract
|
||||
files as its system prompt, sends exactly one real model request, and is verified
|
||||
to return exactly `MOSAIC_HELLO_OK`.
|
||||
|
||||
Mosaic Stack is a modern, PDA-friendly platform designed to help users manage their personal and professional lives with:
|
||||
## Layout
|
||||
|
||||
- **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
|
||||
```text
|
||||
BRIEF.md requirements for the original container proof
|
||||
BUILD-LOG.md append-only build/verification log
|
||||
LAYERS.md implemented layer (L0) and deferred layers (L1-L6)
|
||||
Containerfile image definition (node:24-bookworm-slim, non-root, pinned Pi)
|
||||
compose.yaml one service: mosaic-agent (one-shot; configured via env)
|
||||
package.json pins @earendil-works/pi-coding-agent at exactly 0.84.4
|
||||
package-lock.json resolved lockfile used by npm ci in the image
|
||||
.env.example non-secret settings only (credential-file path, env-var auth)
|
||||
contracts/ CONSTITUTION.md, STANDARDS.md, SOUL.md, USER.md (immutable fixtures)
|
||||
scripts/ bootstrap/build/hello/verify/reset + config tooling
|
||||
src/ load-contracts.sh, run-agent.sh (run inside the container)
|
||||
docs/plans/ architecture and milestone plans
|
||||
```
|
||||
|
||||
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:
|
||||
The sole discovery entry point is:
|
||||
|
||||
```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
|
||||
```text
|
||||
~/.config/mosaic-dev/config.json
|
||||
```
|
||||
|
||||
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:
|
||||
Created only by the explicit, idempotent bootstrap:
|
||||
|
||||
```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)
|
||||
scripts/bootstrap.sh # create-if-absent; validates existing config, never rewrites
|
||||
```
|
||||
|
||||
### 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:**
|
||||
Minimal shape (`configVersion` 1):
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"stats": {
|
||||
"hits": 1250,
|
||||
"misses": 180,
|
||||
"sets": 195,
|
||||
"deletes": 15,
|
||||
"hitRate": 0.874
|
||||
"configVersion": 1,
|
||||
"environment": "development",
|
||||
"dataRoot": "/home/jwoltje/.mosaic-dev",
|
||||
"execution": {
|
||||
"backend": "docker",
|
||||
"provider": "zai",
|
||||
"model": "glm-5.3-flash"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Benefits
|
||||
Rules enforced by `scripts/mosaic-config.mjs`:
|
||||
|
||||
- **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)
|
||||
- Unknown keys, unsupported versions/backends, and malformed JSON exit nonzero; nothing is modified.
|
||||
- `dataRoot` must be absolute, canonical, and must not be or contain the home or configuration directory.
|
||||
- Validation failures never touch config, state, or images.
|
||||
- `scripts/test-config.sh` runs the sandboxed config selftests (no Docker required).
|
||||
|
||||
Cache hit rates typically stabilize at 70-90% for active workspaces.
|
||||
Run paths (`build/hello/verify/reset`) fail closed when configuration is missing or invalid; they never invent it.
|
||||
|
||||
## Type Sharing
|
||||
## Missions & tasks (M2)
|
||||
|
||||
Types used by both frontend and backend live in `@mosaic/shared`:
|
||||
Missions and tasks are validated JSON data (strict schemas, version-pinned). The M2 layer is host-side only: mission directives are recorded for provenance but do not yet reach the runtime system prompt (capability/policy layer comes later).
|
||||
|
||||
```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;
|
||||
}
|
||||
```text
|
||||
missions/hello.json objective + directives (missionVersion 1)
|
||||
tasks/hello-marker.json prompt + optional mission ref + expectExact + timeout
|
||||
<dataRoot>/runs/r-<id>/ immutable run record: task.json, mission.json,
|
||||
stderr.txt, result.json (all write-once)
|
||||
```
|
||||
|
||||
See [Type Sharing Strategy](docs/2-development/3-type-sharing/1-strategy.md) for the complete type sharing strategy.
|
||||
Usage:
|
||||
|
||||
## 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
|
||||
```bash
|
||||
scripts/run-task.sh validate tasks/hello-marker.json # strict validation, writes nothing
|
||||
scripts/run-task.sh run tasks/hello-marker.json # execute; result recorded under dataRoot/runs
|
||||
scripts/mosaic-task.mjs list # list runs and statuses
|
||||
scripts/test-task.sh # selftests (schema negatives + live runs)
|
||||
```
|
||||
|
||||
**Types:** `feat`, `fix`, `docs`, `test`, `refactor`, `chore`
|
||||
A run exits 0 only when its expectation is met (`expectExact` match); mismatches, nonzero agent exits, and timeouts record `status: failed` in `result.json` and exit 1. Each run gets a unique directory — rerunning never rewrites history.
|
||||
|
||||
## Testing Requirements
|
||||
## Release model (M3)
|
||||
|
||||
- **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
|
||||
`RELEASE` single-sources the release version (0.0.X until declared stable); the image tag derives from it plus the pinned Pi version. Activation is health-gated and every event is recorded:
|
||||
|
||||
## Documentation
|
||||
```bash
|
||||
scripts/release.sh package # build + tag the release image
|
||||
scripts/release.sh activate # health check (exact marker) -> atomic pointer swap
|
||||
scripts/release.sh activate --fault-injection # prove the refusal path (drills only)
|
||||
scripts/release.sh rollback # health-gated return to the previous release
|
||||
scripts/release.sh status # release, tag, active pointer, recent log
|
||||
scripts/test-release.sh # release selftests
|
||||
```
|
||||
|
||||
Complete documentation is organized in a Bookstack-compatible structure in the `docs/` directory.
|
||||
- `<dataRoot>/state/active.json` — the activation pointer (atomic tmp+rename replace)
|
||||
- `<dataRoot>/state/activation-log.jsonl` — append-only history: package / activate / refused / rollback
|
||||
|
||||
### 📚 Getting Started
|
||||
A failed health check never activates; the previously active release remains deployed. Updating the software therefore cannot corrupt the running installation: package beside, gate, then flip. Verified by the update/refusal/rollback drills in BUILD-LOG Phase 7.
|
||||
|
||||
- **[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
|
||||
## Runtime adapters (M4)
|
||||
|
||||
### 💻 Development
|
||||
The harness boundary is formalized: everything upstream (config, contracts, missions, tasks, run records) is harness-agnostic; everything inside an adapter belongs to one runtime.
|
||||
|
||||
- **[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
|
||||
```text
|
||||
adapters/<name>/adapter.sh env in: MOSAIC_SYSTEM_PROMPT_FILE, MOSAIC_REQUEST,
|
||||
MOSAIC_PROVIDER, MOSAIC_MODEL
|
||||
stdout: response only; stderr: diagnostics
|
||||
```
|
||||
|
||||
### 🏗️ Architecture
|
||||
- Selection: `execution.adapter` in config.json (optional; `pi` default; allowlist `pi`, `mock`)
|
||||
- `pi` — pinned Pi CLI, noninteractive print mode, ambient discovery off
|
||||
- `mock` — deterministic test adapter; never for real verification
|
||||
- Mission directives have a sanctioned injection point: when a task references a mission, the task runner mounts the run snapshot and the generated prompt gains a `MISSION (runtime)` section (objective + directives) after the four immutable contracts
|
||||
- Adding a harness (Claude, Codex, OpenCode) later means adding one directory — no orchestrator changes
|
||||
|
||||
- **[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
|
||||
See `adapters/README.md` for the full contract.
|
||||
|
||||
### 🔌 API Reference
|
||||
See `docs/plans/2026-09-02_atomic-mosaic-foundation.md` for the full plan.
|
||||
|
||||
- **[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
|
||||
Inside the container:
|
||||
|
||||
**Browse all documentation:** [docs/](docs/)
|
||||
```text
|
||||
/opt/mosaic/contracts immutable contract files
|
||||
/var/lib/mosaic generated runtime state (mounted from configured dataRoot)
|
||||
/workspace agent workspace
|
||||
```
|
||||
|
||||
## Related Projects
|
||||
## How it works
|
||||
|
||||
- **jarvis-brain** — Original JSON-based personal assistant (migration source)
|
||||
- **MoltBot** — Stock messaging gateway for multi-platform integration
|
||||
1. `scripts/build.sh` builds the release image (`mosaic-poc-agent:<pi>-r<release>`,
|
||||
tag derived from `RELEASE` + the pinned Pi version) with Docker Compose.
|
||||
2. On each run, `/opt/mosaic/src/load-contracts.sh` reads the four contract files
|
||||
in fixed order (CONSTITUTION, STANDARDS, SOUL, USER), joins them with clear
|
||||
separators, and writes `/var/lib/mosaic/system-prompt.md`.
|
||||
3. `/opt/mosaic/src/run-agent.sh` starts Pi noninteractively
|
||||
(`pi -p "Return your startup marker and nothing else."`) with
|
||||
`--system-prompt "$(cat /var/lib/mosaic/system-prompt.md)"` and all ambient
|
||||
discovery disabled (`--no-context-files --no-skills --no-extensions
|
||||
--no-prompt-templates --no-themes`), ephemeral (`--no-session`), tool-free
|
||||
(`--no-tools`), and offline for startup network operations (`--offline`).
|
||||
4. `scripts/verify.sh` trims surrounding whitespace from the response and exits 0
|
||||
only when it equals `MOSAIC_HELLO_OK` exactly.
|
||||
|
||||
## Security
|
||||
## Usage
|
||||
|
||||
- **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
|
||||
```bash
|
||||
scripts/bootstrap.sh # create config.json if absent (idempotent)
|
||||
scripts/build.sh # build the image
|
||||
scripts/hello.sh # one-shot request; prints the model response
|
||||
scripts/verify.sh # full gated test; exit 0 only on exact MOSAIC_HELLO_OK
|
||||
scripts/run-task.sh # run a mission/task file (see Missions & tasks)
|
||||
scripts/release.sh # package / activate / rollback / status (see Release model)
|
||||
scripts/test-config.sh # fast config-layer selftests (no Docker)
|
||||
scripts/test-task.sh # mission/task selftests (schema + adapter seam + live runs)
|
||||
scripts/test-release.sh # release selftests
|
||||
scripts/reset.sh # delete the configured data root (safety-checked)
|
||||
```
|
||||
|
||||
## License
|
||||
Prove the failure path (acceptance criterion 9):
|
||||
|
||||
[Add license information]
|
||||
```bash
|
||||
EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh # must exit nonzero
|
||||
```
|
||||
|
||||
## Support
|
||||
## Authentication
|
||||
|
||||
- **Issues:** https://git.mosaicstack.dev/mosaic/stack/issues
|
||||
- **Documentation:** https://git.mosaicstack.dev/mosaic/stack/wiki
|
||||
Pi's documented container authentication (see the package's
|
||||
`docs/containerization.md`) is used, in this order:
|
||||
|
||||
---
|
||||
1. **Read-only mounted credential file** (default): the host pi auth file
|
||||
`~/.pi/agent/auth.json` is bind-mounted read-only to
|
||||
`/home/node/.pi/agent/auth.json`. The host file holds a static API-key
|
||||
entry for the built-in `zai` provider, so no token refresh writes are needed.
|
||||
2. **Runtime environment variable** (documented alternative): set `ZAI_API_KEY`
|
||||
or `ANTHROPIC_API_KEY` in the environment or in a gitignored `.env`; compose
|
||||
passes them through. Pi's documented precedence applies.
|
||||
|
||||
**Mosaic Stack v0.0.1** — Building the future of personal assistants.
|
||||
Credentials are never committed, never copied into the image, and never printed.
|
||||
`.env.example` contains non-secret settings only.
|
||||
|
||||
## Boundaries honored
|
||||
|
||||
- No mounts of `~/.mosaic` or `~/.config/mosaic`; no Docker socket mount.
|
||||
- Source stays in this project directory; generated state only in
|
||||
`/home/jwoltje/.mosaic-dev` (host) and `/var/lib/mosaic` (container).
|
||||
- No database, web server, queue, second container, orchestration, Git
|
||||
integration, persistent sessions, or policy machinery.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Mosaic runtime adapters
|
||||
|
||||
An adapter is the entire harness-specific surface of the system. Everything
|
||||
upstream of an adapter — configuration, contracts, missions, tasks, run
|
||||
records — is harness-agnostic; everything inside an adapter may assume one
|
||||
specific agent runtime.
|
||||
|
||||
## Contract
|
||||
|
||||
An adapter lives at:
|
||||
|
||||
```text
|
||||
/opt/mosaic/adapters/<name>/adapter.sh
|
||||
```
|
||||
|
||||
and must be executable. The dispatcher (`/opt/mosaic/src/run-agent.sh`)
|
||||
selects it via `MOSAIC_ADAPTER` (default: `pi`) and execs it after the
|
||||
system prompt has been generated.
|
||||
|
||||
**Inputs (environment):**
|
||||
|
||||
| Variable | Meaning |
|
||||
|---|---|
|
||||
| `MOSAIC_SYSTEM_PROMPT_FILE` | Absolute path to the generated system prompt (contracts + optional mission section). Read it; do not modify it. |
|
||||
| `MOSAIC_REQUEST` | The exact user request text (may contain newlines). |
|
||||
| `MOSAIC_PROVIDER` | Configured provider name. |
|
||||
| `MOSAIC_MODEL` | Configured model id. |
|
||||
|
||||
Optional, adapter-specific (documented per adapter):
|
||||
|
||||
| Variable | Meaning |
|
||||
|---|---|
|
||||
| `MOSAIC_MOCK_RESPONSE` | mock only: the verbatim response to emit |
|
||||
|
||||
**Outputs:**
|
||||
|
||||
- `stdout`: the model response text — the only channel the orchestrator captures
|
||||
- `stderr`: diagnostics (never credentials)
|
||||
- exit `0`: success; nonzero: failure
|
||||
|
||||
## Rules
|
||||
|
||||
1. Adapters print ONLY the response on stdout. Status lines go to stderr.
|
||||
2. Adapters never read configuration files; the resolved settings arrive via environment.
|
||||
3. Adapters never write outside `/var/lib/mosaic`.
|
||||
4. Adding an adapter requires: a new directory, the contract implementation, and
|
||||
adding the name to the allowlist in `scripts/mosaic-config.mjs`.
|
||||
|
||||
## Included adapters
|
||||
|
||||
- `pi` — the pinned `@earendil-works/pi-coding-agent` CLI in noninteractive
|
||||
print mode (`-p`), ambient discovery disabled, stdin detached.
|
||||
- `mock` — deterministic echo of `MOSAIC_MOCK_RESPONSE`. Test-only: never use
|
||||
it where a real model response is required.
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Mock adapter: deterministic response for seam tests. NEVER use where a
|
||||
# real model response is required.
|
||||
#
|
||||
# Contract: see /opt/mosaic/adapters/README.md.
|
||||
set -eu
|
||||
|
||||
[ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "mock adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&2; exit 2; }
|
||||
[ -n "${MOSAIC_REQUEST:-}" ] || { echo "mock adapter: MOSAIC_REQUEST is required" >&2; exit 2; }
|
||||
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "mock adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
|
||||
|
||||
echo "mock adapter: responding verbatim from MOSAIC_MOCK_RESPONSE" >&2
|
||||
# Deterministic plumbing evidence: which MOSAIC_* variables did the
|
||||
# orchestrator actually deliver? (Auth secrets are not MOSAIC_-prefixed.)
|
||||
(env | grep '^MOSAIC_' | sort) >&2 2>/dev/null || true
|
||||
printf '%s\n' "${MOSAIC_MOCK_RESPONSE:-}"
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
# Pi adapter: implements the Mosaic adapter contract for the pinned
|
||||
# @earendil-works/pi-coding-agent CLI.
|
||||
#
|
||||
# Contract: see /opt/mosaic/adapters/README.md. stdout = response only.
|
||||
set -eu
|
||||
|
||||
[ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "pi adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&2; exit 2; }
|
||||
[ -n "${MOSAIC_REQUEST:-}" ] || { echo "pi adapter: MOSAIC_REQUEST is required" >&2; exit 2; }
|
||||
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "pi adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
|
||||
|
||||
: "${PI_PROVIDER:?pi adapter: PI_PROVIDER is required}"
|
||||
: "${PI_MODEL:?pi adapter: PI_MODEL is required}"
|
||||
|
||||
# Workspace (M5): run inside the provided workspace when present.
|
||||
if [ -n "${MOSAIC_WORKSPACE:-}" ]; then
|
||||
mkdir -p "$MOSAIC_WORKSPACE"
|
||||
cd "$MOSAIC_WORKSPACE"
|
||||
fi
|
||||
|
||||
# Capabilities (M5): explicit allowlist or no tools.
|
||||
TOOLS_FLAG="--no-tools"
|
||||
[ -n "${MOSAIC_TOOLS:-}" ] && TOOLS_FLAG="--tools $MOSAIC_TOOLS"
|
||||
|
||||
# All flags documented in the pi package README (CLI Reference):
|
||||
# -p/--print noninteractive: print the response and exit
|
||||
# --system-prompt replace the default prompt with the generated one
|
||||
# --no-* no ambient context/skills/extensions/templates/themes
|
||||
# --no-session ephemeral; TOOLS_FLAG per capabilities
|
||||
# --offline no startup network operations (update checks/telemetry)
|
||||
exec pi \
|
||||
--offline \
|
||||
--no-session \
|
||||
--no-extensions \
|
||||
--no-skills \
|
||||
--no-prompt-templates \
|
||||
--no-themes \
|
||||
--no-context-files \
|
||||
$TOOLS_FLAG \
|
||||
--provider "$PI_PROVIDER" \
|
||||
--model "$PI_MODEL" \
|
||||
--system-prompt "$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" \
|
||||
-p "$MOSAIC_REQUEST"
|
||||
@@ -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,40 +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
|
||||
|
||||
# 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,110 +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": "vitest run",
|
||||
"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",
|
||||
"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,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,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