aegida-console / docs / superpowers / plans / 2026-08-03-aegida-gate-migration.md
2026-08-03-aegida-gate-migration.md
Raw

Corp UI Aegida Gate Migration Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Move corp-ui from its private gateway/S3 contracts to authenticated OpenAI-compatible aegida-gate model, chat, Files API, and effective-quota contracts while preserving the existing local conversation UX.

Architecture: Keep same-origin Next.js routes as a browser/session boundary. They relay the user's JWT to aegida-gate, translate OpenAI SSE back into the UI's existing plain-text stream, persist only conversation metadata plus opaque gate file IDs, and expose safe model/quota proxies to client hooks.

Tech Stack: Next.js 16, React 19, TypeScript, Vitest/Testing Library, PostgreSQL migrations, jose HS256 JWTs.

Global Constraints

  • Work directly on the user-approved main branch; do not create a worktree.
  • Before writing code, read the relevant Next.js 16 guides under node_modules/next/dist/docs/ as required by AGENTS.md; cite the files read in each task report.
  • Use strict TDD for every production behavior.
  • Preserve and do not overwrite pre-existing uncommitted attachment work. Before each task, compare git status and the task's target files; if a target was already dirty at plan start, stop that task rather than staging the user's changes.
  • The plan-start dirty paths are .env.example, README.md, app/api/chat/route.test.ts, hooks/use-chat.test.tsx, and untracked docker-compose.yml, attachment/storage/migration tests, and scripts/smoke-db.sh. Treat them as user-owned until explicitly cleared or authorized.
  • Browser code calls only same-origin /api/...; only server routes know AEGIDA_GATE_URL.
  • Server routes relay the exact incoming user Bearer token. They never substitute a shared gateway key for user requests.
  • JWTs require exact JWT_ISSUER, JWT_AUDIENCE, AEGIDA_TENANT_ID, tenant_id, and canonical decimal external_user_id; HS256 remains the initial algorithm.
  • Default/fallback model is exactly auto; arbitrary model IDs must match ^[a-z0-9][a-z0-9._-]{0,127}$ rather than a hard-coded union.
  • Chat uses POST /v1/chat/completions with stream:true and parses only OpenAI SSE data: JSON plus [DONE].
  • Attachments use two steps: gate POST /v1/files with purpose=user_data, then native file_id content parts in Chat. No presigned S3 URL is sent to inference.
  • Quota is informational. Disable send only for an explicit exhausted=true; a quota dependency error remains nonfatal UI state.
  • Preserve local conversation IDs/message persistence and the existing X-Conversation-Id, X-User-Message-Id, and X-Assistant-Message-Id browser contract.
  • Do not stage files outside corp-ui or any pre-existing user changes not authored by the current task.

File Structure

  • lib/auth/types.ts, lib/auth/server.ts: extended JWT identity contract.
  • app/api/auth/login/route.ts: signs the full gate identity.
  • db/migrations/004_aegida_gate_models.sql: remove static model checks/default to auto.
  • db/migrations/005_gate_file_ids.sql: retain local attachment metadata with opaque gate IDs.
  • lib/chat/types.ts, lib/chat/models.ts, lib/chat/validation.ts: dynamic model IDs/catalog types.
  • lib/chat/gateway.ts: OpenAI request/SSE adapter.
  • lib/chat/gate-files.ts: server-only Files API client.
  • lib/chat/quota.ts: server-only quota client.
  • app/api/models/route.ts, app/api/me/quota/route.ts: safe same-origin proxies.
  • app/api/attachments/route.ts, app/api/attachments/[id]/route.ts: gate upload/content proxies.
  • lib/db/attachments.ts, lib/db/conversations.ts: local metadata and file reference resolution.
  • app/api/chat/route.ts: forwards JWT and native file references.
  • hooks/use-models.ts, hooks/use-chat.ts: catalog/quota lifecycle.
  • components/chat/model-selector.tsx, chat-header.tsx, chat-app.tsx, chat-composer.tsx: dynamic models and balance UI.
  • .env.example, README.md, k8s/config.yaml, k8s/app.yaml, docker-compose.yml: deployment contract.

Task 1: Issue gate-verifiable user JWTs

