fix(03): revise plans based on checker feedback

This commit is contained in:
2026-03-23 21:10:23 -06:00
parent 1ff61d9ba4
commit ac606cf9ff
3 changed files with 83 additions and 17 deletions

View File

@@ -12,12 +12,14 @@ files_modified:
- packages/shared/shared/models/tenant.py - packages/shared/shared/models/tenant.py
- packages/shared/shared/api/billing.py - packages/shared/shared/api/billing.py
- packages/shared/shared/api/channels.py - packages/shared/shared/api/channels.py
- packages/shared/shared/api/llm_keys.py
- packages/shared/shared/api/usage.py - packages/shared/shared/api/usage.py
- packages/shared/shared/crypto.py - packages/shared/shared/crypto.py
- packages/orchestrator/orchestrator/agents/runner.py - packages/orchestrator/orchestrator/agents/runner.py
- packages/orchestrator/orchestrator/audit/logger.py - packages/orchestrator/orchestrator/audit/logger.py
- migrations/versions/005_billing_and_usage.py - migrations/versions/005_billing_and_usage.py
- tests/unit/test_key_encryption.py - tests/unit/test_key_encryption.py
- tests/unit/test_llm_keys_crud.py
- tests/unit/test_slack_oauth.py - tests/unit/test_slack_oauth.py
- tests/unit/test_stripe_webhooks.py - tests/unit/test_stripe_webhooks.py
- tests/unit/test_usage_aggregation.py - tests/unit/test_usage_aggregation.py
@@ -34,6 +36,7 @@ must_haves:
truths: truths:
- "Audit events for LLM calls include prompt_tokens, completion_tokens, cost_usd, and provider in metadata" - "Audit events for LLM calls include prompt_tokens, completion_tokens, cost_usd, and provider in metadata"
- "BYO API keys can be encrypted and decrypted without data loss using Fernet" - "BYO API keys can be encrypted and decrypted without data loss using Fernet"
- "BYO API keys can be listed (redacted), created, and deleted via REST endpoints"
- "Slack OAuth state can be HMAC-signed and verified for CSRF protection" - "Slack OAuth state can be HMAC-signed and verified for CSRF protection"
- "Stripe webhook events can be processed idempotently" - "Stripe webhook events can be processed idempotently"
- "Token usage can be aggregated per agent and per provider from audit events" - "Token usage can be aggregated per agent and per provider from audit events"
@@ -45,6 +48,8 @@ must_haves:
provides: "KeyEncryptionService with Fernet encrypt/decrypt/rotate" provides: "KeyEncryptionService with Fernet encrypt/decrypt/rotate"
- path: "packages/shared/shared/api/channels.py" - path: "packages/shared/shared/api/channels.py"
provides: "Slack OAuth state generation/verification, OAuth callback endpoint" provides: "Slack OAuth state generation/verification, OAuth callback endpoint"
- path: "packages/shared/shared/api/llm_keys.py"
provides: "LLM key CRUD endpoints: GET (list, redacted), POST (encrypt + store), DELETE"
- path: "packages/shared/shared/api/billing.py" - path: "packages/shared/shared/api/billing.py"
provides: "Stripe webhook handler, checkout session creation, billing portal session" provides: "Stripe webhook handler, checkout session creation, billing portal session"
- path: "packages/shared/shared/api/usage.py" - path: "packages/shared/shared/api/usage.py"
@@ -64,13 +69,21 @@ must_haves:
to: "PLATFORM_ENCRYPTION_KEY env var" to: "PLATFORM_ENCRYPTION_KEY env var"
via: "Fernet key loaded at init" via: "Fernet key loaded at init"
pattern: "MultiFernet" pattern: "MultiFernet"
- from: "packages/shared/shared/api/llm_keys.py"
to: "packages/shared/shared/crypto.py"
via: "KeyEncryptionService.encrypt() on POST, never decrypts on GET"
pattern: "KeyEncryptionService"
- from: "packages/portal/app/(dashboard)/settings/api-keys/page.tsx"
to: "packages/shared/shared/api/llm_keys.py"
via: "GET/POST/DELETE /api/portal/tenants/{tenant_id}/llm-keys"
pattern: "tenants.*llm-keys"
--- ---
<objective> <objective>
Backend foundation for Phase 3: database migrations, dependency installs, audit trail token metadata, encryption service, and all backend API endpoints for billing, channel connection, and usage aggregation. Backend foundation for Phase 3: database migrations, dependency installs, audit trail token metadata, encryption service, and all backend API endpoints for billing, channel connection, LLM key management, and usage aggregation.
Purpose: Every portal UI feature in Phase 3 depends on backend APIs and database schema. This plan ships all backend infrastructure so Plans 02-04 can focus on frontend. Purpose: Every portal UI feature in Phase 3 depends on backend APIs and database schema. This plan ships all backend infrastructure so Plans 02-04 can focus on frontend.
Output: New DB tables/fields, billing + channel + usage API endpoints, encryption service, enhanced audit logger, and comprehensive test scaffolds. Output: New DB tables/fields, billing + channel + LLM key + usage API endpoints, encryption service, enhanced audit logger, and comprehensive test scaffolds.
</objective> </objective>
<execution_context> <execution_context>
@@ -217,7 +230,7 @@ portal_router = APIRouter(prefix="/api/portal")
- CREATE TABLE tenant_llm_keys with RLS enabled (same FORCE ROW LEVEL SECURITY pattern as agents) - CREATE TABLE tenant_llm_keys with RLS enabled (same FORCE ROW LEVEL SECURITY pattern as agents)
- CREATE TABLE stripe_events (event_id TEXT PK, processed_at TIMESTAMPTZ DEFAULT now()) - CREATE TABLE stripe_events (event_id TEXT PK, processed_at TIMESTAMPTZ DEFAULT now())
- CREATE INDEX idx_audit_events_tenant_type_created ON audit_events (tenant_id, action_type, created_at DESC) - CREATE INDEX idx_audit_events_tenant_type_created ON audit_events (tenant_id, action_type, created_at DESC)
- GRANT SELECT, INSERT on tenant_llm_keys to konstruct_app - GRANT SELECT, INSERT, DELETE on tenant_llm_keys to konstruct_app (DELETE needed for key removal)
- GRANT SELECT, INSERT on stripe_events to konstruct_app - GRANT SELECT, INSERT on stripe_events to konstruct_app
9. Write test scaffolds: 9. Write test scaffolds:
@@ -233,7 +246,7 @@ portal_router = APIRouter(prefix="/api/portal")
- TenantLlmKey and StripeEvent models exist in billing.py - TenantLlmKey and StripeEvent models exist in billing.py
- KeyEncryptionService passes encrypt/decrypt/rotate tests - KeyEncryptionService passes encrypt/decrypt/rotate tests
- Budget alert threshold logic passes at all levels - Budget alert threshold logic passes at all levels
- Alembic migration 005 exists with all schema changes - Alembic migration 005 exists with all schema changes (including DELETE grant on tenant_llm_keys)
- Config has all new settings fields - Config has all new settings fields
</done> </done>
</task> </task>
@@ -287,7 +300,7 @@ portal_router = APIRouter(prefix="/api/portal")
- `GET /api/portal/usage/{tenant_id}/budget-alerts` — for each agent with budget_limit_usd, compare current month cost_usd sum against limit. Return status: "ok" (<80%), "warning" (80-99%), "exceeded" (>=100%). - `GET /api/portal/usage/{tenant_id}/budget-alerts` — for each agent with budget_limit_usd, compare current month cost_usd sum against limit. Return status: "ok" (<80%), "warning" (80-99%), "exceeded" (>=100%).
- Include the composite index from migration 005 for performance. - Include the composite index from migration 005 for performance.
4. Register new routers in the appropriate main.py files. Add channels_router, billing_router, and usage_router to the FastAPI app. The stripe webhook route should be on a separate prefix (/api/webhooks/stripe) without auth. 4. Register new routers in the appropriate main.py files. Add channels_router, billing_router, llm_keys_router, and usage_router to the FastAPI app. The stripe webhook route should be on a separate prefix (/api/webhooks/stripe) without auth.
5. Enhance audit logger — in `packages/orchestrator/orchestrator/agents/runner.py`, extend the metadata dict passed to `log_llm_call()` to include: 5. Enhance audit logger — in `packages/orchestrator/orchestrator/agents/runner.py`, extend the metadata dict passed to `log_llm_call()` to include:
- prompt_tokens: extracted from LiteLLM response usage object - prompt_tokens: extracted from LiteLLM response usage object
@@ -319,23 +332,68 @@ portal_router = APIRouter(prefix="/api/portal")
</done> </done>
</task> </task>
<task type="auto" tdd="true">
<name>Task 3: LLM key CRUD API endpoints</name>
<files>
packages/shared/shared/api/llm_keys.py,
tests/unit/test_llm_keys_crud.py
</files>
<behavior>
- test_create_llm_key: POST with {provider, label, api_key} encrypts key and returns {id, provider, label, created_at} (no key in response)
- test_list_llm_keys_redacted: GET returns list with provider, label, created_at, key_hint (last 4 chars) — never the full key
- test_delete_llm_key: DELETE removes key, subsequent GET no longer includes it
- test_create_duplicate_provider: POST with same tenant_id+provider returns 409 Conflict (UNIQUE constraint)
- test_delete_nonexistent_key: DELETE with unknown key_id returns 404
</behavior>
<action>
1. Create `packages/shared/shared/api/llm_keys.py`:
- `llm_keys_router = APIRouter(prefix="/api/portal/tenants/{tenant_id}/llm-keys")`
- `GET /api/portal/tenants/{tenant_id}/llm-keys` — list all BYO keys for tenant. Return list of {id, provider, label, key_hint, created_at}. key_hint = last 4 characters of the original key (stored alongside encrypted_key in a separate column, or computed at creation and stored in label metadata). NEVER decrypt the key for listing.
- `POST /api/portal/tenants/{tenant_id}/llm-keys` — accepts {provider: str, label: str, api_key: str}. Encrypt api_key using KeyEncryptionService.encrypt(). Store key_hint (last 4 chars of api_key) for display. Insert into tenant_llm_keys table. Return {id, provider, label, key_hint, created_at} with 201 status. Handle UNIQUE(tenant_id, provider) conflict -> return 409.
- `DELETE /api/portal/tenants/{tenant_id}/llm-keys/{key_id}` — delete the key row. Verify tenant_id matches to prevent cross-tenant deletion. Return 204 on success, 404 if not found.
- Use the same dependency injection pattern as existing portal endpoints (get_db session, tenant authorization).
2. Update migration 005 if needed: add `key_hint` column (VARCHAR(4), nullable=True) to tenant_llm_keys table for storing the last 4 chars safely without decryption on list.
3. Write tests in `tests/unit/test_llm_keys_crud.py`:
- test_create_llm_key: verify encrypted_key is stored (not plaintext), response has no api_key field
- test_list_llm_keys_redacted: verify response never contains encrypted_key or plaintext key, only key_hint
- test_delete_llm_key: verify removal and 204 status
- test_create_duplicate_provider: verify 409 on UNIQUE violation
- test_delete_nonexistent_key: verify 404
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct && pytest tests/unit/test_llm_keys_crud.py -x -v</automated>
</verify>
<done>
- GET /api/portal/tenants/{tenant_id}/llm-keys returns redacted key list (provider, label, key_hint, created_at)
- POST creates encrypted key and returns 201 with no secret in response
- DELETE removes key and returns 204
- Duplicate provider per tenant returns 409
- Cross-tenant deletion prevented by tenant_id check
- All 5 tests pass
</done>
</task>
</tasks> </tasks>
<verification> <verification>
All Wave 0 test scaffolds created and passing: All test scaffolds created and passing:
- `pytest tests/unit/test_key_encryption.py -x` — Fernet encrypt/decrypt/rotate - `pytest tests/unit/test_key_encryption.py -x` — Fernet encrypt/decrypt/rotate
- `pytest tests/unit/test_budget_alerts.py -x` — threshold logic - `pytest tests/unit/test_budget_alerts.py -x` — threshold logic
- `pytest tests/unit/test_slack_oauth.py -x` — OAuth state HMAC - `pytest tests/unit/test_slack_oauth.py -x` — OAuth state HMAC
- `pytest tests/unit/test_stripe_webhooks.py -x` — idempotency, status updates, cancellation - `pytest tests/unit/test_stripe_webhooks.py -x` — idempotency, status updates, cancellation
- `pytest tests/unit/test_usage_aggregation.py -x` — SQL aggregates - `pytest tests/unit/test_usage_aggregation.py -x` — SQL aggregates
- `pytest tests/unit/test_llm_keys_crud.py -x` — LLM key CRUD operations
- `pytest tests/unit -x -q` — full unit suite still green - `pytest tests/unit -x -q` — full unit suite still green
</verification> </verification>
<success_criteria> <success_criteria>
- All 5 test files pass with 0 failures - All 6 test files pass with 0 failures
- Alembic migration 005 exists and is syntactically valid - Alembic migration 005 exists and is syntactically valid
- New API routers registered and importable - New API routers registered and importable (including llm_keys_router)
- KeyEncryptionService encrypt/decrypt roundtrip works - KeyEncryptionService encrypt/decrypt roundtrip works
- LLM key CRUD endpoints return redacted data (never expose plaintext keys)
- Audit logger metadata includes prompt_tokens, completion_tokens, cost_usd, provider - Audit logger metadata includes prompt_tokens, completion_tokens, cost_usd, provider
- Existing test suite remains green - Existing test suite remains green
</success_criteria> </success_criteria>

