diff --git a/.claude/agents/backend-engineer.md b/.claude/agents/backend-engineer.md index dbcc18dd..a4bcb030 100644 --- a/.claude/agents/backend-engineer.md +++ b/.claude/agents/backend-engineer.md @@ -4,7 +4,7 @@ model: sonnet description: Implements ONLY the backend slice of a feature — HTTP API/services, business logic, DB schema/migrations, events, and the backend's own unit tests — against a frozen API contract. Does NOT build UI, the independent test suite, deploy wiring, or docs. Use for backend implementation in a contract-first fan-out. # Figma is reserved for frontend-engineer; pure-code agent gets core tools only (no MCP). tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite -skills: [api-contract-first, feature-flags, verification-protocol, model-cascade] +skills: [api-contract-first, feature-flags, logging, verification-protocol, model-cascade] --- You are a **backend engineer** for FuzeFront. You implement the **backend slice only**. diff --git a/.claude/agents/frontend-engineer.md b/.claude/agents/frontend-engineer.md index 2ccbc44a..3e8d6a02 100644 --- a/.claude/agents/frontend-engineer.md +++ b/.claude/agents/frontend-engineer.md @@ -5,7 +5,7 @@ description: Implements ONLY the UI slice of a feature — a design-system-first # SOLE owner of the Figma MCP plugin (design-to-code). All other domain agents have # Figma removed from their tool grant — it is reserved here for the UI/design-system slice. tools: "*" -skills: [fuzefront-ui-package, design-system-inheritance, design-system-conformance, ui-frame-contract, frontend-design, feature-flags, ui-runtime-validation, verification-protocol, model-cascade] +skills: [fuzefront-ui-package, design-system-inheritance, design-system-conformance, ui-frame-contract, frontend-design, feature-flags, logging, ui-runtime-validation, verification-protocol, model-cascade] --- You are a **frontend engineer**. You implement the **UI slice only**. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ba45be07..74c7eddd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,586 +1,608 @@ -name: Deploy FuzeFront Website (SSH) - -on: - push: - branches: [ master, fuzefront-website ] - pull_request: - branches: [ master ] - -env: - AWS_REGION: us-east-1 - ECR_REPOSITORY_BACKEND: fuzefront-website-backend - ECR_REPOSITORY_FRONTEND: fuzefront-website-frontend - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: +name: Deploy FuzeFront Website (SSH) + +# SCOPED to the website stack. Everything this workflow touches lives under +# fuzefront-website/ (backend, frontend, and infrastructure/main.tf), but it had +# no `paths:` filter — so it planned Terraform against AWS on EVERY pull request +# to master, including ones that change nothing it deploys. +# +# That plan currently fails outright: data.aws_vpc.default, data.aws_lb.main and +# data.aws_autoscaling_group.main all resolve to nothing in us-east-1, i.e. the +# AWS resources these data sources read no longer exist. So this was a +# permanently-red check on unrelated PRs. +# +# The filter does NOT hide that. A PR that touches fuzefront-website/** still +# runs it and still fails — loudly, on the change that owns the problem. What it +# stops is an unrelated PR being blocked by infrastructure it never touched. +# +# Whether the AWS website stack should exist at all is a separate, open question +# (prod is Contabo k3s via Helm/Argo per CLAUDE.md). Not decided here. +on: + push: + branches: [ master, fuzefront-website ] + paths: + - 'fuzefront-website/**' + - '.github/workflows/deploy.yml' + pull_request: + branches: [ master ] + paths: + - 'fuzefront-website/**' + - '.github/workflows/deploy.yml' + +env: + AWS_REGION: us-east-1 + ECR_REPOSITORY_BACKEND: fuzefront-website-backend + ECR_REPOSITORY_FRONTEND: fuzefront-website-frontend + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: node-version: '24.x' - cache: 'npm' - cache-dependency-path: | - fuzefront-website/backend/package-lock.json - fuzefront-website/frontend/package-lock.json - - - name: Install backend dependencies - working-directory: ./fuzefront-website/backend - run: npm ci - - - name: Install frontend dependencies - working-directory: ./fuzefront-website/frontend - run: npm ci - - - name: Run backend tests - working-directory: ./fuzefront-website/backend - run: npm test - - - name: Run backend linting - working-directory: ./fuzefront-website/backend - run: npm run lint - - - name: Run backend type checking - working-directory: ./fuzefront-website/backend - run: npm run type-check - - - name: Run frontend linting - working-directory: ./fuzefront-website/frontend - run: npm run lint - - - name: Run frontend type checking - working-directory: ./fuzefront-website/frontend - run: npm run type-check - - - name: Build frontend - working-directory: ./fuzefront-website/frontend - run: npm run build - - build-and-push: - needs: test - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/fuzefront-website' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Create ECR repositories if they don't exist - run: | - echo "🔍 Creating ECR repositories if needed..." - aws ecr describe-repositories --repository-names $ECR_REPOSITORY_BACKEND --region $AWS_REGION || \ - aws ecr create-repository --repository-name $ECR_REPOSITORY_BACKEND --region $AWS_REGION - - aws ecr describe-repositories --repository-names $ECR_REPOSITORY_FRONTEND --region $AWS_REGION || \ - aws ecr create-repository --repository-name $ECR_REPOSITORY_FRONTEND --region $AWS_REGION - echo "✅ ECR repositories ready" - - - name: Check current ECR images (before build) - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - run: | - echo "🔍 Current ECR images before build:" - echo "Backend images:" - aws ecr list-images --repository-name $ECR_REPOSITORY_BACKEND --region $AWS_REGION --query 'imageIds[*].[imageTag,imagePushedAt]' --output table || echo "No backend images found" - echo "Frontend images:" - aws ecr list-images --repository-name $ECR_REPOSITORY_FRONTEND --region $AWS_REGION --query 'imageIds[*].[imageTag,imagePushedAt]' --output table || echo "No frontend images found" - - - name: Build and push backend Docker image - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ github.sha }} - run: | - echo "🚀 Building backend Docker image..." - echo "ECR Registry: $ECR_REGISTRY" - echo "Image Tag: $IMAGE_TAG" - echo "Repository: $ECR_REPOSITORY_BACKEND" - - cd fuzefront-website/backend - echo "📁 Working directory: $(pwd)" - echo "📋 Files in backend directory:" - ls -la - - # Clear Docker cache and system - echo "🧹 Cleaning Docker cache..." - docker builder prune -f || true - docker system prune -f || true - - # Build backend image with comprehensive logging - echo "🔨 Building backend image with tag: $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:$IMAGE_TAG" - if docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:$IMAGE_TAG .; then - echo "✅ Backend build successful" - else - echo "❌ Backend build failed" - exit 1 - fi - - echo "🔨 Building backend image with latest tag..." - if docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:latest .; then - echo "✅ Backend latest build successful" - else - echo "❌ Backend latest build failed" - exit 1 - fi - - # Push images with error checking - echo "📤 Pushing backend image: $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:$IMAGE_TAG" - if docker push $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:$IMAGE_TAG; then - echo "✅ Backend push successful" - else - echo "❌ Backend push failed" - exit 1 - fi - - echo "📤 Pushing backend image: $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:latest" - if docker push $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:latest; then - echo "✅ Backend latest push successful" - else - echo "❌ Backend latest push failed" - exit 1 - fi - - - name: Build and push frontend Docker image - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ github.sha }} - run: | - echo "🚀 Building frontend Docker image..." - echo "ECR Registry: $ECR_REGISTRY" - echo "Image Tag: $IMAGE_TAG" - echo "Repository: $ECR_REPOSITORY_FRONTEND" - - cd fuzefront-website/frontend - echo "📁 Working directory: $(pwd)" - echo "📋 Files in frontend directory:" - ls -la - - echo "🔍 Checking App.tsx content:" - head -10 src/App.tsx - - # Clear Docker cache and system - echo "🧹 Cleaning Docker cache..." - docker builder prune -f || true - docker system prune -f || true - - # Build frontend image with comprehensive logging - echo "🔨 Building frontend image with tag: $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:$IMAGE_TAG" - if docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:$IMAGE_TAG .; then - echo "✅ Frontend build successful" - else - echo "❌ Frontend build failed" - exit 1 - fi - - echo "🔨 Building frontend image with latest tag..." - if docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:latest .; then - echo "✅ Frontend latest build successful" - else - echo "❌ Frontend latest build failed" - exit 1 - fi - - # Push images with error checking - echo "📤 Pushing frontend image: $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:$IMAGE_TAG" - if docker push $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:$IMAGE_TAG; then - echo "✅ Frontend push successful" - else - echo "❌ Frontend push failed" - exit 1 - fi - - echo "📤 Pushing frontend image: $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:latest" - if docker push $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:latest; then - echo "✅ Frontend latest push successful" - else - echo "❌ Frontend latest push failed" - exit 1 - fi - - - name: Verify ECR images were pushed successfully - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ github.sha }} - run: | - echo "🔍 Verifying ECR images after push:" - - echo "Backend images:" - if aws ecr describe-images --repository-name $ECR_REPOSITORY_BACKEND --region $AWS_REGION --image-ids imageTag=latest --query 'imageDetails[0].imagePushedAt' --output text; then - echo "✅ Backend latest image verified in ECR" - else - echo "❌ Backend latest image NOT found in ECR" - exit 1 - fi - - echo "Frontend images:" - if aws ecr describe-images --repository-name $ECR_REPOSITORY_FRONTEND --region $AWS_REGION --image-ids imageTag=latest --query 'imageDetails[0].imagePushedAt' --output text; then - echo "✅ Frontend latest image verified in ECR" - else - echo "❌ Frontend latest image NOT found in ECR" - exit 1 - fi - - echo "📊 All ECR images after build:" - echo "Backend:" - aws ecr list-images --repository-name $ECR_REPOSITORY_BACKEND --region $AWS_REGION --query 'imageIds[*].[imageTag,imagePushedAt]' --output table - echo "Frontend:" - aws ecr list-images --repository-name $ECR_REPOSITORY_FRONTEND --region $AWS_REGION --query 'imageIds[*].[imageTag,imagePushedAt]' --output table - - deploy-infrastructure: - needs: build-and-push - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/fuzefront-website' - outputs: - asg_name: ${{ steps.terraform-outputs.outputs.asg_name }} - alb_dns_name: ${{ steps.terraform-outputs.outputs.alb_dns_name }} - instance_id: ${{ steps.ensure-instance.outputs.instance_id }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Setup Terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: 1.5.0 - - - name: Terraform Init - working-directory: ./fuzefront-website/infrastructure - run: terraform init - - - name: Terraform Plan - working-directory: ./fuzefront-website/infrastructure - run: | - terraform plan \ - -var="domain_name=${{ secrets.DOMAIN_NAME }}" \ - -var="ssl_email=${{ secrets.SSL_EMAIL }}" \ - -out=tfplan - - - name: Terraform Apply - working-directory: ./fuzefront-website/infrastructure - run: terraform apply tfplan - - - name: Get Terraform outputs - working-directory: ./fuzefront-website/infrastructure - id: terraform-outputs - run: | - echo "alb_dns_name=$(terraform output -raw load_balancer_dns_name)" >> $GITHUB_OUTPUT - echo "asg_name=$(terraform output -raw autoscaling_group_name)" >> $GITHUB_OUTPUT - - - name: Ensure dedicated deployment instance exists - id: ensure-instance - run: | - echo "Checking for dedicated deployment instance..." - - # Check if our specific named instance exists and is running - INSTANCE_ID=$(aws ec2 describe-instances \ - --region ${{ env.AWS_REGION }} \ - --filters \ - "Name=instance-state-name,Values=running" \ - "Name=tag:Name,Values=fuzefront-website-production-instance" \ - --query 'Reservations[0].Instances[0].InstanceId' \ - --output text 2>/dev/null || echo "None") - - if [ "$INSTANCE_ID" == "None" ] || [ -z "$INSTANCE_ID" ]; then - echo "No dedicated instance found. Creating one..." - - # Force ASG to create an instance with the correct name - aws autoscaling update-auto-scaling-group \ - --auto-scaling-group-name ${{ steps.terraform-outputs.outputs.asg_name }} \ - --min-size 1 \ - --desired-capacity 1 \ - --max-size 3 \ - --region ${{ env.AWS_REGION }} - - echo "Waiting for ASG to create instance..." - for i in {1..20}; do - sleep 30 - INSTANCE_ID=$(aws ec2 describe-instances \ - --region ${{ env.AWS_REGION }} \ - --filters \ - "Name=instance-state-name,Values=running" \ - "Name=tag:aws:autoscaling:groupName,Values=${{ steps.terraform-outputs.outputs.asg_name }}" \ - --query 'Reservations[0].Instances[0].InstanceId' \ - --output text 2>/dev/null || echo "None") - - if [ "$INSTANCE_ID" != "None" ] && [ -n "$INSTANCE_ID" ]; then - echo "Instance created: $INSTANCE_ID" - - # Ensure it has the correct name tag - aws ec2 create-tags \ - --region ${{ env.AWS_REGION }} \ - --resources $INSTANCE_ID \ - --tags Key=Name,Value=fuzefront-website-production-instance - - echo "Instance tagged with correct name" - break - fi - - echo "Attempt $i: No instance ready yet, waiting 30s..." - if [ $i -eq 20 ]; then - echo "ERROR: Failed to create instance after 10 minutes" - exit 1 - fi - done - else - echo "Dedicated instance already exists: $INSTANCE_ID" - fi - - echo "instance_id=$INSTANCE_ID" >> $GITHUB_OUTPUT - - deploy-application: - needs: deploy-infrastructure - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/fuzefront-website' - - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Setup SSH key - run: | - echo "${{ secrets.SSH_PRIVATE_KEY }}" > /tmp/ssh_key - chmod 600 /tmp/ssh_key - - - name: Get target instance IP - id: get-instances - run: | - # Use the instance ID from infrastructure deployment step - INSTANCE_ID="${{ needs.deploy-infrastructure.outputs.instance_id }}" - echo "Using dedicated instance from previous step: $INSTANCE_ID" - echo "instance_id=$INSTANCE_ID" >> $GITHUB_OUTPUT - - - name: Get instance IP address - id: get-ip - run: | - INSTANCE_IP=$(aws ec2 describe-instances \ - --instance-ids ${{ steps.get-instances.outputs.instance_id }} \ - --query 'Reservations[0].Instances[0].PublicIpAddress' \ - --output text) - echo "Instance IP: $INSTANCE_IP" - echo "instance_ip=$INSTANCE_IP" >> $GITHUB_OUTPUT - - - name: Deploy to existing instance via SSH - run: | - INSTANCE_IP="${{ steps.get-ip.outputs.instance_ip }}" - echo "Deploying to instance: $INSTANCE_IP" - - # Wait for SSH to be available - echo "Waiting for SSH to be available..." - for i in {1..30}; do - SSH_OUTPUT=$(ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no -o ConnectTimeout=10 ubuntu@$INSTANCE_IP 'echo "SSH OK"' 2>&1) - SSH_EXIT_CODE=$? - - if [ $SSH_EXIT_CODE -eq 0 ]; then - echo "SSH connection successful" - break - fi - - # Check for permission denied error - don't retry these - if echo "$SSH_OUTPUT" | grep -q "Permission denied (publickey)"; then - echo "ERROR: SSH Permission denied (publickey) - check your SSH key configuration" - echo "SSH Output: $SSH_OUTPUT" - exit 1 - fi - - echo "SSH attempt $i failed, retrying in 10 seconds..." - echo "SSH error: $SSH_OUTPUT" - sleep 10 - if [ $i -eq 30 ]; then - echo "SSH connection failed after 30 attempts" - echo "Final SSH error: $SSH_OUTPUT" - exit 1 - fi - done - - # First check the current state of the instance - echo "Checking current instance state..." - ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ubuntu@$INSTANCE_IP << 'EOF' - echo "=== Instance Status Check ===" - echo "OS Info: $(cat /etc/os-release | grep PRETTY_NAME)" - echo "Uptime: $(uptime)" - echo "Docker installed: $(which docker || echo 'NOT INSTALLED')" - echo "Docker running: $(sudo systemctl is-active docker || echo 'NOT RUNNING')" - echo "Docker version: $(docker --version 2>/dev/null || echo 'DOCKER NOT AVAILABLE')" - echo "Existing containers: $(docker ps -a --format 'table {{.Names}}\t{{.Status}}' 2>/dev/null || echo 'DOCKER NOT AVAILABLE')" - echo "Deploy script exists: $(ls -la /opt/fuzefront-website/deploy.sh 2>/dev/null || echo 'DEPLOY SCRIPT MISSING')" - echo "SSM Agent status: $(sudo systemctl is-active amazon-ssm-agent || echo 'SSM NOT RUNNING')" - echo "=== End Status Check ===" - EOF - - # Enhanced deployment with forced container restart - echo "🚀 Proceeding with enhanced deployment..." - ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ubuntu@$INSTANCE_IP << 'EOF' - export AWS_REGION=${{ env.AWS_REGION }} - export ECR_REPOSITORY_BACKEND=${{ env.ECR_REPOSITORY_BACKEND }} - export ECR_REPOSITORY_FRONTEND=${{ env.ECR_REPOSITORY_FRONTEND }} - - echo "🔍 Pre-deployment container status:" - docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || echo "No containers running" - - echo "🔍 Current ECR images on instance:" - docker images | grep fuzefront-website || echo "No fuzefront images found" - - echo "🔄 Forcing ECR login and image pull..." - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com - - echo "📥 Pulling latest images from ECR..." - docker pull 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com/fuzefront-website-backend:latest - docker pull 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com/fuzefront-website-frontend:latest - - echo "🛑 Stopping existing containers..." - docker stop backend frontend reverse-proxy 2>/dev/null || echo "Some containers already stopped" - docker rm backend frontend reverse-proxy 2>/dev/null || echo "Some containers already removed" - - echo "🌐 Ensuring Docker network exists..." - docker network create fuzefront-network 2>/dev/null || echo "Network already exists" - - echo "🚀 Starting fresh containers with latest images..." - - # Start backend - docker run -d \ - --name backend \ - --network fuzefront-network \ - --restart unless-stopped \ - -p 3001:3001 \ - 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com/fuzefront-website-backend:latest - - # Start frontend - docker run -d \ - --name frontend \ - --network fuzefront-network \ - --restart unless-stopped \ - -p 3000:3000 \ - 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com/fuzefront-website-frontend:latest - - # Start reverse proxy - docker run -d \ - --name reverse-proxy \ - --network fuzefront-network \ - --restart unless-stopped \ - -p 80:80 \ - -v /tmp/nginx-reverse-proxy/nginx.conf:/etc/nginx/nginx.conf:ro \ - nginx:alpine - - echo "⏳ Waiting for containers to start..." - sleep 30 - - echo "🔍 Post-deployment container status:" - docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" - - echo "🏥 Testing container health:" - echo "Backend health:" - curl -s http://localhost:3001/health || echo "❌ Backend health failed" - echo "" - echo "Frontend health:" - curl -s http://localhost:3000/health || echo "❌ Frontend health failed" - echo "" - echo "Reverse proxy health:" - curl -s http://localhost:80/health || echo "❌ Reverse proxy health failed" - - echo "✅ Deployment completed!" - EOF - - - name: Verify deployment - run: | - ALB_DNS="${{ needs.deploy-infrastructure.outputs.alb_dns_name }}" - echo "🔍 Comprehensive deployment verification" - echo "Load Balancer DNS: $ALB_DNS" - - # Test health endpoint - echo "🏥 Testing health endpoint..." - for i in {1..30}; do - if curl -f "http://$ALB_DNS/health"; then - echo "✅ Health check passed!" - break - fi - echo "Attempt $i failed, retrying in 10 seconds..." - sleep 10 - done - - # Test analytics health endpoint (our fix) - echo "🔍 Testing analytics health endpoint..." - if curl -s "http://$ALB_DNS/api/analytics/health" | grep -q "healthy"; then - echo "✅ Analytics health endpoint working!" - else - echo "❌ Analytics health endpoint failed" - fi - - # Check for new frontend bundle (React app deployment verification) - echo "🔍 Checking frontend bundle..." - HTML_CONTENT=$(curl -s "http://$ALB_DNS") - JS_BUNDLE=$(echo "$HTML_CONTENT" | grep -o '/assets/index-[^"]*\.js' | head -1) - echo "Current JS bundle: $JS_BUNDLE" - - # Verify React content is being served - echo "🔍 Verifying React content..." - if echo "$HTML_CONTENT" | grep -q '
'; then - echo "✅ React root element found in HTML" - else - echo "❌ React root element missing!" - exit 1 - fi - - # Test if JS bundle is accessible - if [ -n "$JS_BUNDLE" ]; then - echo "🔍 Testing JS bundle accessibility..." - if curl -f -s "http://$ALB_DNS$JS_BUNDLE" > /dev/null; then - echo "✅ JS bundle is accessible" - - # Check bundle size (should be different for minimal React app) - BUNDLE_SIZE=$(curl -s "http://$ALB_DNS$JS_BUNDLE" | wc -c) - echo "📊 Bundle size: $BUNDLE_SIZE bytes" - - # Check if bundle contains our minimal React app code - if curl -s "http://$ALB_DNS$JS_BUNDLE" | grep -q "React is Working"; then - echo "✅ Minimal React app detected in bundle!" - else - echo "⚠️ Minimal React app content not found in bundle" - fi - else - echo "❌ JS bundle not accessible" - exit 1 - fi - fi - - echo "🎉 Deployment verification completed!" - echo "🌐 Website should be live at: http://$ALB_DNS" - - notify: - needs: [test, build-and-push, deploy-infrastructure, deploy-application] - runs-on: ubuntu-latest - if: always() - - steps: - - name: Notify deployment status - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/fuzefront-website' - run: | - if [ "${{ needs.deploy-application.result }}" == "success" ]; then - echo "✅ Deployment successful! Website is live at https://${{ secrets.DOMAIN_NAME }}" - else - echo "❌ Deployment failed. Check the logs for details." + cache: 'npm' + cache-dependency-path: | + fuzefront-website/backend/package-lock.json + fuzefront-website/frontend/package-lock.json + + - name: Install backend dependencies + working-directory: ./fuzefront-website/backend + run: npm ci + + - name: Install frontend dependencies + working-directory: ./fuzefront-website/frontend + run: npm ci + + - name: Run backend tests + working-directory: ./fuzefront-website/backend + run: npm test + + - name: Run backend linting + working-directory: ./fuzefront-website/backend + run: npm run lint + + - name: Run backend type checking + working-directory: ./fuzefront-website/backend + run: npm run type-check + + - name: Run frontend linting + working-directory: ./fuzefront-website/frontend + run: npm run lint + + - name: Run frontend type checking + working-directory: ./fuzefront-website/frontend + run: npm run type-check + + - name: Build frontend + working-directory: ./fuzefront-website/frontend + run: npm run build + + build-and-push: + needs: test + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/fuzefront-website' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Create ECR repositories if they don't exist + run: | + echo "🔍 Creating ECR repositories if needed..." + aws ecr describe-repositories --repository-names $ECR_REPOSITORY_BACKEND --region $AWS_REGION || \ + aws ecr create-repository --repository-name $ECR_REPOSITORY_BACKEND --region $AWS_REGION + + aws ecr describe-repositories --repository-names $ECR_REPOSITORY_FRONTEND --region $AWS_REGION || \ + aws ecr create-repository --repository-name $ECR_REPOSITORY_FRONTEND --region $AWS_REGION + echo "✅ ECR repositories ready" + + - name: Check current ECR images (before build) + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + echo "🔍 Current ECR images before build:" + echo "Backend images:" + aws ecr list-images --repository-name $ECR_REPOSITORY_BACKEND --region $AWS_REGION --query 'imageIds[*].[imageTag,imagePushedAt]' --output table || echo "No backend images found" + echo "Frontend images:" + aws ecr list-images --repository-name $ECR_REPOSITORY_FRONTEND --region $AWS_REGION --query 'imageIds[*].[imageTag,imagePushedAt]' --output table || echo "No frontend images found" + + - name: Build and push backend Docker image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ github.sha }} + run: | + echo "🚀 Building backend Docker image..." + echo "ECR Registry: $ECR_REGISTRY" + echo "Image Tag: $IMAGE_TAG" + echo "Repository: $ECR_REPOSITORY_BACKEND" + + cd fuzefront-website/backend + echo "📁 Working directory: $(pwd)" + echo "📋 Files in backend directory:" + ls -la + + # Clear Docker cache and system + echo "🧹 Cleaning Docker cache..." + docker builder prune -f || true + docker system prune -f || true + + # Build backend image with comprehensive logging + echo "🔨 Building backend image with tag: $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:$IMAGE_TAG" + if docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:$IMAGE_TAG .; then + echo "✅ Backend build successful" + else + echo "❌ Backend build failed" + exit 1 + fi + + echo "🔨 Building backend image with latest tag..." + if docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:latest .; then + echo "✅ Backend latest build successful" + else + echo "❌ Backend latest build failed" + exit 1 + fi + + # Push images with error checking + echo "📤 Pushing backend image: $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:$IMAGE_TAG" + if docker push $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:$IMAGE_TAG; then + echo "✅ Backend push successful" + else + echo "❌ Backend push failed" + exit 1 + fi + + echo "📤 Pushing backend image: $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:latest" + if docker push $ECR_REGISTRY/$ECR_REPOSITORY_BACKEND:latest; then + echo "✅ Backend latest push successful" + else + echo "❌ Backend latest push failed" + exit 1 + fi + + - name: Build and push frontend Docker image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ github.sha }} + run: | + echo "🚀 Building frontend Docker image..." + echo "ECR Registry: $ECR_REGISTRY" + echo "Image Tag: $IMAGE_TAG" + echo "Repository: $ECR_REPOSITORY_FRONTEND" + + cd fuzefront-website/frontend + echo "📁 Working directory: $(pwd)" + echo "📋 Files in frontend directory:" + ls -la + + echo "🔍 Checking App.tsx content:" + head -10 src/App.tsx + + # Clear Docker cache and system + echo "🧹 Cleaning Docker cache..." + docker builder prune -f || true + docker system prune -f || true + + # Build frontend image with comprehensive logging + echo "🔨 Building frontend image with tag: $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:$IMAGE_TAG" + if docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:$IMAGE_TAG .; then + echo "✅ Frontend build successful" + else + echo "❌ Frontend build failed" + exit 1 + fi + + echo "🔨 Building frontend image with latest tag..." + if docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:latest .; then + echo "✅ Frontend latest build successful" + else + echo "❌ Frontend latest build failed" + exit 1 + fi + + # Push images with error checking + echo "📤 Pushing frontend image: $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:$IMAGE_TAG" + if docker push $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:$IMAGE_TAG; then + echo "✅ Frontend push successful" + else + echo "❌ Frontend push failed" + exit 1 + fi + + echo "📤 Pushing frontend image: $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:latest" + if docker push $ECR_REGISTRY/$ECR_REPOSITORY_FRONTEND:latest; then + echo "✅ Frontend latest push successful" + else + echo "❌ Frontend latest push failed" + exit 1 + fi + + - name: Verify ECR images were pushed successfully + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ github.sha }} + run: | + echo "🔍 Verifying ECR images after push:" + + echo "Backend images:" + if aws ecr describe-images --repository-name $ECR_REPOSITORY_BACKEND --region $AWS_REGION --image-ids imageTag=latest --query 'imageDetails[0].imagePushedAt' --output text; then + echo "✅ Backend latest image verified in ECR" + else + echo "❌ Backend latest image NOT found in ECR" + exit 1 + fi + + echo "Frontend images:" + if aws ecr describe-images --repository-name $ECR_REPOSITORY_FRONTEND --region $AWS_REGION --image-ids imageTag=latest --query 'imageDetails[0].imagePushedAt' --output text; then + echo "✅ Frontend latest image verified in ECR" + else + echo "❌ Frontend latest image NOT found in ECR" + exit 1 + fi + + echo "📊 All ECR images after build:" + echo "Backend:" + aws ecr list-images --repository-name $ECR_REPOSITORY_BACKEND --region $AWS_REGION --query 'imageIds[*].[imageTag,imagePushedAt]' --output table + echo "Frontend:" + aws ecr list-images --repository-name $ECR_REPOSITORY_FRONTEND --region $AWS_REGION --query 'imageIds[*].[imageTag,imagePushedAt]' --output table + + deploy-infrastructure: + needs: build-and-push + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/fuzefront-website' + outputs: + asg_name: ${{ steps.terraform-outputs.outputs.asg_name }} + alb_dns_name: ${{ steps.terraform-outputs.outputs.alb_dns_name }} + instance_id: ${{ steps.ensure-instance.outputs.instance_id }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.5.0 + + - name: Terraform Init + working-directory: ./fuzefront-website/infrastructure + run: terraform init + + - name: Terraform Plan + working-directory: ./fuzefront-website/infrastructure + run: | + terraform plan \ + -var="domain_name=${{ secrets.DOMAIN_NAME }}" \ + -var="ssl_email=${{ secrets.SSL_EMAIL }}" \ + -out=tfplan + + - name: Terraform Apply + working-directory: ./fuzefront-website/infrastructure + run: terraform apply tfplan + + - name: Get Terraform outputs + working-directory: ./fuzefront-website/infrastructure + id: terraform-outputs + run: | + echo "alb_dns_name=$(terraform output -raw load_balancer_dns_name)" >> $GITHUB_OUTPUT + echo "asg_name=$(terraform output -raw autoscaling_group_name)" >> $GITHUB_OUTPUT + + - name: Ensure dedicated deployment instance exists + id: ensure-instance + run: | + echo "Checking for dedicated deployment instance..." + + # Check if our specific named instance exists and is running + INSTANCE_ID=$(aws ec2 describe-instances \ + --region ${{ env.AWS_REGION }} \ + --filters \ + "Name=instance-state-name,Values=running" \ + "Name=tag:Name,Values=fuzefront-website-production-instance" \ + --query 'Reservations[0].Instances[0].InstanceId' \ + --output text 2>/dev/null || echo "None") + + if [ "$INSTANCE_ID" == "None" ] || [ -z "$INSTANCE_ID" ]; then + echo "No dedicated instance found. Creating one..." + + # Force ASG to create an instance with the correct name + aws autoscaling update-auto-scaling-group \ + --auto-scaling-group-name ${{ steps.terraform-outputs.outputs.asg_name }} \ + --min-size 1 \ + --desired-capacity 1 \ + --max-size 3 \ + --region ${{ env.AWS_REGION }} + + echo "Waiting for ASG to create instance..." + for i in {1..20}; do + sleep 30 + INSTANCE_ID=$(aws ec2 describe-instances \ + --region ${{ env.AWS_REGION }} \ + --filters \ + "Name=instance-state-name,Values=running" \ + "Name=tag:aws:autoscaling:groupName,Values=${{ steps.terraform-outputs.outputs.asg_name }}" \ + --query 'Reservations[0].Instances[0].InstanceId' \ + --output text 2>/dev/null || echo "None") + + if [ "$INSTANCE_ID" != "None" ] && [ -n "$INSTANCE_ID" ]; then + echo "Instance created: $INSTANCE_ID" + + # Ensure it has the correct name tag + aws ec2 create-tags \ + --region ${{ env.AWS_REGION }} \ + --resources $INSTANCE_ID \ + --tags Key=Name,Value=fuzefront-website-production-instance + + echo "Instance tagged with correct name" + break + fi + + echo "Attempt $i: No instance ready yet, waiting 30s..." + if [ $i -eq 20 ]; then + echo "ERROR: Failed to create instance after 10 minutes" + exit 1 + fi + done + else + echo "Dedicated instance already exists: $INSTANCE_ID" + fi + + echo "instance_id=$INSTANCE_ID" >> $GITHUB_OUTPUT + + deploy-application: + needs: deploy-infrastructure + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/fuzefront-website' + + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Setup SSH key + run: | + echo "${{ secrets.SSH_PRIVATE_KEY }}" > /tmp/ssh_key + chmod 600 /tmp/ssh_key + + - name: Get target instance IP + id: get-instances + run: | + # Use the instance ID from infrastructure deployment step + INSTANCE_ID="${{ needs.deploy-infrastructure.outputs.instance_id }}" + echo "Using dedicated instance from previous step: $INSTANCE_ID" + echo "instance_id=$INSTANCE_ID" >> $GITHUB_OUTPUT + + - name: Get instance IP address + id: get-ip + run: | + INSTANCE_IP=$(aws ec2 describe-instances \ + --instance-ids ${{ steps.get-instances.outputs.instance_id }} \ + --query 'Reservations[0].Instances[0].PublicIpAddress' \ + --output text) + echo "Instance IP: $INSTANCE_IP" + echo "instance_ip=$INSTANCE_IP" >> $GITHUB_OUTPUT + + - name: Deploy to existing instance via SSH + run: | + INSTANCE_IP="${{ steps.get-ip.outputs.instance_ip }}" + echo "Deploying to instance: $INSTANCE_IP" + + # Wait for SSH to be available + echo "Waiting for SSH to be available..." + for i in {1..30}; do + SSH_OUTPUT=$(ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no -o ConnectTimeout=10 ubuntu@$INSTANCE_IP 'echo "SSH OK"' 2>&1) + SSH_EXIT_CODE=$? + + if [ $SSH_EXIT_CODE -eq 0 ]; then + echo "SSH connection successful" + break + fi + + # Check for permission denied error - don't retry these + if echo "$SSH_OUTPUT" | grep -q "Permission denied (publickey)"; then + echo "ERROR: SSH Permission denied (publickey) - check your SSH key configuration" + echo "SSH Output: $SSH_OUTPUT" + exit 1 + fi + + echo "SSH attempt $i failed, retrying in 10 seconds..." + echo "SSH error: $SSH_OUTPUT" + sleep 10 + if [ $i -eq 30 ]; then + echo "SSH connection failed after 30 attempts" + echo "Final SSH error: $SSH_OUTPUT" + exit 1 + fi + done + + # First check the current state of the instance + echo "Checking current instance state..." + ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ubuntu@$INSTANCE_IP << 'EOF' + echo "=== Instance Status Check ===" + echo "OS Info: $(cat /etc/os-release | grep PRETTY_NAME)" + echo "Uptime: $(uptime)" + echo "Docker installed: $(which docker || echo 'NOT INSTALLED')" + echo "Docker running: $(sudo systemctl is-active docker || echo 'NOT RUNNING')" + echo "Docker version: $(docker --version 2>/dev/null || echo 'DOCKER NOT AVAILABLE')" + echo "Existing containers: $(docker ps -a --format 'table {{.Names}}\t{{.Status}}' 2>/dev/null || echo 'DOCKER NOT AVAILABLE')" + echo "Deploy script exists: $(ls -la /opt/fuzefront-website/deploy.sh 2>/dev/null || echo 'DEPLOY SCRIPT MISSING')" + echo "SSM Agent status: $(sudo systemctl is-active amazon-ssm-agent || echo 'SSM NOT RUNNING')" + echo "=== End Status Check ===" + EOF + + # Enhanced deployment with forced container restart + echo "🚀 Proceeding with enhanced deployment..." + ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ubuntu@$INSTANCE_IP << 'EOF' + export AWS_REGION=${{ env.AWS_REGION }} + export ECR_REPOSITORY_BACKEND=${{ env.ECR_REPOSITORY_BACKEND }} + export ECR_REPOSITORY_FRONTEND=${{ env.ECR_REPOSITORY_FRONTEND }} + + echo "🔍 Pre-deployment container status:" + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || echo "No containers running" + + echo "🔍 Current ECR images on instance:" + docker images | grep fuzefront-website || echo "No fuzefront images found" + + echo "🔄 Forcing ECR login and image pull..." + aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com + + echo "📥 Pulling latest images from ECR..." + docker pull 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com/fuzefront-website-backend:latest + docker pull 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com/fuzefront-website-frontend:latest + + echo "🛑 Stopping existing containers..." + docker stop backend frontend reverse-proxy 2>/dev/null || echo "Some containers already stopped" + docker rm backend frontend reverse-proxy 2>/dev/null || echo "Some containers already removed" + + echo "🌐 Ensuring Docker network exists..." + docker network create fuzefront-network 2>/dev/null || echo "Network already exists" + + echo "🚀 Starting fresh containers with latest images..." + + # Start backend + docker run -d \ + --name backend \ + --network fuzefront-network \ + --restart unless-stopped \ + -p 3001:3001 \ + 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com/fuzefront-website-backend:latest + + # Start frontend + docker run -d \ + --name frontend \ + --network fuzefront-network \ + --restart unless-stopped \ + -p 3000:3000 \ + 524103092742.dkr.ecr.$AWS_REGION.amazonaws.com/fuzefront-website-frontend:latest + + # Start reverse proxy + docker run -d \ + --name reverse-proxy \ + --network fuzefront-network \ + --restart unless-stopped \ + -p 80:80 \ + -v /tmp/nginx-reverse-proxy/nginx.conf:/etc/nginx/nginx.conf:ro \ + nginx:alpine + + echo "⏳ Waiting for containers to start..." + sleep 30 + + echo "🔍 Post-deployment container status:" + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" + + echo "🏥 Testing container health:" + echo "Backend health:" + curl -s http://localhost:3001/health || echo "❌ Backend health failed" + echo "" + echo "Frontend health:" + curl -s http://localhost:3000/health || echo "❌ Frontend health failed" + echo "" + echo "Reverse proxy health:" + curl -s http://localhost:80/health || echo "❌ Reverse proxy health failed" + + echo "✅ Deployment completed!" + EOF + + - name: Verify deployment + run: | + ALB_DNS="${{ needs.deploy-infrastructure.outputs.alb_dns_name }}" + echo "🔍 Comprehensive deployment verification" + echo "Load Balancer DNS: $ALB_DNS" + + # Test health endpoint + echo "🏥 Testing health endpoint..." + for i in {1..30}; do + if curl -f "http://$ALB_DNS/health"; then + echo "✅ Health check passed!" + break + fi + echo "Attempt $i failed, retrying in 10 seconds..." + sleep 10 + done + + # Test analytics health endpoint (our fix) + echo "🔍 Testing analytics health endpoint..." + if curl -s "http://$ALB_DNS/api/analytics/health" | grep -q "healthy"; then + echo "✅ Analytics health endpoint working!" + else + echo "❌ Analytics health endpoint failed" + fi + + # Check for new frontend bundle (React app deployment verification) + echo "🔍 Checking frontend bundle..." + HTML_CONTENT=$(curl -s "http://$ALB_DNS") + JS_BUNDLE=$(echo "$HTML_CONTENT" | grep -o '/assets/index-[^"]*\.js' | head -1) + echo "Current JS bundle: $JS_BUNDLE" + + # Verify React content is being served + echo "🔍 Verifying React content..." + if echo "$HTML_CONTENT" | grep -q '
'; then + echo "✅ React root element found in HTML" + else + echo "❌ React root element missing!" + exit 1 + fi + + # Test if JS bundle is accessible + if [ -n "$JS_BUNDLE" ]; then + echo "🔍 Testing JS bundle accessibility..." + if curl -f -s "http://$ALB_DNS$JS_BUNDLE" > /dev/null; then + echo "✅ JS bundle is accessible" + + # Check bundle size (should be different for minimal React app) + BUNDLE_SIZE=$(curl -s "http://$ALB_DNS$JS_BUNDLE" | wc -c) + echo "📊 Bundle size: $BUNDLE_SIZE bytes" + + # Check if bundle contains our minimal React app code + if curl -s "http://$ALB_DNS$JS_BUNDLE" | grep -q "React is Working"; then + echo "✅ Minimal React app detected in bundle!" + else + echo "⚠️ Minimal React app content not found in bundle" + fi + else + echo "❌ JS bundle not accessible" + exit 1 + fi + fi + + echo "🎉 Deployment verification completed!" + echo "🌐 Website should be live at: http://$ALB_DNS" + + notify: + needs: [test, build-and-push, deploy-infrastructure, deploy-application] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Notify deployment status + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/fuzefront-website' + run: | + if [ "${{ needs.deploy-application.result }}" == "success" ]; then + echo "✅ Deployment successful! Website is live at https://${{ secrets.DOMAIN_NAME }}" + else + echo "❌ Deployment failed. Check the logs for details." fi \ No newline at end of file diff --git a/agent-templates/schema/role-manifest.schema.json b/agent-templates/schema/role-manifest.schema.json index 87809834..f10670fd 100644 --- a/agent-templates/schema/role-manifest.schema.json +++ b/agent-templates/schema/role-manifest.schema.json @@ -80,6 +80,40 @@ "metadata": { "type": "object", "description": "Passed through as agent `metadata` (free-form tracking)." + }, + "a2a": { + "type": "object", + "additionalProperties": false, + "description": "OPTIONAL A2A discoverability/publication block. Mirrors the frozen contract FuzeAgent/agent-templates/contracts/a2a/v1/schema/role-a2a-extension.schema.json. Every field has a derived default, so no existing role.json needs it. The card projection reads role/name/description/services/metadata/coordinator regardless; this block only lets a role improve discoverability (examples/tags) or opt out of publication.", + "properties": { + "publish": { + "type": "boolean", + "default": true, + "description": "false hides this role from the public card. Still reachable on the EXTENDED card if the caller is allowlisted (authz.md §5)." + }, + "extendedOnly": { + "type": "boolean", + "default": false, + "description": "true publishes this skill ONLY on the authenticated extended card, never on the anonymous /.well-known/agent-card.json. Use for skills whose mere existence is sensitive." + }, + "tags": { + "type": "array", + "items": { "type": "string" }, + "description": "Extra tags merged with the derived tags. Derived tags are never removed." + }, + "examples": { + "type": "array", + "items": { "type": "string" }, + "description": "Example prompts a caller can send to this skill — the primary signal a calling agent uses to decide fit. Absent examples make a skill effectively undiscoverable." + }, + "inputModes": { "type": "array", "items": { "type": "string" } }, + "outputModes": { "type": "array", "items": { "type": "string" } }, + "scopes": { + "type": "array", + "items": { "type": "string" }, + "description": "OAuth scopes required for THIS skill, projected into the skill's securityRequirements." + } + } } } }