Project owner spravuje citlivé konfigurační hodnoty (API klíče, tokeny, hesla) vázané na konkrétní prostředí projektu. Hodnoty jsou injektovány jako env vars do user-app podu přes K8s Secret a maskovaně zpřístupněny všem třem agentům (Mara, Theo, Kai) v jejich claudeMdContent kontextu.
- Secrets jsou vázány na
(projectId, environmentId). Pro projekty bez explicit env (environment_id = NULL) se jako cílové prostředí použije DEFAULT env tenanta (slug=talkide). - Source of truth pro hodnoty je K8s Secret — PostgreSQL drží pouze metadata (id, name, masked_preview, timestamps). Holá hodnota se NIKDY neukládá do PG, NIKDY neserializuje do conversation history, NIKDY neobjevuje v BE logách.
- K8s Secret naming:
{project-slug}-user-secretsv namespace{tenant-slug}-{env-slug}. Keys = secret names, values = base64(user value). Přidán jako druhýenvFrom.secretRefvK8sAppDeployer.buildDeployment(vedle existujícího$resourceName-db). - Inject-at-deploy: změna secret se projeví při příštím deployi (rolling restart). Běžící pod nepřebírá změny za runtime. UI zobrazuje hint: “Apply changes by re-deploying.”
- Mara widget
request-secret: Mara emituje fenced bloktalkide-promptskind: "request-secret". FE zobrazí password input; po uložení POST na secrets endpoint → Mara dostane ack jako user message. Value prochází pouze přes FE input + HTTPS + BE K8s write — nikdy přes conversation log. - Liquibase: nový soubor s dalším pořadovým číslem (backend developer přiřadí při implementaci; projekt je v PRODUCTION fázi — NIKDY neměnit existující changesets).
- Related: ADR-026, UC-10010 F1, UC-10013 F3, UC-10014 F4
Security Invariants (CRITICAL — porušení = bezpečnostní incident)
| Invariant | Zdůvodnění |
|---|---|
| Value NIKDY v PostgreSQL | DB je wider-scope; K8s Secret RBAC je granulárněji řízeno |
| Value NIKDY v conversation logu | Conversation history je persistována v DB a přenášena do Claude API |
| Value NIKDY v BE logách | Logback pattern nesmí logovat request body secrets endpointů |
| Value NIKDY v Pinia store | FE store je přenášen do SSE stream a debug toolů |
| Value NIKDY v worker claudeMdContent | Worker dostává jen masked preview (viz §Agent context) |
| Worker→BE secrets API NEEXISTUJE | ADR-024 §C4: worker nemá user JWT, nemůže volat privilegované endpointy |
| HTTPS only | TLS enforced na ingress; plaintext cesta k secret hodnotám nepřípustná |
| Project owner only (MVP) | Extension na team RBAC je Out of Scope (viz §Out of scope) |
| Anti-enumeration na non-owner / cross-tenant | Vrací 404 NOT_FOUND_PROJECT (ne 403) — attacker nesmí zjistit existenci cizích projektů |
Entity Model
Tabulka environment_secrets
| Sloupec | Typ | Constraints | Poznámka |
|---|---|---|---|
id | bigint | PK, NOT NULL | generovaný sekvenčně |
project_id | bigint | NOT NULL, FK → projects(id) ON DELETE CASCADE | vazba na projekt |
environment_id | bigint | NOT NULL, FK → environment(id) | NIKDY NULL — resolver vždy doplní DEFAULT env |
name | varchar(64) | NOT NULL | klíč env var; regex ^[A-Z][A-Z0-9_]{0,63}$ |
masked_preview | varchar(32) | NOT NULL | cached při zápisu (první 4 + *** + poslední 4 chars value); např. sk_l***1234 |
created_at | timestamptz | NOT NULL | |
updated_at | timestamptz | NOT NULL | aktualizuje se při PUT |
created_by_user_id | bigint | NOT NULL, FK → users(id) | audit |
Unique index: (project_id, environment_id, name) — jméno secret je unikátní per project+env.
CHECK constraint (inline <sql> element v Liquibase, NE <addCheckConstraint> — viz UC-09001 incident):
name ~ '^[A-Z][A-Z0-9_]{0,63}
**Liquibase changeset**: nový soubor s dalším pořadovým číslem po posledním existujícím changesétu. Backend developer přiřadí číslo při implementaci (PRODUCTION fáze — immutable constraint).
```xml
<!-- Příklad struktury — číslo N přiřadí BE dev -->
<changeSet id="00N-create-environment-secrets-table" author="system">
<createTable tableName="environment_secrets">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="project_id" type="bigint">
<constraints nullable="false"/>
</column>
<column name="environment_id" type="bigint">
<constraints nullable="false"/>
</column>
<column name="name" type="varchar(64)">
<constraints nullable="false"/>
</column>
<column name="masked_preview" type="varchar(32)">
<constraints nullable="false"/>
</column>
<column name="created_at" type="timestamptz">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="timestamptz">
<constraints nullable="false"/>
</column>
<column name="created_by_user_id" type="bigint">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint
baseTableName="environment_secrets" baseColumnNames="project_id"
constraintName="fk_env_secrets_project"
referencedTableName="projects" referencedColumnNames="id"
onDelete="CASCADE"/>
<addForeignKeyConstraint
baseTableName="environment_secrets" baseColumnNames="environment_id"
constraintName="fk_env_secrets_environment"
referencedTableName="environment" referencedColumnNames="id"/>
<addForeignKeyConstraint
baseTableName="environment_secrets" baseColumnNames="created_by_user_id"
constraintName="fk_env_secrets_user"
referencedTableName="users" referencedColumnNames="id"/>
<createIndex tableName="environment_secrets" indexName="idx_env_secrets_project_env_name" unique="true">
<column name="project_id"/>
<column name="environment_id"/>
<column name="name"/>
</createIndex>
<sql>
ALTER TABLE environment_secrets
ADD CONSTRAINT chk_env_secrets_name_pattern
CHECK (name ~ '^[A-Z][A-Z0-9_]{0,63}
---
## API Endpoints
### GET /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets
Vrátí seznam secrets pro dané prostředí projektu. Pouze metadata — hodnoty NIKDY v response.
```mermaid
sequenceDiagram
actor User
User->>+FE: otevře záložku Secrets v Project Settings
FE->>+BE: GET /api/v1/projects/{projectId}/environments/{envId}/secrets
Note over FE,BE: Authorization: Bearer {userJWT}
BE->>BE: ověř tenant scope a owner (anti-enumeration: non-owner i cross-tenant → 404)
alt projektId neexistuje, cizí tenant nebo user není owner
BE-->>FE: 404 Not Found ErrorResponse
end
BE->>+DB: SELECT id, name, masked_preview, created_at, updated_at, created_by_user_id<br/>FROM environment_secrets<br/>WHERE project_id=? AND environment_id=?<br/>ORDER BY name ASC
DB-->>-BE: list rows
BE-->>-FE: 200 OK SecretListResponse
FE-->>-User: zobrazí tabulku secrets
200 OK SecretListResponse:
{
"secrets": [
{
"name": "OPENAI_KEY",
"maskedPreview": "sk-p***abcd",
"createdAt": "2026-05-20T10:00:00Z",
"updatedAt": "2026-05-20T10:00:00Z",
"createdByUserId": 42
},
{
"name": "STRIPE_API_KEY",
"maskedPreview": "sk_l***1234",
"createdAt": "2026-05-21T14:30:00Z",
"updatedAt": "2026-05-22T09:15:00Z",
"createdByUserId": 42
}
]
}
404 Not Found ErrorResponse (non-owner nebo cross-tenant — anti-enumeration):
{
"code": "NOT_FOUND_PROJECT",
"message": "Project not found"
}
POST /api/v1/projects/{projectId}/environments/{envId}/secrets
Vytvoří nový secret. Value je synchronně zapsána do K8s Secret, do DB se uloží pouze metadata + masked_preview.
sequenceDiagram
actor User
User->>+FE: vyplní name + value, klikne Save
FE->>FE: validace: name regex ^[A-Z][A-Z0-9_]{0,63}$, value 1-4096 chars
alt validace selhala
FE-->>User: inline error message
end
FE->>+BE: POST /api/v1/projects/{projectId}/environments/{envId}/secrets<br/>{ "name": "STRIPE_API_KEY", "value": "sk_live_..." }
Note over FE,BE: Authorization: Bearer {userJWT}
BE->>BE: ověř tenant scope a owner (anti-enumeration: non-owner i cross-tenant → 404)
alt projektId neexistuje, cizí tenant nebo user není owner
BE-->>FE: 404 Not Found ErrorResponse
end
BE->>BE: validuj name (regex), value (1–4096 chars, no null bytes)
BE->>+DB: SELECT 1 FROM environment_secrets WHERE project_id=? AND environment_id=? AND name=?
DB-->>-BE: existující row?
alt secret se stejným názvem již existuje
BE-->>FE: 409 Conflict ErrorResponse
end
BE->>BE: vypočítej masked_preview z value<br/>(prvních 4 chars + "***" + posledních 4 chars)
BE->>+K8s: GET Secret "{project-slug}-user-secrets" in ns "{tenant-slug}-{env-slug}"
alt Secret neexistuje
BE->>K8s: CREATE Secret (type=Opaque, data={name: base64(value)})
else Secret existuje
BE->>K8s: PATCH Secret — přidej/přepiš key {name: base64(value)}
end
K8s-->>-BE: ok
BE->>+DB: INSERT INTO environment_secrets<br/>(project_id, environment_id, name, masked_preview, created_at, updated_at, created_by_user_id)
DB-->>-BE: id
BE-->>-FE: 201 Created SecretResponse
FE-->>-User: nový řádek v tabulce + hint "Apply changes by re-deploying"
POST /api/v1/projects/{projectId}/environments/{envId}/secrets CreateSecretRequest:
{
"name": "STRIPE_API_KEY",
"value": "sk_live_abc123xyz789"
}
201 Created SecretResponse:
{
"name": "STRIPE_API_KEY",
"maskedPreview": "sk_l***x789",
"createdAt": "2026-05-23T12:00:00Z",
"updatedAt": "2026-05-23T12:00:00Z"
}
400 Bad Request (validation) ErrorResponse:
{
"code": "VALIDATION",
"message": "name must match pattern ^[A-Z][A-Z0-9_]{0,63}$"
}
404 Not Found ErrorResponse (non-owner nebo cross-tenant — anti-enumeration):
{
"code": "NOT_FOUND_PROJECT",
"message": "Project not found"
}
409 Conflict ErrorResponse:
{
"code": "CONFLICT_ENVIRONMENT_SECRET",
"message": "Secret with name STRIPE_API_KEY already exists in this environment"
}
PUT /api/v1/projects/{projectId}/environments/{envId}/secrets/{name}
Aktualizuje hodnotu existujícího secret. Name je immutabilní (je součástí URL, ne request body). K8s Secret se patchuje idempotentně.
sequenceDiagram
actor User
User->>+FE: edituje value existujícího secretu, klikne Save
FE->>FE: validace: value 1-4096 chars
alt validace selhala
FE-->>User: inline error message
end
FE->>+BE: PUT /api/v1/projects/{projectId}/environments/{envId}/secrets/{name}<br/>{ "value": "sk_live_newvalue..." }
BE->>BE: ověř tenant scope a owner (anti-enumeration: non-owner i cross-tenant → 404)
BE->>BE: validuj value (1–4096 chars, no null bytes)
BE->>+DB: SELECT id FROM environment_secrets WHERE project_id=? AND environment_id=? AND name=?
DB-->>-BE: row
alt secret nenalezen
BE-->>FE: 404 Not Found ErrorResponse
end
BE->>BE: vypočítej nový masked_preview
BE->>+K8s: PATCH Secret "{project-slug}-user-secrets" — přepiš key {name: base64(newValue)}
K8s-->>-BE: ok
BE->>+DB: UPDATE environment_secrets SET masked_preview=?, updated_at=now() WHERE id=?
DB-->>-BE: ok
BE-->>-FE: 200 OK SecretResponse
FE-->>-User: aktualizovaný řádek + hint "Apply changes by re-deploying"
PUT /api/v1/projects/{projectId}/environments/{envId}/secrets/{name} UpdateSecretRequest:
{
"value": "sk_live_newvalue_abc987"
}
200 OK SecretResponse:
{
"name": "STRIPE_API_KEY",
"maskedPreview": "sk_l***b987",
"createdAt": "2026-05-23T12:00:00Z",
"updatedAt": "2026-05-23T15:30:00Z"
}
400 Bad Request (validation) ErrorResponse:
{
"code": "VALIDATION",
"message": "value must not be blank"
}
404 Not Found ErrorResponse (non-owner, cross-tenant nebo secret nenalezen — anti-enumeration):
{
"code": "NOT_FOUND_PROJECT",
"message": "Project not found"
}
DELETE /api/v1/projects/{projectId}/environments/{envId}/secrets/{name}
Odstraní secret — smaže key z K8s Secret a odstraní DB řádek.
sequenceDiagram
actor User
User->>+FE: klikne Delete na řádku, potvrdí dialog
FE->>+BE: DELETE /api/v1/projects/{projectId}/environments/{envId}/secrets/{name}
BE->>BE: ověř tenant scope a owner (anti-enumeration: non-owner i cross-tenant → 404)
BE->>+DB: SELECT id FROM environment_secrets WHERE project_id=? AND environment_id=? AND name=?
DB-->>-BE: row
alt secret nenalezen
BE-->>FE: 404 Not Found ErrorResponse
end
BE->>+K8s: GET Secret "{project-slug}-user-secrets"
alt Secret existuje a obsahuje key {name}
BE->>K8s: PATCH Secret — odstraň key {name}
end
K8s-->>-BE: ok
BE->>+DB: DELETE FROM environment_secrets WHERE id=?
DB-->>-BE: ok
BE-->>-FE: 204 No Content
FE-->>-User: řádek odstraněn z tabulky + hint "Apply changes by re-deploying"
204 No Content — prázdné tělo.
404 Not Found ErrorResponse (non-owner, cross-tenant nebo secret nenalezen — anti-enumeration):
{
"code": "NOT_FOUND_PROJECT",
"message": "Project not found"
}
K8s Integration
Secret naming a namespace
| Prvek | Hodnota | Příklad |
|---|---|---|
| Secret name | {project-slug}-user-secrets | todo-list-user-secrets |
| Namespace | {tenant-slug}-{env-slug} | popelkam-talkide (DEFAULT), popelkam-production (user env) |
| Secret type | Opaque | — |
| Keys | secret names (env var names) | STRIPE_API_KEY, OPENAI_KEY |
| Values | base64(user value) | K8s kódování automaticky při create/patch |
Naming záměrně neobsahuje tenant prefix v Secret name — namespace ho drží (bez redundance).
K8sSecretSyncer
Nová service/komponenta K8sSecretSyncer (vzor: PostgresDatabaseProvisioner — Fabric8 CRUD přes client.secrets().inNamespace(ns).resource(secret).create()):
| Operace | Behavior |
|---|---|
| Create (Secret neexistuje) | client.secrets().inNamespace(ns).resource(newSecret).create() |
| Add/Update key (Secret existuje) | client.secrets().inNamespace(ns).withName(secretName).patch(patchedSecret) |
| Remove key | PATCH Secret — odstraní key; pokud Secret poté nemá žádný key, Secret zůstane (prázdný je OK) |
| Secret neexistuje při DELETE | gracefully skip (idempotent) |
K8sAppDeployer — wiring
K8sAppDeployer.buildDeployment dostane dva envFrom.secretRef zdroje:
// Stávající (neměnit):
.addNewEnvFrom().withNewSecretRef().withName("$resourceName-db").endSecretRef().endEnvFrom()
// Nové (přidat):
.addNewEnvFrom().withNewSecretRef()
.withName("$projectSlug-user-secrets")
.withOptional(true) // gracefully skip pokud Secret neexistuje
.endSecretRef().endEnvFrom()
optional: true zajišťuje, že pod nastartuje i bez user secrets (projekt bez secrets = normální stav).
Pod restart policy
Změna user secret nevyvolává automatický restart podu. Nové hodnoty se projeví až při next deploy (rolling restart). FE zobrazuje persistent hint v Secrets tabu:
“Secret changes are applied on next deployment. Re-deploy your app to activate the changes.”
Mara Widget — request-secret
JSON payload schema
Mara emituje fenced blok v konverzaci:
```talkide-prompt
{
"kind": "request-secret",
"name": "STRIPE_API_KEY",
"description": "Stripe live API key for billing integration",
"envId": 123,
"projectId": 456
}
```
| Pole | Typ | Povinné | Popis |
|---|---|---|---|
kind | "request-secret" | ano | diskriminátor widgetu |
name | string | ano | název secret (musí splňovat regex, Mara volí snake_case uppercase) |
description | string | ne | nápověda pro uživatele co zadat |
envId | number | ano | ID cílového prostředí |
projectId | number | ano | ID projektu |
FE komponent — TalkidePromptCard (nový case)
TalkidePromptCard.vue dostane nový case request-secret vedle stávajících:
| UI prvek | Detail |
|---|---|
| Label | name pole (tučně) |
| Popis | description pole (šedě pod labelem) |
| Env badge | read-only chip s názvem prostředí (lookup přes envId) |
| Input | <input type="password" autocomplete="off"> — value NIKDY v store |
| Save button | POST na /api/v1/projects/{projectId}/environments/{envId}/secrets |
Flow po Save
- FE POST na secrets endpoint s
{ name, value }. - BE vrátí
201 Created. - FE injektuje do konverzace user message (text, NE přes Mara agent): “Secret STRIPE_API_KEY stored in production environment.”
- Value je okamžitě zahozena z input DOM — přechod na masked state.
- Mara ve svém dalším turn vidí ack a pokračuje v práci.
Value NIKDY neprojde Pinia store, SSE stream, ani conversation history v DB.
Agent Context Injection
Všichni tři agenti (Mara, Theo, Kai) dostávají seznam secrets per prostředí do svého claudeMdContent.
Formát sekce
## Available Secrets
### Environment: production
- STRIPE_API_KEY: sk_l***1234 (created 2026-05-20)
- OPENAI_KEY: sk-p***abcd (created 2026-05-18)
### Environment: TalkIDE (default)
- DATABASE_URL: post***5432 (created 2026-05-15)
Pravidla:
- Masked preview se čte přímo z DB
masked_previewsloupce (nikdy z K8s — K8s value není agent-accessible). - Pokud projekt má
environment_id = NULL, použije se DEFAULT env tenanta (slug=talkide). - Sekce se vloží do
claudeMdContentkaždého agenta.
Implementační poznámka — context rendery
MaraContextRenderer.kt (com.mddsummer.talkide.features.conversation.domain.MaraContextRenderer) je identifikovaný. Analogické rendery pro Theo a Kai persona:
TBD path discovery during impl — backend developer při implementaci dohledá renderer třídy pro Theo a Kai (pravděpodobně TheoContextRenderer.kt a KaiContextRenderer.kt ve stejném feature package nebo v features/conversation/domain/). Sekce ## Available Secrets se přidá do všech tří rendererů konzistentně.
Pokud sdílí společný base/builder, stačí přidat sekci jednou do společné logiky.
Frontend — Project Settings (refactor na taby)
Refactor ProjectSettingsPanel.vue
ProjectSettingsPanel.vue se refaktoruje z flat sekcí na tabbed layout:
| Tab | Obsah |
|---|---|
| General | Stávající obecná nastavení projektu |
| Environments | Výběr cílového prostředí projektu (vazba project.environment_id) |
| Secrets | Správa secrets (tato UC) |
| Danger | Archivace, smazání projektu |
Tab Secrets — UI flow
- Uživatel otevře Project Settings → klikne na záložku Secrets.
- Env selector (dropdown) — defaultní hodnota = aktuální prostředí projektu nebo DEFAULT.
- Změna env selectoru načte secrets pro dané prostředí (
GET /api/v1/projects/{id}/environments/{envId}/secrets). - Tabulka: sloupce
Name | Masked preview | Updated | Actions. - Empty state: “No secrets yet. Add one to inject env vars into your app.”
- Add Secret button → modal:
- Name input (text, uppercase suggestion/hint)
- Value input (
type="password",autocomplete="off") - Save → POST → row přibude, modal se zavře
- Edit (row action) → modal:
- Name (read-only chip)
- New value input (
type="password") - Save → PUT → row se aktualizuje
- Delete (row action) → confirm dialog → DELETE → row se odstraní
- Persistent hint pod tabulkou: “Secret changes are applied on next deployment. Re-deploy your app to activate the changes.”
Validace — Frontend
| Pole | Constraints | Velikost | Pattern | Poznámka |
|---|---|---|---|---|
name | not_blank | 1 – 64 | ^[A-Z][A-Z0-9_]{0,63}$ | hint: uppercase + underscore; inline regex error |
value | not_blank | 1 – 4096 | — | type="password", nikdy v store |
Backend
Validace — Backend
| Pole | Constraints | Velikost | Pattern | Poznámka |
|---|---|---|---|---|
name (POST) | not_blank | 1 – 64 | ^[A-Z][A-Z0-9_]{0,63}$ | 400 pokud pattern nesedí |
name (URL path, PUT/DELETE) | not_blank | 1 – 64 | ^[A-Z][A-Z0-9_]{0,63}$ | 400 pokud pattern nesedí |
value (POST/PUT) | not_blank, no null bytes (�) | 1 – 4096 | — | 400 pokud překročí nebo obsahuje null byte |
projectId | existující projekt, tenant scope, user je owner | — | — | 404 NOT_FOUND_PROJECT pokud neexistuje, cizí tenant nebo user není owner (anti-enumeration) |
environmentId | existující prostředí patřící projektu nebo DEFAULT | — | — | 404 pokud neexistuje |
Masked preview výpočet: value.take(4) + "***" + value.takeLast(4). Pokud je value kratší než 8 znaků, zobrazí se **** (celá hodnota maskována).
Audit log: každá write operace (POST/PUT/DELETE) generuje BE INFO log ve formátu:
INFO c.m.t.secrets.SecretAuditLog - [SECRET_WRITE] projectId={} envId={} name={} op={CREATE|UPDATE|DELETE} userId={}
Value se NIKDY neobjevuje v logu.
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
TC-001: Projekt existuje, user je owner, secret STRIPE_API_KEY neexistuje | POST /secrets s validním name a value | 201 Created; DB row s masked_preview; K8s Secret obsahuje klíč STRIPE_API_KEY |
TC-002: Secret STRIPE_API_KEY již existuje v prostředí | POST /secrets se stejným názvem | 409 Conflict, code=CONFLICT_ENVIRONMENT_SECRET; žádná změna v DB ani K8s |
| TC-003: Name nesplňuje regex (obsahuje lowercase nebo začíná číslicí) | POST /secrets s name stripe_key nebo 1KEY | 400 Bad Request, code=VALIDATION |
| TC-004: Value má 4097 znaků | POST /secrets s přetékající value | 400 Bad Request, code=VALIDATION |
| TC-005: Prostředí má 3 secrets | GET /secrets pro daný (projectId, envId) | 200 OK; pole 3 items; masked_preview správně; žádná raw value v response |
| TC-006: Secret existuje | PUT /secrets/{name} s novou validní value | 200 OK; masked_preview aktualizován; K8s Secret patchován; DB updated_at aktualizován |
| TC-007: Secret existuje | DELETE /secrets/{name} | 204 No Content; DB row smazán; K8s Secret key odstraněn |
| TC-008: User není project owner (jiný authenticated user pro daný projekt nebo cross-tenant projekt) | POST /secrets nebo GET /secrets | 404 Not Found, code=NOT_FOUND_PROJECT (anti-enumeration — neenumeruje existenci projektů) |
| TC-009: projectId patří jinému tenantovi (cross-tenant) | GET /secrets s cizím projectId | 404 Not Found, code=NOT_FOUND_PROJECT (stejný response jako TC-008 — anti-enumeration pattern je konzistentní pro non-owner i cross-tenant) |
TC-010: Mara emituje request-secret widget; user vyplní value a klikne Save | FE POSTuje na secrets endpoint | 201 Created; ack message v konverzaci; value není v conversation DB |
| TC-011: Secret vytvořen přes API | kubectl get secret {project-slug}-user-secrets -n {tenant-slug}-{env-slug} -o jsonpath='{.data.STRIPE_API_KEY}' | base64-decoded value odpovídá zadané hodnotě |
TC-012: Value je sk_live_abc1234 (16 znaků) | POST /secrets | maskedPreview = "sk_l***1234" (první 4 + *** + poslední 4) |
| TC-013: Projekt má 2 secrets ve dvou prostředích | Agent context fetch pro daný projekt | claudeMdContent obsahuje sekci ## Available Secrets s oběma prostředími a masked preview; raw values nejsou přítomny |
Out of Scope (MVP)
- Audit log UI — history view kdo a kdy secret měnil (následující UC)
- Per-secret RBAC — pouze project owner v MVP; team role extension v dalším UC
- Secret rotation automation — manuální update přes PUT
- External KMS integration — Vault, AWS KMS, DO Secrets Manager
- Worker direct secret API — worker dostává POUZE masked preview přes claudeMdContent, žádný přímý přístup k hodnotám
- Secret picker widget (
pick-secret) — jenrequest-secretv MVP - Bulk import secrets (ENV file upload)
- Secret scoping na branch/preview deploy (v MVP = per env, ne per deploy)
**Liquibase changeset**: nový soubor s dalším pořadovým číslem po posledním existujícím changesétu. Backend developer přiřadí číslo při implementaci (PRODUCTION fáze — immutable constraint).
�FENCE�1
---
## API Endpoints
### GET /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets
Vrátí seznam secrets pro dané prostředí projektu. Pouze metadata — hodnoty NIKDY v response.
�FENCE�2
`200 OK` **SecretListResponse**:
�FENCE�3
`404 Not Found` **ErrorResponse** *(non-owner nebo cross-tenant — anti-enumeration)*:
�FENCE�4
---
### POST /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets
Vytvoří nový secret. Value je synchronně zapsána do K8s Secret, do DB se uloží pouze metadata + masked_preview.
�FENCE�5
`POST /api/v1/projects/{projectId}/environments/{envId}/secrets` **CreateSecretRequest**:
�FENCE�6
`201 Created` **SecretResponse**:
�FENCE�7
`400 Bad Request` *(validation)* **ErrorResponse**:
�FENCE�8
`404 Not Found` **ErrorResponse** *(non-owner nebo cross-tenant — anti-enumeration)*:
�FENCE�9
`409 Conflict` **ErrorResponse**:
�FENCE�10
---
### PUT /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets/{`{`}name{`}`}
Aktualizuje hodnotu existujícího secret. Name je immutabilní (je součástí URL, ne request body). K8s Secret se patchuje idempotentně.
�FENCE�11
`PUT /api/v1/projects/{projectId}/environments/{envId}/secrets/{name}` **UpdateSecretRequest**:
�FENCE�12
`200 OK` **SecretResponse**:
�FENCE�13
`400 Bad Request` *(validation)* **ErrorResponse**:
�FENCE�14
`404 Not Found` **ErrorResponse** *(non-owner, cross-tenant nebo secret nenalezen — anti-enumeration)*:
�FENCE�15
---
### DELETE /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets/{`{`}name{`}`}
Odstraní secret — smaže key z K8s Secret a odstraní DB řádek.
�FENCE�16
`204 No Content` — prázdné tělo.
`404 Not Found` **ErrorResponse** *(non-owner, cross-tenant nebo secret nenalezen — anti-enumeration)*:
�FENCE�17
---
## K8s Integration
### Secret naming a namespace
| Prvek | Hodnota | Příklad |
|-------|---------|---------|
| Secret name | `{project-slug}-user-secrets` | `todo-list-user-secrets` |
| Namespace | `{tenant-slug}-{env-slug}` | `popelkam-talkide` (DEFAULT), `popelkam-production` (user env) |
| Secret type | `Opaque` | — |
| Keys | secret names (env var names) | `STRIPE_API_KEY`, `OPENAI_KEY` |
| Values | `base64(user value)` | K8s kódování automaticky při create/patch |
Naming záměrně neobsahuje tenant prefix v Secret name — namespace ho drží (bez redundance).
### K8sSecretSyncer
Nová service/komponenta `K8sSecretSyncer` (vzor: `PostgresDatabaseProvisioner` — Fabric8 CRUD přes `client.secrets().inNamespace(ns).resource(secret).create()`):
| Operace | Behavior |
|---------|----------|
| Create (Secret neexistuje) | `client.secrets().inNamespace(ns).resource(newSecret).create()` |
| Add/Update key (Secret existuje) | `client.secrets().inNamespace(ns).withName(secretName).patch(patchedSecret)` |
| Remove key | PATCH Secret — odstraní key; pokud Secret poté nemá žádný key, Secret zůstane (prázdný je OK) |
| Secret neexistuje při DELETE | gracefully skip (idempotent) |
### K8sAppDeployer — wiring
`K8sAppDeployer.buildDeployment` dostane dva `envFrom.secretRef` zdroje:
�FENCE�18
`optional: true` zajišťuje, že pod nastartuje i bez user secrets (projekt bez secrets = normální stav).
### Pod restart policy
Změna user secret **nevyvolává automatický restart podu**. Nové hodnoty se projeví až při next deploy (rolling restart). FE zobrazuje persistent hint v Secrets tabu:
> *"Secret changes are applied on next deployment. Re-deploy your app to activate the changes."*
---
## Mara Widget — request-secret
### JSON payload schema
Mara emituje fenced blok v konverzaci:
�FENCE�19
| Pole | Typ | Povinné | Popis |
|------|-----|---------|-------|
| `kind` | `"request-secret"` | ano | diskriminátor widgetu |
| `name` | `string` | ano | název secret (musí splňovat regex, Mara volí snake_case uppercase) |
| `description` | `string` | ne | nápověda pro uživatele co zadat |
| `envId` | `number` | ano | ID cílového prostředí |
| `projectId` | `number` | ano | ID projektu |
### FE komponent — TalkidePromptCard (nový case)
`TalkidePromptCard.vue` dostane nový case `request-secret` vedle stávajících:
| UI prvek | Detail |
|---------|--------|
| Label | `name` pole (tučně) |
| Popis | `description` pole (šedě pod labelem) |
| Env badge | read-only chip s názvem prostředí (lookup přes `envId`) |
| Input | `<input type="password" autocomplete="off">` — value NIKDY v store |
| Save button | POST na `/api/v1/projects/{projectId}/environments/{envId}/secrets` |
### Flow po Save
1. FE POST na secrets endpoint s `{ name, value }`.
2. BE vrátí `201 Created`.
3. FE injektuje do konverzace user message (text, NE přes Mara agent): *"Secret STRIPE_API_KEY stored in production environment."*
4. Value je okamžitě zahozena z input DOM — přechod na masked state.
5. Mara ve svém dalším turn vidí ack a pokračuje v práci.
Value NIKDY neprojde Pinia store, SSE stream, ani conversation history v DB.
---
## Agent Context Injection
Všichni tři agenti (Mara, Theo, Kai) dostávají seznam secrets per prostředí do svého `claudeMdContent`.
### Formát sekce
�FENCE�20
Pravidla:
- Masked preview se čte přímo z DB `masked_preview` sloupce (nikdy z K8s — K8s value není agent-accessible).
- Pokud projekt má `environment_id = NULL`, použije se DEFAULT env tenanta (`slug=talkide`).
- Sekce se vloží do `claudeMdContent` každého agenta.
### Implementační poznámka — context rendery
`MaraContextRenderer.kt` (`com.mddsummer.talkide.features.conversation.domain.MaraContextRenderer`) je identifikovaný. Analogické rendery pro Theo a Kai persona:
**TBD path discovery during impl** — backend developer při implementaci dohledá renderer třídy pro Theo a Kai (pravděpodobně `TheoContextRenderer.kt` a `KaiContextRenderer.kt` ve stejném feature package nebo v `features/conversation/domain/`). Sekce `## Available Secrets` se přidá do všech tří rendererů konzistentně.
Pokud sdílí společný base/builder, stačí přidat sekci jednou do společné logiky.
---
## Frontend — Project Settings (refactor na taby)
### Refactor ProjectSettingsPanel.vue
`ProjectSettingsPanel.vue` se refaktoruje z flat sekcí na tabbed layout:
| Tab | Obsah |
|-----|-------|
| **General** | Stávající obecná nastavení projektu |
| **Environments** | Výběr cílového prostředí projektu (vazba `project.environment_id`) |
| **Secrets** | Správa secrets (tato UC) |
| **Danger** | Archivace, smazání projektu |
### Tab Secrets — UI flow
1. Uživatel otevře Project Settings → klikne na záložku **Secrets**.
2. Env selector (dropdown) — defaultní hodnota = aktuální prostředí projektu nebo DEFAULT.
3. Změna env selectoru načte secrets pro dané prostředí (`GET /api/v1/projects/{id}/environments/{envId}/secrets`).
4. Tabulka: sloupce `Name | Masked preview | Updated | Actions`.
5. **Empty state**: *"No secrets yet. Add one to inject env vars into your app."*
6. **Add Secret** button → modal:
- Name input (text, uppercase suggestion/hint)
- Value input (`type="password"`, `autocomplete="off"`)
- Save → POST → row přibude, modal se zavře
7. **Edit** (row action) → modal:
- Name (read-only chip)
- New value input (`type="password"`)
- Save → PUT → row se aktualizuje
8. **Delete** (row action) → confirm dialog → DELETE → row se odstraní
9. Persistent hint pod tabulkou: *"Secret changes are applied on next deployment. Re-deploy your app to activate the changes."*
### Validace — Frontend
| Pole | Constraints | Velikost | Pattern | Poznámka |
|------|-------------|----------|---------|---------|
| `name` | not_blank | 1 – 64 | `^[A-Z][A-Z0-9_]{0,63}$` | hint: uppercase + underscore; inline regex error |
| `value` | not_blank | 1 – 4096 | — | `type="password"`, nikdy v store |
---
## Backend
### Validace — Backend
| Pole | Constraints | Velikost | Pattern | Poznámka |
|------|-------------|----------|---------|---------|
| `name` (POST) | not_blank | 1 – 64 | `^[A-Z][A-Z0-9_]{0,63}$` | 400 pokud pattern nesedí |
| `name` (URL path, PUT/DELETE) | not_blank | 1 – 64 | `^[A-Z][A-Z0-9_]{0,63}$` | 400 pokud pattern nesedí |
| `value` (POST/PUT) | not_blank, no null bytes (`�`) | 1 – 4096 | — | 400 pokud překročí nebo obsahuje null byte |
| `projectId` | existující projekt, tenant scope, user je owner | — | — | 404 `NOT_FOUND_PROJECT` pokud neexistuje, cizí tenant nebo user není owner (anti-enumeration) |
| `environmentId` | existující prostředí patřící projektu nebo DEFAULT | — | — | 404 pokud neexistuje |
**Masked preview** výpočet: `value.take(4) + "***" + value.takeLast(4)`. Pokud je value kratší než 8 znaků, zobrazí se `****` (celá hodnota maskována).
**Audit log**: každá write operace (POST/PUT/DELETE) generuje BE INFO log ve formátu:
�FENCE�21
Value se NIKDY neobjevuje v logu.
### Test Cases
| GIVEN | WHEN | THEN |
|-------|------|------|
| TC-001: Projekt existuje, user je owner, secret `STRIPE_API_KEY` neexistuje | `POST /secrets` s validním name a value | 201 Created; DB row s masked_preview; K8s Secret obsahuje klíč `STRIPE_API_KEY` |
| TC-002: Secret `STRIPE_API_KEY` již existuje v prostředí | `POST /secrets` se stejným názvem | 409 Conflict, `code=CONFLICT_ENVIRONMENT_SECRET`; žádná změna v DB ani K8s |
| TC-003: Name nesplňuje regex (obsahuje lowercase nebo začíná číslicí) | `POST /secrets` s name `stripe_key` nebo `1KEY` | 400 Bad Request, `code=VALIDATION` |
| TC-004: Value má 4097 znaků | `POST /secrets` s přetékající value | 400 Bad Request, `code=VALIDATION` |
| TC-005: Prostředí má 3 secrets | `GET /secrets` pro daný (projectId, envId) | 200 OK; pole 3 items; masked_preview správně; žádná raw value v response |
| TC-006: Secret existuje | `PUT /secrets/{name}` s novou validní value | 200 OK; masked_preview aktualizován; K8s Secret patchován; DB `updated_at` aktualizován |
| TC-007: Secret existuje | `DELETE /secrets/{name}` | 204 No Content; DB row smazán; K8s Secret key odstraněn |
| TC-008: User není project owner (jiný authenticated user pro daný projekt nebo cross-tenant projekt) | `POST /secrets` nebo `GET /secrets` | 404 Not Found, `code=NOT_FOUND_PROJECT` (anti-enumeration — neenumeruje existenci projektů) |
| TC-009: projectId patří jinému tenantovi (cross-tenant) | `GET /secrets` s cizím projectId | 404 Not Found, `code=NOT_FOUND_PROJECT` (stejný response jako TC-008 — anti-enumeration pattern je konzistentní pro non-owner i cross-tenant) |
| TC-010: Mara emituje `request-secret` widget; user vyplní value a klikne Save | FE POSTuje na secrets endpoint | 201 Created; ack message v konverzaci; value není v conversation DB |
| TC-011: Secret vytvořen přes API | `kubectl get secret {project-slug}-user-secrets -n {tenant-slug}-{env-slug} -o jsonpath='{.data.STRIPE_API_KEY}'` | base64-decoded value odpovídá zadané hodnotě |
| TC-012: Value je `sk_live_abc1234` (16 znaků) | `POST /secrets` | `maskedPreview = "sk_l***1234"` (první 4 + *** + poslední 4) |
| TC-013: Projekt má 2 secrets ve dvou prostředích | Agent context fetch pro daný projekt | `claudeMdContent` obsahuje sekci `## Available Secrets` s oběma prostředími a masked preview; raw values nejsou přítomny |
---
## Out of Scope (MVP)
* Audit log UI — history view kdo a kdy secret měnil (následující UC)
* Per-secret RBAC — pouze project owner v MVP; team role extension v dalším UC
* Secret rotation automation — manuální update přes PUT
* External KMS integration — Vault, AWS KMS, DO Secrets Manager
* Worker direct secret API — worker dostává POUZE masked preview přes claudeMdContent, žádný přímý přístup k hodnotám
* Secret picker widget (`pick-secret`) — jen `request-secret` v MVP
* Bulk import secrets (ENV file upload)
* Secret scoping na branch/preview deploy (v MVP = per env, ne per deploy)
---
);
</sql>
</changeSet>
API Endpoints
GET /api/v1/projects/{projectId}/environments/{envId}/secrets
Vrátí seznam secrets pro dané prostředí projektu. Pouze metadata — hodnoty NIKDY v response.
�FENCE�2
200 OK SecretListResponse:
�FENCE�3
404 Not Found ErrorResponse (non-owner nebo cross-tenant — anti-enumeration):
�FENCE�4
POST /api/v1/projects/{projectId}/environments/{envId}/secrets
Vytvoří nový secret. Value je synchronně zapsána do K8s Secret, do DB se uloží pouze metadata + masked_preview.
�FENCE�5
POST /api/v1/projects/{projectId}/environments/{envId}/secrets CreateSecretRequest:
�FENCE�6
201 Created SecretResponse:
�FENCE�7
400 Bad Request (validation) ErrorResponse:
�FENCE�8
404 Not Found ErrorResponse (non-owner nebo cross-tenant — anti-enumeration):
�FENCE�9
409 Conflict ErrorResponse:
�FENCE�10
PUT /api/v1/projects/{projectId}/environments/{envId}/secrets/{name}
Aktualizuje hodnotu existujícího secret. Name je immutabilní (je součástí URL, ne request body). K8s Secret se patchuje idempotentně.
�FENCE�11
PUT /api/v1/projects/{projectId}/environments/{envId}/secrets/{name} UpdateSecretRequest:
�FENCE�12
200 OK SecretResponse:
�FENCE�13
400 Bad Request (validation) ErrorResponse:
�FENCE�14
404 Not Found ErrorResponse (non-owner, cross-tenant nebo secret nenalezen — anti-enumeration):
�FENCE�15
DELETE /api/v1/projects/{projectId}/environments/{envId}/secrets/{name}
Odstraní secret — smaže key z K8s Secret a odstraní DB řádek.
�FENCE�16
204 No Content — prázdné tělo.
404 Not Found ErrorResponse (non-owner, cross-tenant nebo secret nenalezen — anti-enumeration):
�FENCE�17
K8s Integration
Secret naming a namespace
| Prvek | Hodnota | Příklad |
|---|---|---|
| Secret name | {project-slug}-user-secrets | todo-list-user-secrets |
| Namespace | {tenant-slug}-{env-slug} | popelkam-talkide (DEFAULT), popelkam-production (user env) |
| Secret type | Opaque | — |
| Keys | secret names (env var names) | STRIPE_API_KEY, OPENAI_KEY |
| Values | base64(user value) | K8s kódování automaticky při create/patch |
Naming záměrně neobsahuje tenant prefix v Secret name — namespace ho drží (bez redundance).
K8sSecretSyncer
Nová service/komponenta K8sSecretSyncer (vzor: PostgresDatabaseProvisioner — Fabric8 CRUD přes client.secrets().inNamespace(ns).resource(secret).create()):
| Operace | Behavior |
|---|---|
| Create (Secret neexistuje) | client.secrets().inNamespace(ns).resource(newSecret).create() |
| Add/Update key (Secret existuje) | client.secrets().inNamespace(ns).withName(secretName).patch(patchedSecret) |
| Remove key | PATCH Secret — odstraní key; pokud Secret poté nemá žádný key, Secret zůstane (prázdný je OK) |
| Secret neexistuje při DELETE | gracefully skip (idempotent) |
K8sAppDeployer — wiring
K8sAppDeployer.buildDeployment dostane dva envFrom.secretRef zdroje:
�FENCE�18
optional: true zajišťuje, že pod nastartuje i bez user secrets (projekt bez secrets = normální stav).
Pod restart policy
Změna user secret nevyvolává automatický restart podu. Nové hodnoty se projeví až při next deploy (rolling restart). FE zobrazuje persistent hint v Secrets tabu:
“Secret changes are applied on next deployment. Re-deploy your app to activate the changes.”
Mara Widget — request-secret
JSON payload schema
Mara emituje fenced blok v konverzaci:
�FENCE�19
| Pole | Typ | Povinné | Popis |
|---|---|---|---|
kind | "request-secret" | ano | diskriminátor widgetu |
name | string | ano | název secret (musí splňovat regex, Mara volí snake_case uppercase) |
description | string | ne | nápověda pro uživatele co zadat |
envId | number | ano | ID cílového prostředí |
projectId | number | ano | ID projektu |
FE komponent — TalkidePromptCard (nový case)
TalkidePromptCard.vue dostane nový case request-secret vedle stávajících:
| UI prvek | Detail |
|---|---|
| Label | name pole (tučně) |
| Popis | description pole (šedě pod labelem) |
| Env badge | read-only chip s názvem prostředí (lookup přes envId) |
| Input | <input type="password" autocomplete="off"> — value NIKDY v store |
| Save button | POST na /api/v1/projects/{projectId}/environments/{envId}/secrets |
Flow po Save
- FE POST na secrets endpoint s
{ name, value }. - BE vrátí
201 Created. - FE injektuje do konverzace user message (text, NE přes Mara agent): “Secret STRIPE_API_KEY stored in production environment.”
- Value je okamžitě zahozena z input DOM — přechod na masked state.
- Mara ve svém dalším turn vidí ack a pokračuje v práci.
Value NIKDY neprojde Pinia store, SSE stream, ani conversation history v DB.
Agent Context Injection
Všichni tři agenti (Mara, Theo, Kai) dostávají seznam secrets per prostředí do svého claudeMdContent.
Formát sekce
�FENCE�20
Pravidla:
- Masked preview se čte přímo z DB
masked_previewsloupce (nikdy z K8s — K8s value není agent-accessible). - Pokud projekt má
environment_id = NULL, použije se DEFAULT env tenanta (slug=talkide). - Sekce se vloží do
claudeMdContentkaždého agenta.
Implementační poznámka — context rendery
MaraContextRenderer.kt (com.mddsummer.talkide.features.conversation.domain.MaraContextRenderer) je identifikovaný. Analogické rendery pro Theo a Kai persona:
TBD path discovery during impl — backend developer při implementaci dohledá renderer třídy pro Theo a Kai (pravděpodobně TheoContextRenderer.kt a KaiContextRenderer.kt ve stejném feature package nebo v features/conversation/domain/). Sekce ## Available Secrets se přidá do všech tří rendererů konzistentně.
Pokud sdílí společný base/builder, stačí přidat sekci jednou do společné logiky.
Frontend — Project Settings (refactor na taby)
Refactor ProjectSettingsPanel.vue
ProjectSettingsPanel.vue se refaktoruje z flat sekcí na tabbed layout:
| Tab | Obsah |
|---|---|
| General | Stávající obecná nastavení projektu |
| Environments | Výběr cílového prostředí projektu (vazba project.environment_id) |
| Secrets | Správa secrets (tato UC) |
| Danger | Archivace, smazání projektu |
Tab Secrets — UI flow
- Uživatel otevře Project Settings → klikne na záložku Secrets.
- Env selector (dropdown) — defaultní hodnota = aktuální prostředí projektu nebo DEFAULT.
- Změna env selectoru načte secrets pro dané prostředí (
GET /api/v1/projects/{id}/environments/{envId}/secrets). - Tabulka: sloupce
Name | Masked preview | Updated | Actions. - Empty state: “No secrets yet. Add one to inject env vars into your app.”
- Add Secret button → modal:
- Name input (text, uppercase suggestion/hint)
- Value input (
type="password",autocomplete="off") - Save → POST → row přibude, modal se zavře
- Edit (row action) → modal:
- Name (read-only chip)
- New value input (
type="password") - Save → PUT → row se aktualizuje
- Delete (row action) → confirm dialog → DELETE → row se odstraní
- Persistent hint pod tabulkou: “Secret changes are applied on next deployment. Re-deploy your app to activate the changes.”
Validace — Frontend
| Pole | Constraints | Velikost | Pattern | Poznámka |
|---|---|---|---|---|
name | not_blank | 1 – 64 | ^[A-Z][A-Z0-9_]{0,63}$ | hint: uppercase + underscore; inline regex error |
value | not_blank | 1 – 4096 | — | type="password", nikdy v store |
Backend
Validace — Backend
| Pole | Constraints | Velikost | Pattern | Poznámka |
|---|---|---|---|---|
name (POST) | not_blank | 1 – 64 | ^[A-Z][A-Z0-9_]{0,63}$ | 400 pokud pattern nesedí |
name (URL path, PUT/DELETE) | not_blank | 1 – 64 | ^[A-Z][A-Z0-9_]{0,63}$ | 400 pokud pattern nesedí |
value (POST/PUT) | not_blank, no null bytes (�) | 1 – 4096 | — | 400 pokud překročí nebo obsahuje null byte |
projectId | existující projekt, tenant scope, user je owner | — | — | 404 NOT_FOUND_PROJECT pokud neexistuje, cizí tenant nebo user není owner (anti-enumeration) |
environmentId | existující prostředí patřící projektu nebo DEFAULT | — | — | 404 pokud neexistuje |
Masked preview výpočet: value.take(4) + "***" + value.takeLast(4). Pokud je value kratší než 8 znaků, zobrazí se **** (celá hodnota maskována).
Audit log: každá write operace (POST/PUT/DELETE) generuje BE INFO log ve formátu: �FENCE�21 Value se NIKDY neobjevuje v logu.
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
TC-001: Projekt existuje, user je owner, secret STRIPE_API_KEY neexistuje | POST /secrets s validním name a value | 201 Created; DB row s masked_preview; K8s Secret obsahuje klíč STRIPE_API_KEY |
TC-002: Secret STRIPE_API_KEY již existuje v prostředí | POST /secrets se stejným názvem | 409 Conflict, code=CONFLICT_ENVIRONMENT_SECRET; žádná změna v DB ani K8s |
| TC-003: Name nesplňuje regex (obsahuje lowercase nebo začíná číslicí) | POST /secrets s name stripe_key nebo 1KEY | 400 Bad Request, code=VALIDATION |
| TC-004: Value má 4097 znaků | POST /secrets s přetékající value | 400 Bad Request, code=VALIDATION |
| TC-005: Prostředí má 3 secrets | GET /secrets pro daný (projectId, envId) | 200 OK; pole 3 items; masked_preview správně; žádná raw value v response |
| TC-006: Secret existuje | PUT /secrets/{name} s novou validní value | 200 OK; masked_preview aktualizován; K8s Secret patchován; DB updated_at aktualizován |
| TC-007: Secret existuje | DELETE /secrets/{name} | 204 No Content; DB row smazán; K8s Secret key odstraněn |
| TC-008: User není project owner (jiný authenticated user pro daný projekt nebo cross-tenant projekt) | POST /secrets nebo GET /secrets | 404 Not Found, code=NOT_FOUND_PROJECT (anti-enumeration — neenumeruje existenci projektů) |
| TC-009: projectId patří jinému tenantovi (cross-tenant) | GET /secrets s cizím projectId | 404 Not Found, code=NOT_FOUND_PROJECT (stejný response jako TC-008 — anti-enumeration pattern je konzistentní pro non-owner i cross-tenant) |
TC-010: Mara emituje request-secret widget; user vyplní value a klikne Save | FE POSTuje na secrets endpoint | 201 Created; ack message v konverzaci; value není v conversation DB |
| TC-011: Secret vytvořen přes API | kubectl get secret {project-slug}-user-secrets -n {tenant-slug}-{env-slug} -o jsonpath='{.data.STRIPE_API_KEY}' | base64-decoded value odpovídá zadané hodnotě |
TC-012: Value je sk_live_abc1234 (16 znaků) | POST /secrets | maskedPreview = "sk_l***1234" (první 4 + *** + poslední 4) |
| TC-013: Projekt má 2 secrets ve dvou prostředích | Agent context fetch pro daný projekt | claudeMdContent obsahuje sekci ## Available Secrets s oběma prostředími a masked preview; raw values nejsou přítomny |
Out of Scope (MVP)
- Audit log UI — history view kdo a kdy secret měnil (následující UC)
- Per-secret RBAC — pouze project owner v MVP; team role extension v dalším UC
- Secret rotation automation — manuální update přes PUT
- External KMS integration — Vault, AWS KMS, DO Secrets Manager
- Worker direct secret API — worker dostává POUZE masked preview přes claudeMdContent, žádný přímý přístup k hodnotám
- Secret picker widget (
pick-secret) — jenrequest-secretv MVP - Bulk import secrets (ENV file upload)
- Secret scoping na branch/preview deploy (v MVP = per env, ne per deploy)
**Liquibase changeset**: nový soubor s dalším pořadovým číslem po posledním existujícím changesétu. Backend developer přiřadí číslo při implementaci (PRODUCTION fáze — immutable constraint).
�FENCE�1
---
## API Endpoints
### GET /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets
Vrátí seznam secrets pro dané prostředí projektu. Pouze metadata — hodnoty NIKDY v response.
�FENCE�2
`200 OK` **SecretListResponse**:
�FENCE�3
`404 Not Found` **ErrorResponse** *(non-owner nebo cross-tenant — anti-enumeration)*:
�FENCE�4
---
### POST /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets
Vytvoří nový secret. Value je synchronně zapsána do K8s Secret, do DB se uloží pouze metadata + masked_preview.
�FENCE�5
`POST /api/v1/projects/{projectId}/environments/{envId}/secrets` **CreateSecretRequest**:
�FENCE�6
`201 Created` **SecretResponse**:
�FENCE�7
`400 Bad Request` *(validation)* **ErrorResponse**:
�FENCE�8
`404 Not Found` **ErrorResponse** *(non-owner nebo cross-tenant — anti-enumeration)*:
�FENCE�9
`409 Conflict` **ErrorResponse**:
�FENCE�10
---
### PUT /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets/{`{`}name{`}`}
Aktualizuje hodnotu existujícího secret. Name je immutabilní (je součástí URL, ne request body). K8s Secret se patchuje idempotentně.
�FENCE�11
`PUT /api/v1/projects/{projectId}/environments/{envId}/secrets/{name}` **UpdateSecretRequest**:
�FENCE�12
`200 OK` **SecretResponse**:
�FENCE�13
`400 Bad Request` *(validation)* **ErrorResponse**:
�FENCE�14
`404 Not Found` **ErrorResponse** *(non-owner, cross-tenant nebo secret nenalezen — anti-enumeration)*:
�FENCE�15
---
### DELETE /api/v1/projects/{`{`}projectId{`}`}/environments/{`{`}envId{`}`}/secrets/{`{`}name{`}`}
Odstraní secret — smaže key z K8s Secret a odstraní DB řádek.
�FENCE�16
`204 No Content` — prázdné tělo.
`404 Not Found` **ErrorResponse** *(non-owner, cross-tenant nebo secret nenalezen — anti-enumeration)*:
�FENCE�17
---
## K8s Integration
### Secret naming a namespace
| Prvek | Hodnota | Příklad |
|-------|---------|---------|
| Secret name | `{project-slug}-user-secrets` | `todo-list-user-secrets` |
| Namespace | `{tenant-slug}-{env-slug}` | `popelkam-talkide` (DEFAULT), `popelkam-production` (user env) |
| Secret type | `Opaque` | — |
| Keys | secret names (env var names) | `STRIPE_API_KEY`, `OPENAI_KEY` |
| Values | `base64(user value)` | K8s kódování automaticky při create/patch |
Naming záměrně neobsahuje tenant prefix v Secret name — namespace ho drží (bez redundance).
### K8sSecretSyncer
Nová service/komponenta `K8sSecretSyncer` (vzor: `PostgresDatabaseProvisioner` — Fabric8 CRUD přes `client.secrets().inNamespace(ns).resource(secret).create()`):
| Operace | Behavior |
|---------|----------|
| Create (Secret neexistuje) | `client.secrets().inNamespace(ns).resource(newSecret).create()` |
| Add/Update key (Secret existuje) | `client.secrets().inNamespace(ns).withName(secretName).patch(patchedSecret)` |
| Remove key | PATCH Secret — odstraní key; pokud Secret poté nemá žádný key, Secret zůstane (prázdný je OK) |
| Secret neexistuje při DELETE | gracefully skip (idempotent) |
### K8sAppDeployer — wiring
`K8sAppDeployer.buildDeployment` dostane dva `envFrom.secretRef` zdroje:
�FENCE�18
`optional: true` zajišťuje, že pod nastartuje i bez user secrets (projekt bez secrets = normální stav).
### Pod restart policy
Změna user secret **nevyvolává automatický restart podu**. Nové hodnoty se projeví až při next deploy (rolling restart). FE zobrazuje persistent hint v Secrets tabu:
> *"Secret changes are applied on next deployment. Re-deploy your app to activate the changes."*
---
## Mara Widget — request-secret
### JSON payload schema
Mara emituje fenced blok v konverzaci:
�FENCE�19
| Pole | Typ | Povinné | Popis |
|------|-----|---------|-------|
| `kind` | `"request-secret"` | ano | diskriminátor widgetu |
| `name` | `string` | ano | název secret (musí splňovat regex, Mara volí snake_case uppercase) |
| `description` | `string` | ne | nápověda pro uživatele co zadat |
| `envId` | `number` | ano | ID cílového prostředí |
| `projectId` | `number` | ano | ID projektu |
### FE komponent — TalkidePromptCard (nový case)
`TalkidePromptCard.vue` dostane nový case `request-secret` vedle stávajících:
| UI prvek | Detail |
|---------|--------|
| Label | `name` pole (tučně) |
| Popis | `description` pole (šedě pod labelem) |
| Env badge | read-only chip s názvem prostředí (lookup přes `envId`) |
| Input | `<input type="password" autocomplete="off">` — value NIKDY v store |
| Save button | POST na `/api/v1/projects/{projectId}/environments/{envId}/secrets` |
### Flow po Save
1. FE POST na secrets endpoint s `{ name, value }`.
2. BE vrátí `201 Created`.
3. FE injektuje do konverzace user message (text, NE přes Mara agent): *"Secret STRIPE_API_KEY stored in production environment."*
4. Value je okamžitě zahozena z input DOM — přechod na masked state.
5. Mara ve svém dalším turn vidí ack a pokračuje v práci.
Value NIKDY neprojde Pinia store, SSE stream, ani conversation history v DB.
---
## Agent Context Injection
Všichni tři agenti (Mara, Theo, Kai) dostávají seznam secrets per prostředí do svého `claudeMdContent`.
### Formát sekce
�FENCE�20
Pravidla:
- Masked preview se čte přímo z DB `masked_preview` sloupce (nikdy z K8s — K8s value není agent-accessible).
- Pokud projekt má `environment_id = NULL`, použije se DEFAULT env tenanta (`slug=talkide`).
- Sekce se vloží do `claudeMdContent` každého agenta.
### Implementační poznámka — context rendery
`MaraContextRenderer.kt` (`com.mddsummer.talkide.features.conversation.domain.MaraContextRenderer`) je identifikovaný. Analogické rendery pro Theo a Kai persona:
**TBD path discovery during impl** — backend developer při implementaci dohledá renderer třídy pro Theo a Kai (pravděpodobně `TheoContextRenderer.kt` a `KaiContextRenderer.kt` ve stejném feature package nebo v `features/conversation/domain/`). Sekce `## Available Secrets` se přidá do všech tří rendererů konzistentně.
Pokud sdílí společný base/builder, stačí přidat sekci jednou do společné logiky.
---
## Frontend — Project Settings (refactor na taby)
### Refactor ProjectSettingsPanel.vue
`ProjectSettingsPanel.vue` se refaktoruje z flat sekcí na tabbed layout:
| Tab | Obsah |
|-----|-------|
| **General** | Stávající obecná nastavení projektu |
| **Environments** | Výběr cílového prostředí projektu (vazba `project.environment_id`) |
| **Secrets** | Správa secrets (tato UC) |
| **Danger** | Archivace, smazání projektu |
### Tab Secrets — UI flow
1. Uživatel otevře Project Settings → klikne na záložku **Secrets**.
2. Env selector (dropdown) — defaultní hodnota = aktuální prostředí projektu nebo DEFAULT.
3. Změna env selectoru načte secrets pro dané prostředí (`GET /api/v1/projects/{id}/environments/{envId}/secrets`).
4. Tabulka: sloupce `Name | Masked preview | Updated | Actions`.
5. **Empty state**: *"No secrets yet. Add one to inject env vars into your app."*
6. **Add Secret** button → modal:
- Name input (text, uppercase suggestion/hint)
- Value input (`type="password"`, `autocomplete="off"`)
- Save → POST → row přibude, modal se zavře
7. **Edit** (row action) → modal:
- Name (read-only chip)
- New value input (`type="password"`)
- Save → PUT → row se aktualizuje
8. **Delete** (row action) → confirm dialog → DELETE → row se odstraní
9. Persistent hint pod tabulkou: *"Secret changes are applied on next deployment. Re-deploy your app to activate the changes."*
### Validace — Frontend
| Pole | Constraints | Velikost | Pattern | Poznámka |
|------|-------------|----------|---------|---------|
| `name` | not_blank | 1 – 64 | `^[A-Z][A-Z0-9_]{0,63}$` | hint: uppercase + underscore; inline regex error |
| `value` | not_blank | 1 – 4096 | — | `type="password"`, nikdy v store |
---
## Backend
### Validace — Backend
| Pole | Constraints | Velikost | Pattern | Poznámka |
|------|-------------|----------|---------|---------|
| `name` (POST) | not_blank | 1 – 64 | `^[A-Z][A-Z0-9_]{0,63}$` | 400 pokud pattern nesedí |
| `name` (URL path, PUT/DELETE) | not_blank | 1 – 64 | `^[A-Z][A-Z0-9_]{0,63}$` | 400 pokud pattern nesedí |
| `value` (POST/PUT) | not_blank, no null bytes (`�`) | 1 – 4096 | — | 400 pokud překročí nebo obsahuje null byte |
| `projectId` | existující projekt, tenant scope, user je owner | — | — | 404 `NOT_FOUND_PROJECT` pokud neexistuje, cizí tenant nebo user není owner (anti-enumeration) |
| `environmentId` | existující prostředí patřící projektu nebo DEFAULT | — | — | 404 pokud neexistuje |
**Masked preview** výpočet: `value.take(4) + "***" + value.takeLast(4)`. Pokud je value kratší než 8 znaků, zobrazí se `****` (celá hodnota maskována).
**Audit log**: každá write operace (POST/PUT/DELETE) generuje BE INFO log ve formátu:
�FENCE�21
Value se NIKDY neobjevuje v logu.
### Test Cases
| GIVEN | WHEN | THEN |
|-------|------|------|
| TC-001: Projekt existuje, user je owner, secret `STRIPE_API_KEY` neexistuje | `POST /secrets` s validním name a value | 201 Created; DB row s masked_preview; K8s Secret obsahuje klíč `STRIPE_API_KEY` |
| TC-002: Secret `STRIPE_API_KEY` již existuje v prostředí | `POST /secrets` se stejným názvem | 409 Conflict, `code=CONFLICT_ENVIRONMENT_SECRET`; žádná změna v DB ani K8s |
| TC-003: Name nesplňuje regex (obsahuje lowercase nebo začíná číslicí) | `POST /secrets` s name `stripe_key` nebo `1KEY` | 400 Bad Request, `code=VALIDATION` |
| TC-004: Value má 4097 znaků | `POST /secrets` s přetékající value | 400 Bad Request, `code=VALIDATION` |
| TC-005: Prostředí má 3 secrets | `GET /secrets` pro daný (projectId, envId) | 200 OK; pole 3 items; masked_preview správně; žádná raw value v response |
| TC-006: Secret existuje | `PUT /secrets/{name}` s novou validní value | 200 OK; masked_preview aktualizován; K8s Secret patchován; DB `updated_at` aktualizován |
| TC-007: Secret existuje | `DELETE /secrets/{name}` | 204 No Content; DB row smazán; K8s Secret key odstraněn |
| TC-008: User není project owner (jiný authenticated user pro daný projekt nebo cross-tenant projekt) | `POST /secrets` nebo `GET /secrets` | 404 Not Found, `code=NOT_FOUND_PROJECT` (anti-enumeration — neenumeruje existenci projektů) |
| TC-009: projectId patří jinému tenantovi (cross-tenant) | `GET /secrets` s cizím projectId | 404 Not Found, `code=NOT_FOUND_PROJECT` (stejný response jako TC-008 — anti-enumeration pattern je konzistentní pro non-owner i cross-tenant) |
| TC-010: Mara emituje `request-secret` widget; user vyplní value a klikne Save | FE POSTuje na secrets endpoint | 201 Created; ack message v konverzaci; value není v conversation DB |
| TC-011: Secret vytvořen přes API | `kubectl get secret {project-slug}-user-secrets -n {tenant-slug}-{env-slug} -o jsonpath='{.data.STRIPE_API_KEY}'` | base64-decoded value odpovídá zadané hodnotě |
| TC-012: Value je `sk_live_abc1234` (16 znaků) | `POST /secrets` | `maskedPreview = "sk_l***1234"` (první 4 + *** + poslední 4) |
| TC-013: Projekt má 2 secrets ve dvou prostředích | Agent context fetch pro daný projekt | `claudeMdContent` obsahuje sekci `## Available Secrets` s oběma prostředími a masked preview; raw values nejsou přítomny |
---
## Out of Scope (MVP)
* Audit log UI — history view kdo a kdy secret měnil (následující UC)
* Per-secret RBAC — pouze project owner v MVP; team role extension v dalším UC
* Secret rotation automation — manuální update přes PUT
* External KMS integration — Vault, AWS KMS, DO Secrets Manager
* Worker direct secret API — worker dostává POUZE masked preview přes claudeMdContent, žádný přímý přístup k hodnotám
* Secret picker widget (`pick-secret`) — jen `request-secret` v MVP
* Bulk import secrets (ENV file upload)
* Secret scoping na branch/preview deploy (v MVP = per env, ne per deploy)
---
Thanks for the feedback.