Files:

  • Modify: lib/auth/types.ts
  • Modify: lib/auth/types.test.ts
  • Modify: lib/auth/server.ts
  • Modify: lib/auth/server.test.ts
  • Modify: app/api/auth/login/route.ts
  • Modify: app/api/auth/routes.test.ts

Interfaces:

  • Produces: AuthTokenIdentity with internal/external identity and tenant

  • Preserves browser-visible AuthIdentity

  • Produces gate-compatible HS256 issuer/audience claims

  • Step 1: Read the Next.js route-handler/environment guides

Read the complete relevant files found under node_modules/next/dist/docs/ for Route Handlers and environment variables. Record their exact paths in the report before editing.

  • Step 2: Add failing JWT claim tests

With fixed environment values, sign a real token and verify its claims are exactly:

{
  "sub": "11111111-1111-4111-8111-111111111111",
  "email": "user@example.corp",
  "tenant_id": "tenant-1",
  "external_user_id": "42",
  "iss": "corp-ui",
  "aud": "aegida-gate"
}

plus numeric iat/exp. Tests reject wrong/missing issuer, audience, tenant, external ID, algorithm, signature, and expiry. Login-route tests prove the AuthIdentity.externalUserId returned from the DB is passed into signing and the token body is not logged.

  • Step 3: Run auth tests and confirm RED

Run: npm test -- lib/auth/types.test.ts lib/auth/server.test.ts app/api/auth/routes.test.ts

Expected: missing claims and issuer/audience validation.

  • Step 4: Implement the extended token contract

Define:

export type AuthTokenIdentity = Pick<AuthIdentity, "id" | "email" | "externalUserId"> & {
  tenantId: string;
};

signAuthToken receives the authenticated user plus AEGIDA_TENANT_ID, sets protected HS256, subject, issuer, audience, issued/expiry, and private tenant_id/external_user_id. verifyAuthToken passes issuer, audience, and algorithms:["HS256"] to jwtVerify, validates all fields, and returns them. JWT_SECRET remains required and must be at least 32 UTF-8 bytes.

  • Step 5: Verify and commit Task 1

Run: npm test -- lib/auth/types.test.ts lib/auth/server.test.ts app/api/auth/routes.test.ts

git add lib/auth/types.ts lib/auth/types.test.ts lib/auth/server.ts lib/auth/server.test.ts app/api/auth/login/route.ts app/api/auth/routes.test.ts
git commit -m "feat: issue aegida gate identity claims"

Task 2: Discover models dynamically and persist stable slugs

Files:

  • Create: db/migrations/004_aegida_gate_models.sql
  • Modify: lib/chat/types.ts
  • Modify: lib/chat/models.ts
  • Modify: lib/chat/validation.ts
  • Modify: lib/chat/validation.test.ts
  • Create: lib/chat/catalog.ts
  • Create: lib/chat/catalog.test.ts
  • Create: app/api/models/route.ts
  • Create: app/api/models/route.test.ts
  • Create: hooks/use-models.ts
  • Create: hooks/use-models.test.tsx
  • Modify: components/chat/model-selector.tsx
  • Modify: components/chat/chat-header.tsx
  • Modify: components/chat/chat-app.tsx
  • Modify: app/api/me/model/route.ts
  • Modify: lib/auth/types.ts

Interfaces:

  • Produces: type ModelId = string plus isModelId

  • Produces: GET /api/models safe proxy and useModels

  • Produces: dynamic ModelSelector props

  • Step 1: Read Next.js data-fetching and Route Handler docs

Read the relevant complete local Next 16 documentation and record paths in the report.

  • Step 2: Add failing model validation/migration tests

Tests prove auto, qwen, and claude-sonnet.4 are valid; uppercase, slash, leading separator, empty, and 129-byte IDs fail. Migration tests assert both users/conversations default to auto, existing chatgpt|deepseek|qwen values survive, and named static CHECK constraints are replaced with slug-format checks.

  • Step 3: Run focused tests and confirm RED

Run: npm test -- lib/chat/validation.test.ts lib/db/migrate.test.ts

Expected: static union/set rejects dynamic IDs and migration is absent.

  • Step 4: Implement dynamic ID types and migration

