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.
main branch; do not create a worktree.node_modules/next/dist/docs/ as required by AGENTS.md; cite the files read in each task report.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..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./api/...; only server routes know AEGIDA_GATE_URL.JWT_ISSUER, JWT_AUDIENCE, AEGIDA_TENANT_ID, tenant_id, and canonical decimal external_user_id; HS256 remains the initial algorithm.auto; arbitrary model IDs must match ^[a-z0-9][a-z0-9._-]{0,127}$ rather than a hard-coded union.POST /v1/chat/completions with stream:true and parses only OpenAI SSE data: JSON plus [DONE].POST /v1/files with purpose=user_data, then native file_id content parts in Chat. No presigned S3 URL is sent to inference.exhausted=true; a quota dependency error remains nonfatal UI state.X-Conversation-Id, X-User-Message-Id, and X-Assistant-Message-Id browser contract.corp-ui or any pre-existing user changes not authored by the current task.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.Files:
lib/auth/types.tslib/auth/types.test.tslib/auth/server.tslib/auth/server.test.tsapp/api/auth/login/route.tsapp/api/auth/routes.test.tsInterfaces:
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.
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.
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.
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.
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"
Files:
db/migrations/004_aegida_gate_models.sqllib/chat/types.tslib/chat/models.tslib/chat/validation.tslib/chat/validation.test.tslib/chat/catalog.tslib/chat/catalog.test.tsapp/api/models/route.tsapp/api/models/route.test.tshooks/use-models.tshooks/use-models.test.tsxcomponents/chat/model-selector.tsxcomponents/chat/chat-header.tsxcomponents/chat/chat-app.tsxapp/api/me/model/route.tslib/auth/types.tsInterfaces:
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.
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.
Run: npm test -- lib/chat/validation.test.ts lib/db/migrate.test.ts
Expected: static union/set rejects dynamic IDs and migration is absent.
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.
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.
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.
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.
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"
Files:
lib/chat/gateway.tslib/chat/gateway.test.tslib/chat/types.tsapp/api/chat/route.tsapp/api/chat/route.test.tsInterfaces:
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.
Read the complete local Next 16 streaming/Route Handler references and record paths.
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.
Run: npm test -- lib/chat/gateway.test.ts app/api/chat/route.test.ts
Expected: private payload/plain stream behavior fails expectations.
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.
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"
Files:
db/migrations/005_gate_file_ids.sqllib/chat/gate-files.tslib/chat/gate-files.test.tslib/db/attachments.tslib/db/attachments.test.tslib/db/conversations.tsapp/api/attachments/route.tsapp/api/attachments/[id]/route.tsapp/api/attachments/[id]/route.test.tsapp/api/chat/route.tsapp/api/chat/route.test.tslib/chat/types.tsInterfaces:
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.
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.
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.
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.
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.
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"
Files:
lib/chat/quota.tslib/chat/quota.test.tsapp/api/me/quota/route.tsapp/api/me/quota/route.test.tshooks/use-chat.tshooks/use-chat.test.tsxcomponents/chat/chat-header.tsxcomponents/chat/chat-app.tsxcomponents/chat/chat-composer.tsxcomponents/chat/chat-app.test.tsxInterfaces:
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.
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.
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.
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.
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.
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"
Files:
.env.exampleREADME.mdk8s/config.yamlk8s/app.yamldocker-compose.ymlInterfaces:
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.
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.
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.
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.
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"