diff --git a/.cursor/commands/resolve-production-issue.md b/.cursor/commands/resolve-production-issue.md new file mode 100644 index 00000000..e16eb29b --- /dev/null +++ b/.cursor/commands/resolve-production-issue.md @@ -0,0 +1,106 @@ +# Resolve Production Issue - Mandatory Hotfix Workflow + +## CRITICAL: Follow #file:../rules/cursorrules.mdc EXACTLY + +### STEP 1: Prepare Hotfix Branch (MANDATORY) + +#### Branch Setup +- [ ] Ensure clean working tree (starting from local `dev`) +- [ ] Fetch latest remote references: `git fetch origin` +- [ ] Create hotfix branch from remote master: `git checkout -b hotfix/issue-{ISSUE}-{slug} origin/master` +- [ ] Add `in-progress` label to the issue: `gh issue edit {ISSUE} --add-label in-progress` +- [ ] Keep implementation scoped only to production fix requirements +- [ ] Ensure preprod compatibility for config/deployment updates + +--- + +### STEP 2: Issue Intake & Plan Confirmation + +- [ ] Read the full issue, incident details, and any linked docs/runbooks +- [ ] Identify blast radius (service, API routes, infra, data, auth, tenant impact) +- [ ] Define rollback and validation strategy before coding +- [ ] Post issue comment with: + - root-cause hypothesis + - planned fix scope + - preprod + production validation plan +- [ ] STOP! Announce plan and wait for user approval before changes + +### STEP 3: Implement Hotfix (Scoped + Safe) + +- [ ] Implement only the minimal safe fix for production behavior +- [ ] Follow Python + FastAPI clean architecture conventions: + - Domain entities in `coaching/src/domain/entities/` + - Value objects in `coaching/src/domain/value_objects/` + - Ports in `coaching/src/domain/ports/` + - Application services in `coaching/src/application/` + - Infrastructure adapters in `coaching/src/infrastructure/` + - API routes in `coaching/src/api/routes/` + - Core types/constants in `coaching/src/core/` +- [ ] Use Pydantic models, not untyped `dict[str, Any]` in domain +- [ ] Preserve tenant isolation checks on all data access +- [ ] Update workflow/deployment checks when reliability is part of the incident +- [ ] Remove temporary code, debug logs, and dead code + +### STEP 4: Validate on Hotfix Branch (Preprod First) + +- [ ] Run lint/type/tests locally before push: + ```powershell + # Lint + format + python -m ruff check coaching/ shared/ --fix + python -m ruff format coaching/ shared/ + + # Type checking + python -m mypy coaching/src shared/ --explicit-package-bases + + # Tests + cd coaching && uv run pytest --cov=src + ``` +- [ ] Push hotfix branch: `git push -u origin hotfix/issue-{ISSUE}-{slug}` +- [ ] Confirm preprod deployment workflow runs successfully +- [ ] Execute manual validation in preprod for the incident scenario +- [ ] If validation fails: fix on same hotfix branch and repeat this step + +### STEP 5: Promote Hotfix to Production + +- [ ] Open PR `hotfix/issue-{ISSUE}-{slug} -> master` +- [ ] Include incident context, root cause, and validation evidence in PR body +- [ ] Merge PR only after preprod validation is confirmed +- [ ] Watch production deployment workflow end-to-end +- [ ] Verify post-deploy checks: + - Lambda/runtime state healthy (if applicable) + - API health endpoint responsive + - CORS preflight/critical route behavior validated + +### STEP 6: Propagate Fix Downstream (MANDATORY) + +- [ ] Open PR `master -> staging`, merge after checks pass +- [ ] Open PR `staging -> dev`, merge after checks pass +- [ ] Ensure all three branches now contain the hotfix commit(s) + +### STEP 7: Close Incident + Cleanup + +- [ ] Post closing summary on the issue with: + - root cause + - final fix + - validation evidence (preprod + production) + - follow-up actions (if any) +- [ ] Remove `in-progress` label +- [ ] Close issue with state_reason: `completed` +- [ ] Delete hotfix branches (local + remote) only after downstream merges: + ```powershell + git branch -d hotfix/issue-{ISSUE}-{slug} + git push origin --delete hotfix/issue-{ISSUE}-{slug} + ``` + +--- + +**Non-negotiables:** +- ❌ Never commit directly to `dev`/`staging`/`master` +- ❌ Never skip preprod validation before `hotfix -> master` merge +- ❌ Never leave workflow reliability gaps unverified for deployment incidents +- ❌ Never leave mock data, TODOs, or `dict[str, Any]` in domain +- ❌ Never skip tenant isolation checks +- ✅ Always keep scope to production incident requirements +- ✅ Always document root cause and validation evidence +- ✅ Always merge `master -> staging -> dev` after hotfix production release +- ✅ Always keep docs/config/workflows in sync with the fix diff --git a/.github/workflows/deploy-preprod-hotfix.yml b/.github/workflows/deploy-preprod-hotfix.yml new file mode 100644 index 00000000..1857f4f9 --- /dev/null +++ b/.github/workflows/deploy-preprod-hotfix.yml @@ -0,0 +1,181 @@ +name: Deploy Preprod Hotfix + +on: + push: + branches: + - "hotfix/**" + workflow_dispatch: + inputs: + branch: + description: "Branch to deploy (defaults to current ref)" + required: false + type: string + skip_tests: + description: "Skip tests before deployment" + required: false + default: "false" + type: choice + options: + - "true" + - "false" + +concurrency: + group: deploy-preprod-hotfix-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + pre-deployment-checks: + name: Pre-Deployment Validation + runs-on: ubuntu-latest + if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch || github.ref }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" + enable-cache: true + + - name: Create virtual environment + run: uv venv .venv + + - name: Install dependencies + run: | + source .venv/bin/activate + uv pip install -r coaching/requirements.txt + uv pip install -r coaching/requirements-dev.txt + shell: bash + + - name: Run Ruff Linting + run: | + source .venv/bin/activate + python -m ruff check . --exclude=".venv,venv,__pycache__,.pytest_cache" + shell: bash + + - name: Run MyPy Type Checking + run: | + source .venv/bin/activate + python -m mypy coaching/src/ shared/ --config-file=pyproject.toml + shell: bash + + - name: Run Unit Tests + run: | + source .venv/bin/activate + python -m pytest coaching/tests/unit/ -v --cov=coaching/src --cov-fail-under=70 + shell: bash + env: + PYTHONPATH: coaching:shared:. + + deploy-coaching: + name: Deploy to Preprod + runs-on: ubuntu-latest + needs: [pre-deployment-checks] + if: ${{ always() && (needs.pre-deployment-checks.result == 'success' || (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_tests == 'true')) }} + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch || github.ref }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Pulumi Python dependencies + working-directory: coaching/pulumi + run: pip install -r requirements.txt + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Deploy Coaching Service + uses: pulumi/actions@v5 + with: + command: up + stack-name: preprod + work-dir: coaching/pulumi + env: + PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: us-east-1 + + smoke-tests: + name: Post-Deployment Smoke Tests + runs-on: ubuntu-latest + needs: [deploy-coaching] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch || github.ref }} + + - name: Install Pulumi CLI + uses: pulumi/actions@v5 + with: + pulumi-version: "latest" + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Get API Gateway URL + id: api-url + working-directory: coaching/pulumi + run: | + URL=$(pulumi stack output customDomainUrl --stack preprod) + echo "url=$URL" >> $GITHUB_OUTPUT + env: + PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} + + - name: Health Check + run: | + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${{ steps.api-url.outputs.url }}/health" || echo "000") + if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "404" ]; then + echo "❌ Health check failed with HTTP $HTTP_CODE" + exit 1 + fi + echo "✅ Health check passed ($HTTP_CODE)" + + - name: CORS Preflight Check + run: | + ORIGIN="https://preprod.purposepath.app" + TARGET="${{ steps.api-url.outputs.url }}/api/v1/ai/execute-async" + CORS_HEADERS=$(curl -s -D - -o /dev/null -X OPTIONS "$TARGET" \ + -H "Origin: $ORIGIN" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: Authorization,Content-Type,X-Tenant-Id") + + ALLOW_ORIGIN=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-origin"{print $2}' | tail -n 1) + ALLOW_CREDENTIALS=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-credentials"{print $2}' | tail -n 1) + + if [ "$ALLOW_ORIGIN" != "$ORIGIN" ]; then + echo "❌ Invalid Access-Control-Allow-Origin: '$ALLOW_ORIGIN' (expected '$ORIGIN')" + exit 1 + fi + if [ "$ALLOW_CREDENTIALS" != "true" ]; then + echo "❌ Invalid Access-Control-Allow-Credentials: '$ALLOW_CREDENTIALS' (expected 'true')" + exit 1 + fi + + echo "✅ CORS preflight returned expected headers" diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index c1180b27..4c5e6e36 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -14,11 +14,6 @@ on: options: - 'true' - 'false' - source_image_uri: - description: 'Optional exact image URI (repo@sha256:...) to promote without rebuild' - required: false - default: '' - type: string concurrency: group: deploy-production @@ -31,7 +26,6 @@ jobs: outputs: should_deploy: ${{ steps.promotion-check.outputs.should_deploy }} trigger_reason: ${{ steps.promotion-check.outputs.trigger_reason }} - promotion_source: ${{ steps.promotion-check.outputs.promotion_source }} steps: - name: Evaluate deployment trigger @@ -40,31 +34,27 @@ jobs: run: | SHOULD_DEPLOY="false" TRIGGER_REASON="not-eligible" - PROMOTION_SOURCE="none" + HEAD_REF="${{ github.event.pull_request.head.ref }}" if [ "${{ github.event_name }}" == "pull_request" ] && \ [ "${{ github.event.pull_request.merged }}" == "true" ] && \ [ "${{ github.event.pull_request.base.ref }}" == "master" ] && \ - [ "${{ github.event.pull_request.head.ref }}" == "staging" ]; then + [ "$HEAD_REF" == "staging" ]; then SHOULD_DEPLOY="true" TRIGGER_REASON="merged-staging-to-master-pr" - PROMOTION_SOURCE="staging" elif [ "${{ github.event_name }}" == "pull_request" ] && \ - [ "${{ github.event.pull_request.merged }}" == "true" ] && \ - [ "${{ github.event.pull_request.base.ref }}" == "master" ] && \ - [ "${{ github.event.pull_request.head.ref }}" == "preprod" ]; then + [ "${{ github.event.pull_request.merged }}" == "true" ] && \ + [ "${{ github.event.pull_request.base.ref }}" == "master" ] && \ + [[ "$HEAD_REF" == hotfix/* ]]; then SHOULD_DEPLOY="true" - TRIGGER_REASON="merged-preprod-to-master-pr" - PROMOTION_SOURCE="preprod" + TRIGGER_REASON="merged-hotfix-to-master-pr" elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then SHOULD_DEPLOY="true" TRIGGER_REASON="manual-dispatch" - PROMOTION_SOURCE="manual" fi echo "should_deploy=$SHOULD_DEPLOY" >> "$GITHUB_OUTPUT" echo "trigger_reason=$TRIGGER_REASON" >> "$GITHUB_OUTPUT" - echo "promotion_source=$PROMOTION_SOURCE" >> "$GITHUB_OUTPUT" echo "Promotion check result: $SHOULD_DEPLOY" echo "Reason: $TRIGGER_REASON" @@ -73,7 +63,7 @@ jobs: if: steps.promotion-check.outputs.should_deploy != 'true' run: | echo "No production deployment triggered." - echo "This workflow only deploys on merged PRs from staging/preprod -> master, or manual dispatch." + echo "This workflow deploys on merged PRs from staging/hotfix -> master, or manual dispatch." pre-deployment-checks: name: Pre-Deployment Validation @@ -172,7 +162,6 @@ jobs: runs-on: ubuntu-latest needs: [validate-promotion, deploy-infrastructure] if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && needs.deploy-infrastructure.result == 'success' }} - environment: production permissions: id-token: write contents: write @@ -196,31 +185,6 @@ jobs: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - name: Resolve promotion image URI - id: promotion-image - shell: bash - run: | - IMAGE_URI="" - - if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.source_image_uri }}" ]; then - IMAGE_URI="${{ github.event.inputs.source_image_uri }}" - elif [ "${{ needs.validate-promotion.outputs.trigger_reason }}" == "merged-preprod-to-master-pr" ]; then - ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) - DIGEST=$(aws ecr describe-images \ - --repository-name purposepath-coaching \ - --image-ids imageTag=preprod \ - --query 'imageDetails[0].imageDigest' \ - --output text) - IMAGE_URI="${ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/purposepath-coaching@${DIGEST}" - fi - - echo "image_uri=${IMAGE_URI}" >> "$GITHUB_OUTPUT" - if [ -n "$IMAGE_URI" ]; then - echo "Using promoted image URI: $IMAGE_URI" - else - echo "No explicit image URI provided; deployment may rebuild image." - fi - - name: Deploy Coaching Service uses: pulumi/actions@v5 with: @@ -232,7 +196,6 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: us-east-1 - COACHING_IMAGE_URI: ${{ steps.promotion-image.outputs.image_uri }} - name: Get API Gateway URL id: api-url @@ -243,19 +206,30 @@ jobs: env: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} - - name: Verify promoted image URI - if: ${{ steps.promotion-image.outputs.image_uri != '' }} + - name: Lambda Runtime State Check working-directory: coaching/pulumi - shell: bash run: | - DEPLOYED_IMAGE_URI=$(pulumi stack output deployedImageUri --stack prod) - if [ "$DEPLOYED_IMAGE_URI" != "${{ steps.promotion-image.outputs.image_uri }}" ]; then - echo "Expected image URI: ${{ steps.promotion-image.outputs.image_uri }}" - echo "Deployed image URI: $DEPLOYED_IMAGE_URI" - echo "❌ Deployed image does not match promotion source" - exit 1 - fi - echo "✅ Deployed image matches promotion source" + echo "Validating Lambda runtime state..." + LAMBDA_ARN=$(pulumi stack output lambdaArn --stack prod) + LAMBDA_NAME=${LAMBDA_ARN##*:function:} + + for ATTEMPT in {1..18}; do + STATE=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.State" --output text) + REASON=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.StateReason" --output text) + UPDATE_STATUS=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.LastUpdateStatus" --output text) + + echo "Attempt $ATTEMPT: state=$STATE, updateStatus=$UPDATE_STATUS, reason=$REASON" + + if [ "$STATE" == "Active" ] && [ "$UPDATE_STATUS" == "Successful" ]; then + echo "✅ Lambda state is Active and update status is Successful" + exit 0 + fi + + sleep 10 + done + + echo "❌ Lambda did not reach Active/Successful in expected time window" + exit 1 env: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} @@ -287,8 +261,6 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Environment:** Production" >> $GITHUB_STEP_SUMMARY echo "**Stack:** prod" >> $GITHUB_STEP_SUMMARY - echo "**Promotion source:** ${{ needs.validate-promotion.outputs.promotion_source }}" >> $GITHUB_STEP_SUMMARY - echo "**Promoted image URI:** ${{ steps.promotion-image.outputs.image_uri || 'n/a (rebuild path)' }}" >> $GITHUB_STEP_SUMMARY echo "**API URL:** ${{ steps.api-url.outputs.url }}" >> $GITHUB_STEP_SUMMARY echo "**Region:** us-east-1" >> $GITHUB_STEP_SUMMARY echo "**Release:** v${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY @@ -339,28 +311,37 @@ jobs: - name: CORS Preflight Check run: | echo "Testing CORS preflight behavior..." - ORIGIN="https://purposepath.app" TARGET="${{ steps.api-url.outputs.url }}/api/v1/health" - - CORS_HEADERS=$(curl -s -D - -o /dev/null -X OPTIONS "$TARGET" \ - -H "Origin: $ORIGIN" \ - -H "Access-Control-Request-Method: GET" \ - -H "Access-Control-Request-Headers: Authorization,Content-Type") - - ALLOW_ORIGIN=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-origin"{print $2}' | tail -n 1) - ALLOW_CREDENTIALS=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-credentials"{print $2}' | tail -n 1) - - if [ "$ALLOW_ORIGIN" != "$ORIGIN" ]; then - echo "❌ Invalid Access-Control-Allow-Origin: '$ALLOW_ORIGIN' (expected '$ORIGIN')" - exit 1 - fi - - if [ "$ALLOW_CREDENTIALS" != "true" ]; then - echo "❌ Invalid Access-Control-Allow-Credentials: '$ALLOW_CREDENTIALS' (expected 'true')" - exit 1 - fi - - echo "✅ CORS preflight returned expected headers" + ORIGINS=( + "https://dev.purposepath.app" + "https://staging.purposepath.app" + "https://preprod.purposepath.app" + "https://www.purposepath.app" + "https://purposepath.app" + ) + + for ORIGIN in "${ORIGINS[@]}"; do + echo "Checking origin: $ORIGIN" + CORS_HEADERS=$(curl -s -D - -o /dev/null -X OPTIONS "$TARGET" \ + -H "Origin: $ORIGIN" \ + -H "Access-Control-Request-Method: GET" \ + -H "Access-Control-Request-Headers: Authorization,Content-Type,Baggage,Sentry-Trace") + + ALLOW_ORIGIN=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-origin"{print $2}' | tail -n 1) + ALLOW_CREDENTIALS=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-credentials"{print $2}' | tail -n 1) + + if [ "$ALLOW_ORIGIN" != "$ORIGIN" ]; then + echo "❌ Invalid Access-Control-Allow-Origin: '$ALLOW_ORIGIN' (expected '$ORIGIN')" + exit 1 + fi + + if [ "$ALLOW_CREDENTIALS" != "true" ]; then + echo "❌ Invalid Access-Control-Allow-Credentials: '$ALLOW_CREDENTIALS' (expected 'true')" + exit 1 + fi + done + + echo "✅ CORS preflight returned expected headers for all production frontend origins" - name: Smoke Test Summary run: | diff --git a/coaching/pulumi/Pulumi.preprod.yaml b/coaching/pulumi/Pulumi.preprod.yaml index 244cf03c..1a38cefb 100644 --- a/coaching/pulumi/Pulumi.preprod.yaml +++ b/coaching/pulumi/Pulumi.preprod.yaml @@ -1,2 +1,2 @@ -config: - aws:region: us-east-1 +config: + aws:region: us-east-1 diff --git a/coaching/pulumi/__main__.py b/coaching/pulumi/__main__.py index 0961d8d1..e8aa7ffa 100644 --- a/coaching/pulumi/__main__.py +++ b/coaching/pulumi/__main__.py @@ -59,6 +59,20 @@ # Safe default for permanent preprod: keep async background execution off unless explicitly enabled. "ai_async_jobs_enabled": "false", }, + "preprod": { + "infra_stack": "mottych/purposepath-infrastructure/preprod", + "coaching_infra_stack": "mottych/purposepath-coaching-infrastructure/preprod", + "api_domain": "api.preprod.purposepath.app", + "certificate_output": "apiPreprod", + "jwt_secret": "purposepath-jwt-secret-preprod", + "openai_api_key_secret": "purposepath/preprod/openai-api-key", + "google_vertex_credentials_secret": "purposepath/preprod/google-vertex-credentials", + "jwt_issuer": "https://api.preprod.purposepath.app", + "jwt_audience": "https://preprod.purposepath.app", + "account_api_url": "https://api.preprod.purposepath.app", + "business_api_base_url": "https://api.preprod.purposepath.app/account/api/v1", + "log_level": "INFO", + }, "prod": { "infra_stack": "mottych/purposepath-infrastructure/prod", "coaching_infra_stack": "mottych/purposepath-coaching-infrastructure/prod", @@ -314,21 +328,10 @@ ), ) -# Reuse shared ECR repository when it already exists. -# This avoids cross-stack repository creation conflicts in production. -try: - existing_ecr_repo = aws.ecr.get_repository(name="purposepath-coaching") - ecr_repository_url = pulumi.Output.from_input(existing_ecr_repo.repository_url) -except Exception: - ecr_repo = aws.ecr.Repository( - "coaching-repo", - name="purposepath-coaching", - image_scanning_configuration=aws.ecr.RepositoryImageScanningConfigurationArgs( - scan_on_push=True - ), - force_delete=True, - ) - ecr_repository_url = ecr_repo.repository_url +# Use the shared ECR repository managed outside this stack. +# Do not create/delete this repository from service deployments. +existing_ecr_repo = aws.ecr.get_repository(name="purposepath-coaching") +ecr_repository_url = pulumi.Output.from_input(existing_ecr_repo.repository_url) # Build and push Docker image auth_token = aws.ecr.get_authorization_token() @@ -394,13 +397,30 @@ ) # API Gateway HTTP API -# IMPORTANT: CORS is intentionally handled only in FastAPI middleware. -# Keeping API Gateway CORS enabled created split-brain behavior where -# preflight responses could come from APIGW ("*") while app responses came -# from FastAPI (credential-aware origin regex), causing intermittent browser failures. +# CORS is configured at the gateway layer so browser preflight OPTIONS requests +# always receive CORS headers, even when requests are rejected before FastAPI. api = aws.apigatewayv2.Api( "coaching-api", protocol_type="HTTP", + cors_configuration={ + "allow_credentials": True, + "allow_headers": ["*"], + "allow_methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + "allow_origins": [ + "https://dev.purposepath.app", + "https://staging.purposepath.app", + "https://preprod.purposepath.app", + "https://www.purposepath.app", + "https://purposepath.app", + ], + "expose_headers": [ + "X-Request-Id", + "X-RateLimit-Limit", + "X-RateLimit-Remaining", + "X-RateLimit-Reset", + ], + "max_age": 3600, + }, ) integration = aws.apigatewayv2.Integration( diff --git a/coaching/src/api/main.py b/coaching/src/api/main.py index e4965fca..c2245c81 100644 --- a/coaching/src/api/main.py +++ b/coaching/src/api/main.py @@ -118,17 +118,9 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: "allow_origin_regex": r"(^https://([a-zA-Z0-9-]+\.)*(purposepath|purpopsepath)\.app$)|(^http://localhost:\d+$)", "allow_credentials": True, "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], - "allow_headers": [ - "Content-Type", - "Authorization", - "X-Requested-With", - "Accept", - "Origin", - "X-Api-Key", - "X-Tenant-Id", - "X-User-Id", - "X-CSRF-Token", - ], + # Allow all request headers to prevent preflight breakage when frontend tooling + # adds non-static headers (for example tracing/monitoring headers). + "allow_headers": ["*"], "expose_headers": [ "X-Request-Id", "X-RateLimit-Limit", diff --git a/docs/shared/Specifications/ai-admin/admin_ai_specifications.md b/docs/shared/Specifications/ai-admin/admin_ai_specifications.md index 65f9f8fd..4bce156a 100644 --- a/docs/shared/Specifications/ai-admin/admin_ai_specifications.md +++ b/docs/shared/Specifications/ai-admin/admin_ai_specifications.md @@ -1,21 +1,13 @@ # Admin AI Specifications - LLM Topic Management -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - Last Updated: February 13, 2026 - Version: 3.1 -======== -- Last Updated: January 30, 2026 -- Version: 3.0 ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md ## Revision History | Date | Version | Description | |------|---------|-------------| -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md | 2026-02-13 | 3.1 | Synced spec to implementation for admin model responses, topic auth, topic type terminology, and conversation extraction config. Updated `/models` response shape (`ApiResponse[LLMModelsResponse]`), enforced admin role on topics routes, standardized `measure_system`, updated `conversation_config.max_turns`, and documented extraction model behavior/defaults. | -======== ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md | 2026-01-30 | 3.0 | **Issue #158 Completion:** Added tier-based LLM model selection and topic access control. Replaced `model_code` with `basic_model_code` and `premium_model_code`. Added `tier_level` field (FREE, BASIC, PREMIUM, ULTIMATE). | | 2026-01-25 | 2.0 | **Issue #196 Completion:** Fixed category enum values to match actual TopicCategory implementation, verified all field values match constants.py | | 2025-12-25 | 1.0 | Initial admin specification | @@ -26,11 +18,7 @@ This document specifies all admin endpoints for managing the LLM Topic system. Admin users can update topic configurations, manage prompts, and test topics. -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md **Important:** Most topics are defined in the code-based `endpoint_registry`, but admin create/delete endpoints also exist in the API (currently not used by the Admin UI). In practice, admins mainly: -======== -**Important:** Topics are defined in the code-based `endpoint_registry` and cannot be created or deleted by admins. Admins can only: ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md - Update topic configurations (tier level, dual LLM models, temperature, prompts, etc.) - Manage prompt content (system, user, assistant prompts) - Test topic configurations before activation @@ -68,29 +56,19 @@ Each topic has a `tier_level` that controls: | GET /models | ✅ Implemented | | | POST /topics/validate | ✅ Implemented | | | POST /topics/{topic_id}/test | ✅ Implemented | **New** - Test with auto-enrichment | -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md | GET /topics/stats | ✅ Implemented | Dashboard metrics endpoint used by LLM dashboard | -======== ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md | GET /topics/{topic_id}/stats | ⏳ Planned | Usage statistics | --- ## Authentication -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md All endpoints in this document require a bearer token: - **Authentication**: `Authorization: Bearer {token}` must be present and valid - **Authorization (current implementation)**: - `/api/v1/admin/models*` endpoints enforce admin access (`ADMIN` or `OWNER`) via `require_admin_access` - `/api/v1/admin/topics*` endpoints enforce admin access (`ADMIN` or `OWNER`) via `require_admin_access` -======== -All admin endpoints require: - -- **Authentication**: Bearer token with admin role -- **Authorization**: `admin:topics:*` permission scope ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md - **Headers**: - `Authorization: Bearer {token}` - `Content-Type: application/json` @@ -131,13 +109,8 @@ GET /api/v1/admin/topics "category": "core_values", "topic_type": "conversation_coaching", "tier_level": "free", -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "basic_model_code": "CLAUDE_3_5_HAIKU", "premium_model_code": "CLAUDE_3_5_SONNET_V2", -======== - "basic_model_code": "claude-3-5-sonnet-20241022", - "premium_model_code": "claude-3-5-sonnet-20241022", ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md "temperature": 0.7, "max_tokens": 2000, "is_active": true, @@ -207,13 +180,8 @@ GET /api/v1/admin/topics/{topic_id} "topic_type": "conversation_coaching", "description": "Explore your core values through conversation", "tier_level": "free", -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "basic_model_code": "CLAUDE_3_5_HAIKU", "premium_model_code": "CLAUDE_3_5_SONNET_V2", -======== - "basic_model_code": "claude-3-5-sonnet-20241022", - "premium_model_code": "claude-3-5-sonnet-20241022", ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md "temperature": 0.7, "max_tokens": 2000, "top_p": 1.0, @@ -280,11 +248,7 @@ GET /api/v1/admin/topics/{topic_id} "max_messages_to_llm": 30, "inactivity_timeout_minutes": 30, "session_ttl_days": 14, -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "max_turns": 20, -======== - "estimated_messages": 20, ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md "extraction_model_code": "CLAUDE_3_5_HAIKU" }, "response_schema": null, @@ -353,7 +317,6 @@ For topics with `topic_type: "conversation_coaching"`, the response includes `co | `max_messages_to_llm` | integer | 5-100 | 30 | Maximum messages to include in LLM context (sliding window) | | `inactivity_timeout_minutes` | integer | 5-1440 | 30 | Minutes of inactivity before session auto-pauses | | `session_ttl_days` | integer | 1-90 | 14 | Days to keep paused/completed sessions before deletion | -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md | `max_turns` | integer | 0-100 | 0 | Maximum conversation turns (0 means unlimited) | | `extraction_model_code` | string | - | CLAUDE_3_5_HAIKU | MODEL_REGISTRY code for extraction (e.g., CLAUDE_3_5_HAIKU, CLAUDE_3_5_SONNET_V2) | @@ -369,11 +332,6 @@ For `conversation_coaching` completion/extraction: - Extraction call runs with `temperature=0.3` - Extraction max tokens are capped to `min(8192, extraction_model.max_tokens)` -======== -| `estimated_messages` | integer | 5-100 | 20 | Estimated messages for a typical session (for progress calculation) | -| `extraction_model_code` | string | - | CLAUDE_3_5_HAIKU | MODEL_REGISTRY code for extraction (e.g., CLAUDE_3_5_HAIKU, CLAUDE_3_5_SONNET_V2) | - ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md **Template Status:** The `template_status` array shows each allowed template and its definition status: @@ -413,20 +371,14 @@ POST /api/v1/admin/topics "topic_type": "conversation_coaching", "description": "Discover your life's purpose through guided conversation", "tier_level": "free", -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "basic_model_code": "CLAUDE_3_5_HAIKU", "premium_model_code": "CLAUDE_3_5_SONNET_V2", -======== - "basic_model_code": "claude-3-5-sonnet-20241022", - "premium_model_code": "claude-3-5-sonnet-20241022", ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md "temperature": 0.7, "max_tokens": 2000, "top_p": 1.0, "frequency_penalty": 0.0, "presence_penalty": 0.0, "is_active": false, -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "display_order": 10 } ``` @@ -436,45 +388,6 @@ POST /api/v1/admin/topics - `CreateTopicRequest` does not accept `conversation_config` or `allowed_parameters` - For `conversation_coaching` topics, configure extraction settings using `PUT /api/v1/admin/topics/{topic_id}` with `conversation_config` - `allowed_parameters` are derived from endpoint registry and returned by topic detail endpoints -======== - "display_order": 10, - "allowed_parameters": [ - { - "name": "user_name", - "type": "string", - "required": true, - "description": "User's display name" - }, - { - "name": "core_values", - "type": "string", - "required": false, - "description": "User's defined core values" - } - ] -} -``` - -**Allowed Parameter Types:** - -- `string`: Text value -- `integer`: Whole number -- `float`: Decimal number -- `boolean`: true/false -- `array`: List of values -- `object`: Nested structure - -**Parameter Definition Schema:** - -```json -{ - "name": "parameter_name", - "type": "string|integer|float|boolean|array|object", - "required": true, - "description": "Human-readable description" -} -``` ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md **Validation Rules:** @@ -482,19 +395,11 @@ POST /api/v1/admin/topics |-------|-------|------------------------| | `topic_id` | Required, unique, lowercase, snake_case, 3-50 chars | Regex: `^[a-z][a-z0-9_]*$` | | `topic_name` | Required, 3-100 chars | Any printable characters | -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md | `category` | Required | String (not enum-validated in request model) | | `topic_type` | Required | Enum currently validated by create model: `conversation_coaching`, `single_shot`, `measure_system` | | `tier_level` | Optional, default `free` | Enum: `free`, `basic`, `premium`, `ultimate` | | `basic_model_code` | Required, must be valid model code | Use `GET /api/v1/admin/models` values (used for FREE/BASIC tiers) | | `premium_model_code` | Required, must be valid model code | Use `GET /api/v1/admin/models` values (used for PREMIUM/ULTIMATE tiers) | -======== -| `category` | Required | Enum: `onboarding`, `conversation`, `insights`, `strategic_planning`, `operations_ai`, `operations_strategic_integration`, `analysis` | -| `topic_type` | Required | Enum: `conversation_coaching`, `single_shot`, `measure_system` | -| `tier_level` | Optional, default `free` | Enum: `free`, `basic`, `premium`, `ultimate` | -| `basic_model_code` | Required, must be valid model code | See "Supported Model Codes" below (used for FREE/BASIC tiers) | -| `premium_model_code` | Required, must be valid model code | See "Supported Model Codes" below (used for PREMIUM/ULTIMATE tiers) | ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md | `temperature` | Required, float | 0.0-2.0 | | `max_tokens` | Required, integer | 1-100000 (model dependent) | | `top_p` | Optional, float, default 1.0 | 0.0-1.0 | @@ -506,7 +411,6 @@ POST /api/v1/admin/topics **Supported Model Codes:** -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md Model codes are sourced from `MODEL_REGISTRY` and should be retrieved from `GET /api/v1/admin/models`. Examples currently in use: @@ -524,37 +428,12 @@ Examples currently in use: - `operations_ai` - `operations_strategic_integration` - `analysis` -======== -- `claude-3-5-sonnet-20241022` (recommended) -- `claude-3-5-haiku-20241022` -- `claude-3-opus-20240229` -- `claude-3-sonnet-20240229` -- `claude-3-haiku-20240307` -- `gpt-4o` -- `gpt-4-turbo` -- `gpt-4` -- `gpt-3.5-turbo` - -**Category Descriptions:** - -- `core_values`: Topics related to identifying and exploring personal values -- `purpose`: Life purpose and meaning discovery -- `vision`: Future vision and aspiration setting -- `goals`: Goal setting and achievement planning -- `strategy`: Strategic planning and decision making -- `measure`: Key performance indicators and metrics -- `custom`: Custom topics not fitting standard categories ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md **Topic Type Descriptions:** - `conversation_coaching`: Interactive conversational coaching sessions (multi-turn) - `single_shot`: One-shot evaluations, assessments, and analysis -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `measure_system`: KPI/measure system topic type accepted by current create endpoint validator -======== -- `measure_system`: Measure calculation and tracking ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md **Prompt Types by Topic Type:** @@ -562,11 +441,7 @@ Examples currently in use: |------------|-----------------|-------------| | `conversation_coaching` | `system`, `initiation`, `resume`, `extraction` | System defines coach behavior; initiation starts new sessions; resume continues paused sessions; extraction captures results | | `single_shot` | `system`, `user` | System defines behavior; user template with parameters | -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md | `measure_system` | `system`, `user` | System defines KPI/measure calculation behavior; user template for input | -======== -| `measure_system` | `system`, `user` | System defines calculation behavior; user template for input | ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md **Response:** @@ -626,13 +501,8 @@ PUT /api/v1/admin/topics/{topic_id} "topic_name": "Core Values - Updated Name", "description": "Updated description", "tier_level": "basic", -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "basic_model_code": "CLAUDE_3_5_HAIKU", "premium_model_code": "CLAUDE_3_5_SONNET_V2", -======== - "basic_model_code": "claude-3-5-haiku-20241022", - "premium_model_code": "claude-3-5-sonnet-20241022", ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md "temperature": 0.5, "max_tokens": 1500, "is_active": true, @@ -641,23 +511,9 @@ PUT /api/v1/admin/topics/{topic_id} "max_messages_to_llm": 30, "inactivity_timeout_minutes": 45, "session_ttl_days": 14, -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "max_turns": 25, "extraction_model_code": "CLAUDE_3_5_SONNET_V2" } -======== - "estimated_messages": 25, - "extraction_model_code": "CLAUDE_3_5_SONNET_V2" - }, - "allowed_parameters": [ - { - "name": "user_name", - "type": "string", - "required": true, - "description": "User's display name" - } - ] ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md } ``` @@ -667,11 +523,7 @@ PUT /api/v1/admin/topics/{topic_id} - Cannot update `topic_id` - Cannot update `category` or `topic_type` (create new topic instead) - Cannot update `created_at` or `created_by` -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `allowed_parameters` is not part of `UpdateTopicRequest`; allowed parameters are derived from endpoint registry -======== -- `allowed_parameters` replaces entire list when provided ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md - `conversation_config` is only applicable for `conversation_coaching` topic types **Response:** @@ -745,11 +597,7 @@ GET /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} | Parameter | Type | Required | Description | Allowed Values | |-----------|------|----------|-------------|----------------| | `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md | `prompt_type` | string | Yes | Type of prompt | Any `PromptType` value. Effective values are constrained by topic registry allowed prompt types | -======== -| `prompt_type` | string | Yes | Type of prompt | Enum: `system`, `user`, `assistant` | ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md **Response:** @@ -767,12 +615,8 @@ GET /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} **Status Codes:** - `200 OK`: Success -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `404 Not Found`: Prompt not found on topic - `422 Unprocessable Entity`: Topic not found in DB/registry, or topic exists in registry but no prompts are stored yet -======== -- `404 Not Found`: Topic or prompt not found ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md - `401 Unauthorized`: Missing or invalid auth token - `403 Forbidden`: Insufficient permissions @@ -793,11 +637,7 @@ PUT /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} | Parameter | Type | Required | Description | Allowed Values | |-----------|------|----------|-------------|----------------| | `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md | `prompt_type` | string | Yes | Type of prompt | Any `PromptType` value. Effective values are constrained by topic registry allowed prompt types | -======== -| `prompt_type` | string | Yes | Type of prompt | Enum: `system`, `user`, `assistant` | ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md **Request Body:** @@ -821,11 +661,7 @@ PUT /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} "prompt_type": "system", "s3_key": "prompts/core_values_coaching/system.md", "updated_at": "2024-11-13T16:30:00Z", -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "version": null, -======== - "version": "1.2.0", ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md "message": "Prompt updated successfully" } ``` @@ -833,13 +669,8 @@ PUT /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} **Status Codes:** - `200 OK`: Success -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `404 Not Found`: Prompt not found on topic - `422 Unprocessable Entity`: Invalid/disallowed prompt type, or topic not found in DB/registry -======== -- `400 Bad Request`: Validation error -- `404 Not Found`: Topic not found ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md - `401 Unauthorized`: Missing or invalid auth token - `403 Forbidden`: Insufficient permissions @@ -866,11 +697,7 @@ POST /api/v1/admin/topics/{topic_id}/prompts **Validation:** -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `prompt_type`: Required, any `PromptType` value. Must be allowed for the specific topic by endpoint registry rules. -======== -- `prompt_type`: Required, enum: `system`, `user`, `assistant` ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md - `content`: Required, markdown text, 1-50,000 chars, UTF-8 encoded **Response:** @@ -888,15 +715,9 @@ POST /api/v1/admin/topics/{topic_id}/prompts **Status Codes:** - `201 Created`: Success -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `422 Unprocessable Entity`: Validation error (invalid prompt type or disallowed prompt type/topic mismatch) - `409 Conflict`: Prompt type already exists - `422 Unprocessable Entity`: Topic not found in DB/registry -======== -- `400 Bad Request`: Validation error -- `409 Conflict`: Prompt type already exists -- `404 Not Found`: Topic not found ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md - `401 Unauthorized`: Missing or invalid auth token - `403 Forbidden`: Insufficient permissions @@ -924,10 +745,7 @@ DELETE /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} - `200 OK`: Success - `404 Not Found`: Prompt not found -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `422 Unprocessable Entity`: Topic not found in database -======== ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md - `401 Unauthorized`: Missing or invalid auth token - `403 Forbidden`: Insufficient permissions @@ -947,7 +765,6 @@ GET /api/v1/admin/models ```json { -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "success": true, "data": { "models": [ @@ -973,43 +790,11 @@ GET /api/v1/admin/models - Field names follow current API aliases (`modelName`, `maxTokens`, `costPer1kTokens`, `isActive`, `totalCount`) - Pricing is returned as a single `costPer1kTokens` value -======== - "models": [ - { - "model_code": "claude-3-5-sonnet-20241022", - "model_name": "Claude 3.5 Sonnet", - "provider": "anthropic", - "capabilities": ["chat", "function_calling"], - "context_window": 200000, - "max_output_tokens": 4096, - "cost_per_input_million": 3.00, - "cost_per_output_million": 15.00, - "is_active": true - }, - { - "model_code": "claude-3-5-haiku-20241022", - "model_name": "Claude 3.5 Haiku", - "provider": "anthropic", - "capabilities": ["chat"], - "context_window": 200000, - "max_output_tokens": 4096, - "cost_per_input_million": 0.80, - "cost_per_output_million": 4.00, - "is_active": true - } - ] -} -``` - ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md **Status Codes:** - `200 OK`: Success - `401 Unauthorized`: Missing or invalid auth token -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `403 Forbidden`: Insufficient permissions -======== ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md --- @@ -1029,19 +814,11 @@ POST /api/v1/admin/topics/validate { "topic_id": "test_topic", "topic_name": "Test Topic", -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "category": "analysis", "topic_type": "single_shot", "tier_level": "free", "basic_model_code": "CLAUDE_3_5_HAIKU", "premium_model_code": "CLAUDE_3_5_SONNET_V2", -======== - "category": "custom", - "topic_type": "single_shot", - "tier_level": "free", - "basic_model_code": "claude-3-5-sonnet-20241022", - "premium_model_code": "claude-3-5-sonnet-20241022", ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md "temperature": 0.7, "max_tokens": 2000, "prompts": [ @@ -1049,16 +826,6 @@ POST /api/v1/admin/topics/validate "prompt_type": "system", "content": "Test system prompt with {user_name}" } -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md -======== - ], - "allowed_parameters": [ - { - "name": "user_name", - "type": "string", - "required": true - } ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md ] } ``` @@ -1088,15 +855,7 @@ POST /api/v1/admin/topics/validate } ], "warnings": [ -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "Temperature 1.2 is high; may produce less consistent results" -======== - { - "field": "temperature", - "message": "High temperature may produce inconsistent results", - "code": "HIGH_TEMPERATURE" - } ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md ] } ``` @@ -1104,14 +863,9 @@ POST /api/v1/admin/topics/validate **Status Codes:** - `200 OK`: Validation complete (check `valid` field) -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md - `422 Unprocessable Entity`: Malformed request body / schema validation error - `401 Unauthorized`: Missing or invalid auth token - `403 Forbidden`: Insufficient permissions -======== -- `400 Bad Request`: Malformed request -- `401 Unauthorized`: Missing or invalid auth token ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md --- @@ -1175,13 +929,8 @@ POST /api/v1/admin/topics/{topic_id}/test "response_model": "WebsiteScanResponse", "response_schema": {"title": "WebsiteScanResponse", "type": "object", "properties": {"scan_id": {"type": "string"}, "captured_at": {"type": "string"}}}, "llm_metadata": { -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md "provider": "bedrock", "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", -======== - "provider": "anthropic", - "model": "claude-3-5-sonnet-20241022", ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md "usage": {"prompt_tokens": 1200, "completion_tokens": 600, "total_tokens": 1800}, "finish_reason": "stop" }, @@ -1212,7 +961,6 @@ POST /api/v1/admin/topics/{topic_id}/test --- -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md ### 13. Get Dashboard Topic Stats **Purpose:** Retrieve admin dashboard-level LLM metrics (templates, model utilization, interactions summary, system health). @@ -1284,8 +1032,6 @@ GET /api/v1/admin/topics/stats --- -======== ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md ### 14. Get Topic Usage Statistics (Planned) **Status:** ⏳ Not yet implemented @@ -1350,24 +1096,8 @@ Topics are defined in the `endpoint_registry` code. Admins configure them by: ## Error Codes -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md Error payloads are returned as FastAPI `HTTPException` details and are endpoint-specific. Use per-endpoint status code tables above as the source of truth. -======== -| Code | HTTP Status | Meaning | -|------|-------------|---------| -| `TOPIC_NOT_FOUND` | 404 | Topic ID does not exist | -| `TOPIC_EXISTS` | 409 | Topic ID already taken | -| `INVALID_TOPIC_ID` | 400 | Topic ID format invalid | -| `INVALID_MODEL` | 400 | Model code not recognized | -| `PROMPT_NOT_FOUND` | 404 | Prompt type not found | -| `PROMPT_EXISTS` | 409 | Prompt type already exists | -| `VALIDATION_ERROR` | 400 | Request validation failed | -| `UNAUTHORIZED` | 401 | Missing or invalid auth | -| `FORBIDDEN` | 403 | Insufficient permissions | -| `S3_ERROR` | 500 | Cloud storage error | -| `CACHE_ERROR` | 500 | Cache operation failed | ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md --- @@ -1391,26 +1121,10 @@ X-RateLimit-Reset: 1699987200 ## Permissions -<<<<<<<< HEAD:docs/shared/Specifications/ai-admin/admin_ai_specifications.md Current backend enforcement: - `GET/PUT /api/v1/admin/models*`: requires admin role (`ADMIN` or `OWNER`) via `require_admin_access` - `/api/v1/admin/topics*`: requires admin role (`ADMIN` or `OWNER`) via `require_admin_access` -======== -Required permission scopes: - -| Action | Permission | -|--------|-----------| -| List topics | `admin:topics:read` | -| View topic | `admin:topics:read` | -| Create topic | `admin:topics:write` | -| Update topic | `admin:topics:write` | -| Delete topic | `admin:topics:delete` | -| View prompts | `admin:topics:read` | -| Update prompts | `admin:prompts:write` | -| Test topic | `admin:topics:write` | -| View stats | `admin:topics:stats` | ->>>>>>>> origin/staging:docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md --- diff --git a/docs/shared/Specifications/api-fe/traction-service/README.md b/docs/shared/Specifications/api-fe/traction-service/README.md index dca4beb3..a26be8ff 100644 --- a/docs/shared/Specifications/api-fe/traction-service/README.md +++ b/docs/shared/Specifications/api-fe/traction-service/README.md @@ -1,7 +1,7 @@ # Traction Service API Specifications (v7) **Version:** 7.0 -**Last Updated:** March 3, 2026 +**Last Updated:** December 23, 2025 **Service Base URL:** `{REACT_APP_TRACTION_API_URL}` **Default (Localhost):** `http://localhost:8002` @@ -47,7 +47,6 @@ This directory contains detailed API specifications for the Traction Service, or | [Actions API](./actions-api.md) | ActionsController | 10 endpoints | Action items management | | [Issues API](./issues-api.md) | IssuesController | 5 endpoints | Issue tracking | | [Coaching Insights API](./insights-api.md) | InsightsController | 5 endpoints | AI-generated coaching insights (CRUD) | -| [Integration Service API](./integration-service.md) | ConnectionsController + MeasureIntegrationsController | 12 endpoints | User-facing system connections and measure integration setup | | [Dashboard, Reports & Activities](./dashboard-reports-activities-api.md) | Multiple | 5 endpoints | Command center, reports, activity feeds | > **Note:** People API has been deferred and moved to Account Service for better organizational alignment. @@ -203,12 +202,6 @@ Example: `"2025-12-23T15:30:00.000Z"` ## Version History -### v7.2 (March 3, 2026) -- Updated `integration-service.md` to match implemented `PurposePath.Traction.Lambda` user contracts exactly. -- Corrected endpoint inventory and removed unimplemented user-facing entries from FE contract scope. -- Added explicit parameter lookup endpoint contract (`POST /measure-integrations/parameter-lookup-values`). -- Aligned payload/field names with runtime DTOs (`connectionConfigurationId`, `parameterValues`, `isActive`, etc.). - ### v7.1 (February 25, 2026) - Added measure-definition integration hint contract fields across available-measures and measure create/read/update responses: - `isIntegrationEnabled` diff --git a/docs/shared/Specifications/api-fe/traction-service/integration-service.md b/docs/shared/Specifications/api-fe/traction-service/integration-service.md index e9c07984..d50e8b07 100644 --- a/docs/shared/Specifications/api-fe/traction-service/integration-service.md +++ b/docs/shared/Specifications/api-fe/traction-service/integration-service.md @@ -1,15 +1,14 @@ # Integration Service API Specification (User App) -**Service:** Traction Service (User App Integration APIs) -**Route Prefixes:** `/connections`, `/measure-integrations` -**Version:** 1.2 -**Last Updated:** March 3, 2026 +**Service:** Integration Service +**Base Path:** `/integration` +**Version:** 1.1 +**Last Updated:** February 19, 2026 ## Overview -This specification documents the **implemented user-facing integration APIs** used by the frontend in Settings and Measure flows. - -This file intentionally reflects current runtime behavior in `PurposePath.Traction.Lambda` and excludes admin metadata-definition APIs. +The Integration Service API enables end-user integration workflows in Settings and Measure screens. +This specification covers **user-facing** endpoints only (no admin metadata-definition endpoints). ### Contract Source of Truth @@ -21,16 +20,10 @@ This file intentionally reflects current runtime behavior in `PurposePath.Tracti - Connected systems list with status/diagnostics and measure counts - Add-system bootstrap (provider subaccount + connection persistence) -- Measure integration eligibility/list/get/create/update -- Parameter lookup values from connected systems (`valueKey` + `valueName`) +- Measure integration create/edit/delete with eligibility checks - Snapshot/aggregate scheduling controls and frequency rules -- Save-gating behavior based on current tested-state rules - -### Scope Notes - -- `POST /measure-integrations/{integrationId}/test` is **not** currently exposed by the Traction user controller. -- Integration runtime test/config endpoints exist in `PurposePath.Integration.Lambda` and are not part of this FE user contract. -- Admin metadata-definition APIs are out-of-scope here (see `api-admin` specs). +- Parameter selection with optional lookup values (`valueKey` + `valueName`) +- Test-first lifecycle and save gating based on tested-state validity --- @@ -58,11 +51,11 @@ All endpoints are tenant-scoped and require authenticated user context. | POST | `/connections/{connectionId}/test` | Test connection and refresh health status | | GET | `/connections/{connectionId}/measures` | List integrations under a connection | | GET | `/measure-integrations/eligible-measures` | List measures eligible for integration for selected system | -| GET | `/measure-integrations/eligible` | Alias of eligible-measures endpoint | | GET | `/measure-integrations/{integrationId}` | Get integration details for edit/view | -| POST | `/measure-integrations/parameter-lookup-values` | Fetch lookup options for a parameter | -| POST | `/measure-integrations` | Create integration definition | -| PUT | `/measure-integrations/{integrationId}` | Update integration definition | +| POST | `/measure-integrations` | Create integration (save-gated by tested state) | +| PUT | `/measure-integrations/{integrationId}` | Update integration (save-gated by tested state rules) | +| DELETE | `/measure-integrations/{integrationId}` | Delete integration | +| POST | `/measure-integrations/{integrationId}/test` | Test retrieval only (no measure-data persistence) | --- @@ -85,73 +78,49 @@ interface ConnectionSummaryResponse { } ``` -### MeasureIntegrationDefinitionResponse +### MeasureIntegrationResponse ```typescript -interface MeasureIntegrationDefinitionResponse { - integrationId: string; - measureId: string; - connectionConfigurationId: string; - isActive: boolean; +interface MeasureIntegrationResponse { + integrationId: string; // UUID + measureId: string; // UUID + measureName: string; + connectionId: string; // UUID + systemId: string; // UUID + systemName: string; + + isEnabled: boolean; testStatus: 'NotTested' | 'Tested' | 'Invalidated'; testedAtUtc?: string; - lastTestExecutionId?: string; testFingerprint?: string; - testInvalidationReasonCode?: string; - lastReadingAtUtc?: string; - lastReadingStatus?: 'Success' | 'Failed'; - lastReadingReason?: string; - dataCalculationMethod: 'PreviousPeriod' | 'MovingAverage'; - frequencyValue: number; - frequencyUnit: 'Days' | 'Months'; - lagDaysAfterPeriodEnd: number; - measureTimeZone: string; - parameterValues: Record; -} - -interface IntegrationParameterValueResponse { - isEnabled: boolean; - value?: string; - lookupKey?: string; - lookupDisplayName?: string; -} -``` -### EligibleMeasureForIntegrationResponse + measureType: 'quantitative'; + measureDataType: 'snapshot' | 'aggregate'; + aggregationType?: 'sum' | 'average' | 'count' | 'min' | 'max'; + aggregationPeriod?: 'day' | 'week' | 'month' | 'quarter' | 'year'; + measureIntent: string; -```typescript -interface EligibleMeasureForIntegrationResponse { - measureId: string; - name: string; - unit: string; - dataType?: string; - aggregationType?: string; - aggregationPeriod?: string; - aiQueryIntent?: string; -} -``` + dataCalculationMethod?: 'previousPeriod' | 'movingAverage'; + frequencyValue: number; // integer > 0 + frequencyUnit: 'days' | 'months'; + lagDaysAfterPeriodEnd?: number; // integer >= 0 + measureTimeZone: string; // IANA timezone -### ParameterLookupValues + parameters: IntegrationParameterSelection[]; -```typescript -interface GetParameterLookupValuesRequest { - connectionId: string; - parameterId: string; - systemMeasureConfigId?: string; // mutually exclusive with measureId+systemId - measureId?: string; - systemId?: string; - search?: string; - page?: number; // default 1, min 1 - pageSize?: number; // default 50, min 1, max 200 + lastReadingAtUtc?: string; + lastReadingStatus?: 'Success' | 'Failed'; + lastReadingReason?: string; } -interface ParameterLookupValuesResponse { +interface IntegrationParameterSelection { parameterId: string; - values: Array<{ valueKey: string; valueName: string }>; - page: number; - pageSize: number; - totalCount: number; - totalPages: number; + parameterName: string; + sourceColumnName: string; + dataType: 'string' | 'number' | 'boolean' | 'date' | 'datetime'; + enabled: boolean; + selectedValueKey?: string; + selectedValueName?: string; } ``` @@ -308,32 +277,6 @@ Returns `ConnectionSummaryResponse`. --- -### 4a) Update Connection - -**PATCH** `/connections/{connectionId}` - -Updates editable user metadata for a connection. - -#### Request Body - -```typescript -interface UpdateConnectionRequest { - workspaceContext?: string; - displayName?: string; -} -``` - -#### Response (200) - -Returns `ConnectionSummaryResponse`. - -#### Notes - -- If `workspaceContext` is provided but external provider/reference is missing, update fails with code `EXTERNAL_CONNECTION_REFERENCE_MISSING`. -- Not found returns 404 with `INTEGRATION_CONNECTION_NOT_FOUND`. - ---- - ### 5) Test Connection **POST** `/connections/{connectionId}/test` @@ -364,17 +307,7 @@ Used by expandable row in Connected Systems screen. #### Response (200) -```typescript -interface ConnectionMeasureResponse { - integrationId: string; - measureId: string; - measureName: string; - isEnabled: boolean; - lastReadingAtUtc?: string; - lastReadingStatus?: string; - lastReadingReason?: string; -} -``` +Returns `MeasureIntegrationResponse[]` (list view projection). --- @@ -382,8 +315,6 @@ interface ConnectionMeasureResponse { **GET** `/measure-integrations/eligible-measures?systemId={systemId}` -Alias supported: `GET /measure-integrations/eligible?systemId={systemId}` - Returns measures eligible for new integration under selected system. #### Constraints @@ -395,10 +326,6 @@ Eligible measure must be: - not already connected to another system - quantitative -#### Response (200) - -Returns `EligibleMeasureForIntegrationResponse[]`. - --- ### 8) Create Measure Integration @@ -410,32 +337,36 @@ Returns `EligibleMeasureForIntegrationResponse[]`. ```typescript interface CreateMeasureIntegrationRequest { measureId: string; // required UUID - connectionConfigurationId: string; // required UUID - dataCalculationMethod: 'previousPeriod' | 'movingAverage'; + connectionId: string; // required UUID + isEnabled: boolean; + + dataCalculationMethod?: 'previousPeriod' | 'movingAverage'; frequencyValue: number; // integer > 0 frequencyUnit: 'days' | 'months'; - lagDaysAfterPeriodEnd: number; // integer >= 0 + lagDaysAfterPeriodEnd?: number; // integer >= 0 measureTimeZone: string; // IANA timezone - isActive: boolean; - testFingerprint?: string; - parameterValues: Record; + + testFingerprint: string; // required for create (successful test execution id/fingerprint) } ``` #### Validation Rules -- Create currently requires successful test fingerprint in handler; missing fingerprint fails with `TEST_REQUIRED_BEFORE_CREATE`. -- `connectionConfigurationId` and `measureId` must be valid GUIDs. -- `dataCalculationMethod` and `frequencyUnit` must parse to known enum values. +- Enabled parameters must include both `selectedValueKey` and `selectedValueName`. +- `lagDaysAfterPeriodEnd` required when `dataCalculationMethod=previousPeriod` for aggregate measures. +- Create is rejected without valid tested state/fingerprint. +- Save-gating failure returns deterministic code: `TEST_REQUIRED_BEFORE_CREATE`. -#### Response (200) +#### Response (201) -Returns `MeasureIntegrationDefinitionResponse`. +Returns `MeasureIntegrationResponse`. --- @@ -443,85 +374,60 @@ Returns `MeasureIntegrationDefinitionResponse`. **PUT** `/measure-integrations/{integrationId}` -#### Request Body +Same payload model as create. -```typescript -interface UpdateMeasureIntegrationRequest { - connectionConfigurationId: string; - dataCalculationMethod: 'previousPeriod' | 'movingAverage'; - frequencyValue: number; - frequencyUnit: 'days' | 'months'; - lagDaysAfterPeriodEnd: number; - measureTimeZone: string; - isActive: boolean; - testFingerprint?: string; - parameterValues: Record; -} -``` +`testFingerprint` is optional for update and is only required when template-affecting changes are included. -#### Save-Gating Rules (Current Implementation) +#### Save-Gating Rules -- Template-affecting changes require `testFingerprint`, else fails with `RETEST_REQUIRED_FOR_TEMPLATE_CHANGES`. -- Template-affecting checks currently include: - - connection change - - parameter dictionary/value/lookup changes +- Template-affecting changes require valid tested state before save: + - parameter enable/disable + - parameter value changes - calculation method changes - - lag changes - timezone changes -- Frequency-only changes do not trigger retest. - -#### Response (200) - -Returns `MeasureIntegrationDefinitionResponse`. + - period/lag semantics changes +- Frequency-only changes do not invalidate tested state. +- If template-affecting changes are submitted without a fresh `testFingerprint`, save fails with code `RETEST_REQUIRED_FOR_TEMPLATE_CHANGES`. --- -### 10) Get Parameter Lookup Values +### 10) Test Measure Integration -**POST** `/measure-integrations/parameter-lookup-values` +**POST** `/measure-integrations/{integrationId}/test` -Returns selectable lookup key/name options for a parameter from connected system data. - -#### Request Body - -`GetParameterLookupValuesRequest` (see common data models). - -#### Scope Rules - -- Provide either: - - `systemMeasureConfigId`, or - - `measureId` + `systemId` -- Do not provide both scope modes together. +Runs retrieval path and returns computed value + execution metadata. +Does **not** persist measure actuals/current measure value. #### Response (200) -Returns `ParameterLookupValuesResponse`. - ---- - -### 11) Get Integration Definition - -**GET** `/measure-integrations/{integrationId}` - -Returns full definition for edit/read views. - -#### Response (200) - -Returns `MeasureIntegrationDefinitionResponse`. +```json +{ + "success": true, + "data": { + "integrationId": "f8f8d8b7-81de-42f7-b689-c0d4b709bc3d", + "executionId": "ea619bc1-f4ac-4d75-934b-73fc912af5f0", + "success": true, + "status": "Succeeded", + "actualValue": 125.52, + "measuredAtUtc": "2026-02-19T08:30:00Z", + "windowStartUtc": "2026-01-01T00:00:00Z", + "windowEndUtc": "2026-01-31T23:59:59Z", + "dataSource": "CData", + "errorCode": null, + "errorMessage": null, + "queryMetadata": { + "templateKey": "templates/cdata/revenue/prompt.txt", + "systemType": "CData", + "connectionType": "ApiKey", + "usesExternalReference": false, + "externalProvider": null + } + } +} +``` --- -### 12) Not in Current User Contract - -- `DELETE /measure-integrations/{integrationId}` is not currently exposed in `PurposePath.Traction.Lambda`. -- `POST /measure-integrations/{integrationId}/test` is not currently exposed in `PurposePath.Traction.Lambda`. -- Any FE use of runtime test endpoint must be explicitly routed/documented via separate Integration runtime contract. - ## Error Responses ### Standard Error Envelope @@ -538,12 +444,14 @@ Returns `MeasureIntegrationDefinitionResponse`. ### Common Error Codes -- `INTEGRATION_CONNECTION_NOT_FOUND` -- `EXTERNAL_CONNECTION_REFERENCE_MISSING` -- `TEST_REQUIRED_BEFORE_CREATE` -- `RETEST_REQUIRED_FOR_TEMPLATE_CHANGES` -- `CONNECTION_INVALID` -- `PARAMETER_NOT_FOUND` -- `INVALID_LOOKUP_SCOPE` -- `INVALID_PAGINATION` -- `LOOKUP_NOT_CONFIGURED` +- `CONNECTION_NOT_FOUND` +- `SYSTEM_NOT_ELIGIBLE` +- `MEASURE_NOT_ELIGIBLE` +- `INTEGRATION_NOT_FOUND` +- `ENABLED_PARAMETER_VALUE_REQUIRED` +- `INVALID_TIMEZONE` +- `INVALID_CALCULATION_CONFIGURATION` +- `TEST_REQUIRED_BEFORE_SAVE` +- `TEST_FINGERPRINT_MISMATCH` +- `CONNECTION_TEST_FAILED` +- `EXECUTION_FAILED`