Export isModelId(value): value is ModelId with the exact slug rule and reuse it in auth/chat/preference validation. 004 discovers/drops the existing generated check constraints by table/column from pg_constraint, installs explicitly named checks, changes defaults to auto, and is idempotent through the existing migration runner.

  • Step 5: Add failing catalog proxy/hook/component tests

Tests assert /api/models authenticates, relays the exact user Bearer token to ${AEGIDA_GATE_URL}/v1/models, uses cache:no-store, strictly parses OpenAI {object:"list",data:[...]}, removes malformed/duplicate IDs, and guarantees one auto. Upstream auth failure remains 401; other failures return generic 503 without body leakage.

useModels loads once per authenticated user, cancels stale requests, falls back to only {id:"auto",name:"Aegida Auto"}, and selects auto if a persisted model is absent. ModelSelector renders passed models rather than MODELS globals and retains keyboard accessibility.

  • Step 6: Run tests and confirm RED

Run: npm test -- lib/chat/catalog.test.ts app/api/models/route.test.ts hooks/use-models.test.tsx components/chat/chat-app.test.tsx

Expected: proxy/hook absent and selector remains static.

  • Step 7: Implement catalog proxy and dynamic UI

catalog.ts contains only server-safe parsing and toChatModel; descriptions derive from safe capabilities/owner with a generic fallback, never provider config. ChatApp combines useModels and useChat; model persistence accepts any validated current catalog ID, with server-side catalog confirmation before DB update.

  • Step 8: Verify and commit Task 2

Run: npm test -- lib/chat lib/auth/types.test.ts app/api/models hooks/use-models.test.tsx components/chat/chat-app.test.tsx

git add db/migrations/004_aegida_gate_models.sql lib/chat/types.ts lib/chat/models.ts lib/chat/validation.ts lib/chat/validation.test.ts lib/chat/catalog.ts lib/chat/catalog.test.ts app/api/models hooks/use-models.ts hooks/use-models.test.tsx components/chat/model-selector.tsx components/chat/chat-header.tsx components/chat/chat-app.tsx app/api/me/model/route.ts lib/auth/types.ts
git commit -m "feat: discover aegida gate models"

Task 3: Translate OpenAI Chat SSE while preserving the browser stream

Files:

  • Modify: lib/chat/gateway.ts
  • Modify: lib/chat/gateway.test.ts
  • Modify: lib/chat/types.ts
  • Modify: app/api/chat/route.ts
  • Modify: app/api/chat/route.test.ts

Interfaces:

  • Produces: streamGatewayResponse(request, authorization, signal)

  • Consumes: POST /v1/chat/completions OpenAI SSE

  • Preserves: plain UTF-8 stream returned by /api/chat

  • Step 1: Confirm dirty-file ownership before editing

app/api/chat/route.test.ts was dirty at plan creation. If it is still dirty and the changes are not already committed by the user, stop this task and report the exact overlap. Do not stage it implicitly.

  • Step 2: Read Next.js streaming Route Handler docs

Read the complete local Next 16 streaming/Route Handler references and record paths.

  • Step 3: Add failing OpenAI adapter tests

Assert the exact upstream request:

{
  "model": "auto",
  "stream": true,
  "stream_options": {"include_usage": true},
  "messages": [{"role":"user","content":"hello"}]
}

at ${AEGIDA_GATE_URL}/v1/chat/completions, with the exact incoming Authorization header and no service API key. Feed arbitrarily split SSE bytes containing comments, multiple data: lines, JSON delta chunks, a usage/finish chunk, and [DONE]; assert the returned stream contains only concatenated choices[].delta.content. Malformed JSON, OpenAI error envelope, EOF before [DONE], non-2xx, oversize event, cancellation, and prompt/body leakage map to safe GatewayError/abort behavior.

  • Step 4: Run gateway tests and confirm RED

Run: npm test -- lib/chat/gateway.test.ts app/api/chat/route.test.ts

Expected: private payload/plain stream behavior fails expectations.

  • Step 5: Implement the OpenAI SSE adapter

Replace AI_GATEWAY_URL with validated AEGIDA_GATE_URL base semantics. Implement an incremental SSE parser over ReadableStream without buffering the entire response, cap each event at 1 MiB, decode exact OpenAI delta shapes, ignore usage-only chunks, and require [DONE]. streamGatewayResponse requires the caller's canonical Bearer header.