View File

@@ -47,6 +47,10 @@ must_haves:
to: "/api/portal/channels/{tenant_id}/test" to: "/api/portal/channels/{tenant_id}/test"
via: "POST to send test message" via: "POST to send test message"
pattern: "channels.*test" pattern: "channels.*test"
- from: "packages/portal/app/(dashboard)/settings/api-keys/page.tsx"
to: "/api/portal/tenants/{tenant_id}/llm-keys"
via: "GET/POST/DELETE for BYO key CRUD (endpoints created in Plan 01 Task 3)"
pattern: "tenants.*llm-keys"
--- ---
<objective> <objective>
@@ -80,6 +84,13 @@ From packages/shared/shared/api/channels.py (created in Plan 01):
# POST /api/portal/channels/{tenant_id}/test -> { "success": true, "message": "Test message sent" } (body: { channel_type }) # POST /api/portal/channels/{tenant_id}/test -> { "success": true, "message": "Test message sent" } (body: { channel_type })
``` ```
From packages/shared/shared/api/llm_keys.py (created in Plan 01 Task 3):
```python
# GET /api/portal/tenants/{tenant_id}/llm-keys -> [{ id, provider, label, key_hint, created_at }]
# POST /api/portal/tenants/{tenant_id}/llm-keys -> { id, provider, label, key_hint, created_at } (body: { provider, label, api_key })
# DELETE /api/portal/tenants/{tenant_id}/llm-keys/{key_id} -> 204
```
From packages/shared/shared/api/billing.py (created in Plan 01): From packages/shared/shared/api/billing.py (created in Plan 01):
```python ```python
# POST /api/portal/billing/checkout -> { "checkout_url": "https://checkout.stripe.com/..." } # POST /api/portal/billing/checkout -> { "checkout_url": "https://checkout.stripe.com/..." }
@@ -201,12 +212,12 @@ Established patterns:
<action> <action>
1. Create `packages/portal/app/(dashboard)/settings/api-keys/page.tsx`: 1. Create `packages/portal/app/(dashboard)/settings/api-keys/page.tsx`:
- Tenant-level settings page (per user decision — simpler than per-agent for v1) - Tenant-level settings page (per user decision — simpler than per-agent for v1)
- List existing BYO keys: show provider name, label, created date (NOT the key itself — never display decrypted keys) - List existing BYO keys: show provider name, label, key_hint (last 4 chars), created date (NOT the key itself — never display decrypted keys)
- "Add API Key" button opens a form: - "Add API Key" button opens a form:
- Provider: select dropdown (OpenAI, Anthropic, Custom) - Provider: select dropdown (OpenAI, Anthropic, Custom)
- Label: text input (human-readable name, e.g., "Production OpenAI key") - Label: text input (human-readable name, e.g., "Production OpenAI key")
- API Key: password input (masked by default) - API Key: password input (masked by default)
- Submit: POST to /api/portal/tenants/{tenant_id}/llm-keys (needs endpoint added to Plan 01's channels.py or a new file) - Submit: POST to /api/portal/tenants/{tenant_id}/llm-keys (backend endpoint created in Plan 01 Task 3)
- Delete button per key with confirmation dialog - Delete button per key with confirmation dialog
- Use shadcn/ui Card, Table, Dialog, Button, Input, Select components - Use shadcn/ui Card, Table, Dialog, Button, Input, Select components
- react-hook-form + zod for validation (provider required, label 3-100 chars, key not empty) - react-hook-form + zod for validation (provider required, label 3-100 chars, key not empty)
@@ -217,14 +228,12 @@ Established patterns:
- useDeleteLlmKey() — mutation DELETE /api/portal/tenants/{tenant_id}/llm-keys/{keyId} - useDeleteLlmKey() — mutation DELETE /api/portal/tenants/{tenant_id}/llm-keys/{keyId}
3. Add navigation link to settings/api-keys in the dashboard layout sidebar (if sidebar exists) or in the tenant detail page. 3. Add navigation link to settings/api-keys in the dashboard layout sidebar (if sidebar exists) or in the tenant detail page.
Note: The backend endpoints for LLM key CRUD may need to be added to Plan 01's API if not already included. If the endpoint doesn't exist yet, create it in this task: add routes to packages/shared/shared/api/channels.py or create a separate packages/shared/shared/api/llm_keys.py with GET (list, redacted), POST (encrypt + store), DELETE.
</action> </action>
<verify> <verify>
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated> <automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
</verify> </verify>
<done> <done>
- BYO API key settings page renders with list of existing keys (redacted) - BYO API key settings page renders with list of existing keys (redacted — shows key_hint only)
- Add key form validates and submits to backend - Add key form validates and submits to backend
- Delete key with confirmation dialog works - Delete key with confirmation dialog works
- Portal builds without errors - Portal builds without errors
@@ -264,7 +273,7 @@ Established patterns:
- Operator can paste WhatsApp credentials via guided form - Operator can paste WhatsApp credentials via guided form
- Onboarding wizard completes in 3 steps: connect -> configure -> test - Onboarding wizard completes in 3 steps: connect -> configure -> test
- Agent goes live automatically after successful test message - Agent goes live automatically after successful test message
- Operator can manage BYO API keys from settings page - Operator can manage BYO API keys from settings page (backed by Plan 01 Task 3 endpoints)
- Portal builds without errors - Portal builds without errors
</success_criteria> </success_criteria>

View File

@@ -2,8 +2,8 @@
phase: 03-operator-experience phase: 03-operator-experience
plan: 04 plan: 04
type: execute type: execute
wave: 3 wave: 2
depends_on: ["03-01", "03-03"] depends_on: ["03-01"]
files_modified: files_modified:
- packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx - packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx
- packages/portal/components/usage-chart.tsx - packages/portal/components/usage-chart.tsx
@@ -69,7 +69,6 @@ Output: Usage dashboard page with Recharts visualizations, budget alert badges,
@.planning/phases/03-operator-experience/03-CONTEXT.md @.planning/phases/03-operator-experience/03-CONTEXT.md
@.planning/phases/03-operator-experience/03-RESEARCH.md @.planning/phases/03-operator-experience/03-RESEARCH.md
@.planning/phases/03-operator-experience/03-01-SUMMARY.md @.planning/phases/03-operator-experience/03-01-SUMMARY.md
@.planning/phases/03-operator-experience/03-03-SUMMARY.md
<interfaces> <interfaces>
<!-- From Plan 01 backend APIs --> <!-- From Plan 01 backend APIs -->