diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b6508a9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + # Prisma generate reads the schema file only — no real DB connection needed. + # All other runtime env vars are dummies so Next.js / turbo don't crash during + # type checking or linting. + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ci_db + NEXTAUTH_SECRET: ci-nextauth-secret-not-real + NEXTAUTH_URL: http://localhost:3002 + JWT_SECRET: ci-jwt-secret-not-real + WEBHOOK_SECRET: ci-webhook-secret-not-real + NEXT_PUBLIC_APP_URL: http://localhost:3002 + NEXT_PUBLIC_PLATFORM_URL: http://localhost:3002 + +jobs: + lint-and-typecheck: + name: Lint & Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10.12.4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Type Check + run: pnpm check-types + + build: + name: Build + runs-on: ubuntu-latest + needs: lint-and-typecheck + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: ci_db + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10.12.4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run Prisma migrations + run: pnpm --filter @governs-ai/db exec prisma migrate deploy + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ci_db + + - name: Build + run: pnpm build + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ci_db + + secret-scan: + name: Secret Scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/platform/app/api/v1/budget/context/route.ts b/apps/platform/app/api/v1/budget/context/route.ts index f22e61a..f1c5ce1 100644 --- a/apps/platform/app/api/v1/budget/context/route.ts +++ b/apps/platform/app/api/v1/budget/context/route.ts @@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@governs-ai/db'; export async function GET(req: NextRequest) { + let orgId: string | undefined; + let userId: string | undefined; + try { // Get API key from header const apiKey = req.headers.get('X-Governs-Key'); @@ -29,8 +32,8 @@ export async function GET(req: NextRequest) { ); } - const orgId = keyRecord.org.id; - const userId = keyRecord.user.id; + orgId = keyRecord.org.id; + userId = keyRecord.user.id; // Get budget limits (user first, then org) const userBudget = await prisma.budgetLimit.findFirst({ @@ -43,29 +46,48 @@ export async function GET(req: NextRequest) { const budgetLimit = Number(userBudget?.monthlyLimit || orgBudget?.monthlyLimit || 0); const budgetType = userBudget ? 'user' : 'organization'; - // For now, return simplified budget context without complex aggregations - // TODO: Add real spend calculations once database queries are working - const result = { + // Compute start-of-current-calendar-month in UTC + const now = new Date(); + const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); + + // Scope spend queries: user-level budget uses userId filter; org-level uses orgId + const spendWhere = budgetType === 'user' + ? { userId, orgId, timestamp: { gte: monthStart } } + : { orgId, timestamp: { gte: monthStart } }; + + const [llmAgg, purchaseAgg] = await Promise.all([ + prisma.usageRecord.aggregate({ + where: spendWhere, + _sum: { cost: true }, + }), + prisma.purchaseRecord.aggregate({ + where: spendWhere, + _sum: { amount: true }, + }), + ]); + + const llm_spend = Number(llmAgg._sum.cost ?? 0); + const purchase_spend = Number(purchaseAgg._sum.amount ?? 0); + const current_spend = llm_spend + purchase_spend; + const remaining_budget = Math.max(0, budgetLimit - current_spend); + + return NextResponse.json({ monthly_limit: budgetLimit, - current_spend: 0, // Simplified for now - llm_spend: 0, - purchase_spend: 0, - remaining_budget: budgetLimit, + current_spend, + llm_spend, + purchase_spend, + remaining_budget, budget_type: budgetType, - }; - - return NextResponse.json(result); + }); - } catch (error) { - console.error('Error fetching budget context:', error); - console.error('Error details:', { - message: error.message, - stack: error.stack, + } catch (error: any) { + console.error('[budget:context] Error fetching budget context', { + message: error?.message, orgId, - userId + userId, }); return NextResponse.json( - { error: 'Failed to fetch budget context', details: error.message }, + { error: 'Failed to fetch budget context' }, { status: 500 } ); } diff --git a/apps/platform/lib/services/context-precheck.ts b/apps/platform/lib/services/context-precheck.ts index b824bcc..2e74ccf 100644 --- a/apps/platform/lib/services/context-precheck.ts +++ b/apps/platform/lib/services/context-precheck.ts @@ -1,5 +1,4 @@ // Context precheck service for PII detection and redaction -// This is a simplified version that integrates with the GovernsAI precheck system interface PrecheckResult { decision: 'allow' | 'redact' | 'block' | 'deny'; @@ -10,17 +9,22 @@ interface PrecheckResult { class ContextPrecheckService { /** - * Check content for PII and governance compliance - * This is a simplified implementation - in production, this would integrate - * with the actual GovernsAI precheck service + * Check content for PII and governance compliance. + * + * @param failMode - Controls error behaviour: + * 'closed' (default) — block storage when the check itself fails (safe for sensitive flows) + * 'open' — allow storage on error (only for non-sensitive, best-effort paths) */ - async check(input: { - content: string; - userId: string; - orgId: string; - tool: string; - scope: string; - }): Promise { + async check( + input: { + content: string; + userId: string; + orgId: string; + tool: string; + scope: string; + }, + failMode: 'open' | 'closed' = 'closed' + ): Promise { try { // For now, implement basic PII detection patterns const piiPatterns = [ @@ -104,7 +108,15 @@ class ContextPrecheckService { } catch (error) { console.error('Context precheck failed:', error); - // Fail open: allow storage but log error + if (failMode === 'closed') { + // Fail closed: block storage when governance check itself cannot run + return { + decision: 'block', + piiTypes: [], + reasons: ['precheck_service_unavailable'], + }; + } + // Fail open: caller opted in explicitly — allow but surface the error reason return { decision: 'allow', piiTypes: [],