In /api/chat, retain the original Authorization value after authenticateRequest, pass it to the adapter, and keep persistence/browser headers unchanged. Remove private attachments[].url construction; Task 4 supplies native file parts.

  • Step 6: Verify and commit Task 3

Run: npm test -- lib/chat/gateway.test.ts app/api/chat/route.test.ts hooks/use-chat.test.tsx

Stage only task-authored hunks. If that cannot be proven for the dirty tests, stop rather than commit user work.

git add lib/chat/gateway.ts lib/chat/gateway.test.ts lib/chat/types.ts app/api/chat/route.ts
git commit -m "feat: stream chat through aegida gate"

Task 4: Store attachments through the gate Files API

Files:

  • Create: db/migrations/005_gate_file_ids.sql
  • Create: lib/chat/gate-files.ts
  • Create: lib/chat/gate-files.test.ts
  • Modify: lib/db/attachments.ts
  • Modify: lib/db/attachments.test.ts
  • Modify: lib/db/conversations.ts
  • Modify: app/api/attachments/route.ts
  • Modify: app/api/attachments/[id]/route.ts
  • Modify: app/api/attachments/[id]/route.test.ts
  • Modify: app/api/chat/route.ts
  • Modify: app/api/chat/route.test.ts
  • Modify: lib/chat/types.ts

Interfaces:

  • Produces: gate upload/get-content/delete client using user Bearer

  • Produces local attachment metadata with opaque gate_file_id

  • Produces OpenAI native type:"file" content blocks

  • Step 1: Confirm dirty-file ownership before editing

Several attachment tests and app/api/chat/route.test.ts were dirty/untracked at plan creation. If they remain user-owned, stop and report the overlap. Never delete or absorb the existing S3 work into a task commit without explicit authorization.

  • Step 2: Add failing Files client tests

Assert upload forwards multipart file and purpose:user_data to ${AEGIDA_GATE_URL}/v1/files, exact user Authorization, request cancellation, and no whole-file arrayBuffer conversion. Strictly parse OpenAI FileObject file- ID/name/bytes. Content/delete methods use authenticated standard paths. Non-2xx, oversized JSON, malformed ID, and dependency body sentinels map to safe errors.

  • Step 3: Add failing DB migration/metadata tests

005 adds unique non-null gate_file_id for new rows, makes legacy object_key nullable, and retains old rows for download compatibility. New create/attach/get functions return {id,gateFileId,name,contentType,size} with ownership predicates. Tests prove DB failure after gate upload triggers authenticated gate delete compensation.

  • Step 4: Run focused tests and confirm RED

Run: npm test -- lib/chat/gate-files.test.ts lib/db/attachments.test.ts lib/db/migrate.test.ts app/api/attachments

Expected: gate client/schema absent.

  • Step 5: Implement upload/content proxy and native chat references

Upload route authenticates, locally validates current UI limits, streams the original File to Gate, then persists only returned metadata/ID. Download route resolves the owned row and streams Gate content with the same Authorization; legacy object-key rows continue through the old path during migration.

When attaching to a message, app/api/chat builds the latest user message content as:

[
  {"type":"text","text":"optional text"},
  {"type":"file","file":{"file_id":"file-opaque"}}
]

Earlier text-only history stays string content. Attachment-only requests omit the text block. No object key or presigned URL leaves corp-ui.

  • Step 6: Verify and commit Task 4

Run: npm test -- lib/chat/gate-files.test.ts lib/db app/api/attachments app/api/chat/route.test.ts components/chat/chat-message.test.tsx

Stage only known task-authored files/hunks.

git add db/migrations/005_gate_file_ids.sql lib/chat/gate-files.ts lib/chat/gate-files.test.ts lib/db/attachments.ts lib/db/conversations.ts app/api/attachments app/api/chat/route.ts lib/chat/types.ts
git commit -m "feat: store attachments in aegida gate"

Task 5: Display effective token availability

Files:

  • Create: lib/chat/quota.ts
  • Create: lib/chat/quota.test.ts
  • Create: app/api/me/quota/route.ts
  • Create: app/api/me/quota/route.test.ts
  • Modify: hooks/use-chat.ts
  • Modify: hooks/use-chat.test.tsx
  • Modify: components/chat/chat-header.tsx
  • Modify: components/chat/chat-app.tsx
  • Modify: components/chat/chat-composer.tsx
  • Modify: components/chat/chat-app.test.tsx

Interfaces:

  • Produces safe same-origin GET /api/me/quota?model=...

  • Produces UseChatResult.quota and refresh lifecycle

  • Consumes Aegida quota snapshot, never FinOps directly

  • Step 1: Confirm dirty hook ownership and read data-fetching docs

hooks/use-chat.test.tsx was dirty at plan creation. Stop if still user-owned. Read the complete local Next/React data-fetching guidance relevant to cancellable client requests.

  • Step 2: Add failing quota parser/proxy tests

Strict parser accepts only the approved object/model/token/unlimited/exhausted/reset/as-of relationships. Proxy authenticates, validates model slug, relays user Bearer to /v1/aegida/quota, disables caching, maps upstream 401 to 401, and maps every other malformed/dependency response to generic 503 without body leakage.

  • Step 3: Add failing hook/UI lifecycle tests

Assert quota refresh on initial effective model, successful model switch, and completion/error of a chat turn. Abort stale model requests so a slower old model cannot overwrite current state. available_tokens formats with locale separators, unlimited displays Без лимита, nullable reset is omitted, dependency failure displays Баланс недоступен but leaves send enabled, and explicit exhausted disables send with an explanatory label.

  • Step 4: Run tests and confirm RED

Run: npm test -- lib/chat/quota.test.ts app/api/me/quota/route.test.ts hooks/use-chat.test.tsx components/chat/chat-app.test.tsx

Expected: quota route/state/UI absent.

  • Step 5: Implement quota proxy, hook state, and UI

Keep quota as {status:"loading"|"ready"|"unavailable", snapshot?:...}. Use an incrementing request generation plus AbortController. Do not optimistically decrement client-side; refresh the authoritative snapshot after each turn. Pass quota to header/composer and disable only for snapshot.exhausted === true.

  • Step 6: Verify and commit Task 5

Run: npm test -- lib/chat/quota.test.ts app/api/me/quota/route.test.ts hooks/use-chat.test.tsx components/chat

git add lib/chat/quota.ts lib/chat/quota.test.ts app/api/me/quota hooks/use-chat.ts components/chat/chat-header.tsx components/chat/chat-app.tsx components/chat/chat-composer.tsx
git commit -m "feat: show effective token availability"

Task 6: Update deployment/docs and run full UI verification

Files:

  • Modify: .env.example
  • Modify: README.md
  • Modify: k8s/config.yaml
  • Modify: k8s/app.yaml
  • Modify: docker-compose.yml
  • Modify or remove only after ownership approval: direct S3/MinIO runtime wiring no longer needed by new uploads

Interfaces:

  • Produces documented AEGIDA_GATE_URL, JWT issuer/audience/tenant settings

  • Preserves local PostgreSQL conversation deployment

  • Step 1: Confirm dirty documentation/compose ownership

.env.example, README.md, and untracked docker-compose.yml were dirty at plan creation. Stop if still user-owned and explicit authorization to merge those changes has not been obtained.

  • Step 2: Add/update configuration contract tests

Tests or build-time checks prove server-only AEGIDA_GATE_URL is required in non-mock production, issuer/audience/tenant are configured consistently with Gate, no NEXT_PUBLIC_ gate secret/base URL is emitted, and legacy AI_GATEWAY_API_KEY is unused.

  • Step 3: Document and configure the migration

Document dynamic models, auto, provider-compatible chat/SSE translation, the two-step Files flow, effective quota semantics, JWT claim coordination, and legacy attachment-row compatibility. K8s/Compose use the Gate service DNS/base URL and shared JWT settings; direct MinIO remains only if needed for legacy attachment downloads during retention.

  • Step 4: Run full verification

Run:

npm test
npm run lint
npm run build
./scripts/smoke-db.sh
git diff --check

Expected: all commands exit 0. The DB smoke may skip only through its existing documented dependency guard.

  • Step 5: Commit Task 6

Stage only task-authored hunks after the dirty-file ownership check.

git add .env.example README.md k8s docker-compose.yml
git commit -m "docs: configure corp ui for aegida gate"