Internal Documentation internal
TalkIDE internal documentation

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-secrets v namespace {tenant-slug}-{env-slug}. Keys = secret names, values = base64(user value). Přidán jako druhý envFrom.secretRef v K8sAppDeployer.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 blok talkide-prompt s kind: "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)

InvariantZdůvodnění
Value NIKDY v PostgreSQLDB je wider-scope; K8s Secret RBAC je granulárněji řízeno
Value NIKDY v conversation loguConversation history je persistována v DB a přenášena do Claude API
Value NIKDY v BE logáchLogback pattern nesmí logovat request body secrets endpointů
Value NIKDY v Pinia storeFE store je přenášen do SSE stream a debug toolů
Value NIKDY v worker claudeMdContentWorker dostává jen masked preview (viz §Agent context)
Worker→BE secrets API NEEXISTUJEADR-024 §C4: worker nemá user JWT, nemůže volat privilegované endpointy
HTTPS onlyTLS 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-tenantVrací 404 NOT_FOUND_PROJECT (ne 403) — attacker nesmí zjistit existenci cizích projektů

Entity Model

Tabulka environment_secrets

SloupecTypConstraintsPoznámka
idbigintPK, NOT NULLgenerovaný sekvenčně
project_idbigintNOT NULL, FK → projects(id) ON DELETE CASCADEvazba na projekt
environment_idbigintNOT NULL, FK → environment(id)NIKDY NULL — resolver vždy doplní DEFAULT env
namevarchar(64)NOT NULLklíč env var; regex ^[A-Z][A-Z0-9_]{0,63}$
masked_previewvarchar(32)NOT NULLcached při zápisu (první 4 + *** + poslední 4 chars value); např. sk_l***1234
created_attimestamptzNOT NULL
updated_attimestamptzNOT NULLaktualizuje se při PUT
created_by_user_idbigintNOT 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

PrvekHodnotaPříklad
Secret name{project-slug}-user-secretstodo-list-user-secrets
Namespace{tenant-slug}-{env-slug}popelkam-talkide (DEFAULT), popelkam-production (user env)
Secret typeOpaque
Keyssecret names (env var names)STRIPE_API_KEY, OPENAI_KEY
Valuesbase64(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()):

OperaceBehavior
Create (Secret neexistuje)client.secrets().inNamespace(ns).resource(newSecret).create()
Add/Update key (Secret existuje)client.secrets().inNamespace(ns).withName(secretName).patch(patchedSecret)
Remove keyPATCH Secret — odstraní key; pokud Secret poté nemá žádný key, Secret zůstane (prázdný je OK)
Secret neexistuje při DELETEgracefully 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
}
```
PoleTypPovinnéPopis
kind"request-secret"anodiskriminátor widgetu
namestringanonázev secret (musí splňovat regex, Mara volí snake_case uppercase)
descriptionstringnenápověda pro uživatele co zadat
envIdnumberanoID cílového prostředí
projectIdnumberanoID projektu

FE komponent — TalkidePromptCard (nový case)

TalkidePromptCard.vue dostane nový case request-secret vedle stávajících:

UI prvekDetail
Labelname pole (tučně)
Popisdescription pole (šedě pod labelem)
Env badgeread-only chip s názvem prostředí (lookup přes envId)
Input<input type="password" autocomplete="off"> — value NIKDY v store
Save buttonPOST 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

## 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_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:

TabObsah
GeneralStávající obecná nastavení projektu
EnvironmentsVýběr cílového prostředí projektu (vazba project.environment_id)
SecretsSpráva secrets (tato UC)
DangerArchivace, 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

PoleConstraintsVelikostPatternPoznámka
namenot_blank1 – 64^[A-Z][A-Z0-9_]{0,63}$hint: uppercase + underscore; inline regex error
valuenot_blank1 – 4096type="password", nikdy v store

Backend

Validace — Backend

PoleConstraintsVelikostPatternPoznámka
name (POST)not_blank1 – 64^[A-Z][A-Z0-9_]{0,63}$400 pokud pattern nesedí
name (URL path, PUT/DELETE)not_blank1 – 64^[A-Z][A-Z0-9_]{0,63}$400 pokud pattern nesedí
value (POST/PUT)not_blank, no null bytes ()1 – 4096400 pokud překročí nebo obsahuje null byte
projectIdexistující projekt, tenant scope, user je owner404 NOT_FOUND_PROJECT pokud neexistuje, cizí tenant nebo user není owner (anti-enumeration)
environmentIdexistující prostředí patřící projektu nebo DEFAULT404 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

GIVENWHENTHEN
TC-001: Projekt existuje, user je owner, secret STRIPE_API_KEY neexistujePOST /secrets s validním name a value201 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ázvem409 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 1KEY400 Bad Request, code=VALIDATION
TC-004: Value má 4097 znakůPOST /secrets s přetékající value400 Bad Request, code=VALIDATION
TC-005: Prostředí má 3 secretsGET /secrets pro daný (projectId, envId)200 OK; pole 3 items; masked_preview správně; žádná raw value v response
TC-006: Secret existujePUT /secrets/{name} s novou validní value200 OK; masked_preview aktualizován; K8s Secret patchován; DB updated_at aktualizován
TC-007: Secret existujeDELETE /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 /secrets404 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 projectId404 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 SaveFE POSTuje na secrets endpoint201 Created; ack message v konverzaci; value není v conversation DB
TC-011: Secret vytvořen přes APIkubectl 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 /secretsmaskedPreview = "sk_l***1234" (první 4 + *** + poslední 4)
TC-013: Projekt má 2 secrets ve dvou prostředíchAgent context fetch pro daný projektclaudeMdContent 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)


**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

PrvekHodnotaPříklad
Secret name{project-slug}-user-secretstodo-list-user-secrets
Namespace{tenant-slug}-{env-slug}popelkam-talkide (DEFAULT), popelkam-production (user env)
Secret typeOpaque
Keyssecret names (env var names)STRIPE_API_KEY, OPENAI_KEY
Valuesbase64(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()):

OperaceBehavior
Create (Secret neexistuje)client.secrets().inNamespace(ns).resource(newSecret).create()
Add/Update key (Secret existuje)client.secrets().inNamespace(ns).withName(secretName).patch(patchedSecret)
Remove keyPATCH Secret — odstraní key; pokud Secret poté nemá žádný key, Secret zůstane (prázdný je OK)
Secret neexistuje při DELETEgracefully 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

PoleTypPovinnéPopis
kind"request-secret"anodiskriminátor widgetu
namestringanonázev secret (musí splňovat regex, Mara volí snake_case uppercase)
descriptionstringnenápověda pro uživatele co zadat
envIdnumberanoID cílového prostředí
projectIdnumberanoID projektu

FE komponent — TalkidePromptCard (nový case)

TalkidePromptCard.vue dostane nový case request-secret vedle stávajících:

UI prvekDetail
Labelname pole (tučně)
Popisdescription pole (šedě pod labelem)
Env badgeread-only chip s názvem prostředí (lookup přes envId)
Input<input type="password" autocomplete="off"> — value NIKDY v store
Save buttonPOST 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:

TabObsah
GeneralStávající obecná nastavení projektu
EnvironmentsVýběr cílového prostředí projektu (vazba project.environment_id)
SecretsSpráva secrets (tato UC)
DangerArchivace, 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

PoleConstraintsVelikostPatternPoznámka
namenot_blank1 – 64^[A-Z][A-Z0-9_]{0,63}$hint: uppercase + underscore; inline regex error
valuenot_blank1 – 4096type="password", nikdy v store

Backend

Validace — Backend

PoleConstraintsVelikostPatternPoznámka
name (POST)not_blank1 – 64^[A-Z][A-Z0-9_]{0,63}$400 pokud pattern nesedí
name (URL path, PUT/DELETE)not_blank1 – 64^[A-Z][A-Z0-9_]{0,63}$400 pokud pattern nesedí
value (POST/PUT)not_blank, no null bytes ()1 – 4096400 pokud překročí nebo obsahuje null byte
projectIdexistující projekt, tenant scope, user je owner404 NOT_FOUND_PROJECT pokud neexistuje, cizí tenant nebo user není owner (anti-enumeration)
environmentIdexistující prostředí patřící projektu nebo DEFAULT404 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

GIVENWHENTHEN
TC-001: Projekt existuje, user je owner, secret STRIPE_API_KEY neexistujePOST /secrets s validním name a value201 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ázvem409 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 1KEY400 Bad Request, code=VALIDATION
TC-004: Value má 4097 znakůPOST /secrets s přetékající value400 Bad Request, code=VALIDATION
TC-005: Prostředí má 3 secretsGET /secrets pro daný (projectId, envId)200 OK; pole 3 items; masked_preview správně; žádná raw value v response
TC-006: Secret existujePUT /secrets/{name} s novou validní value200 OK; masked_preview aktualizován; K8s Secret patchován; DB updated_at aktualizován
TC-007: Secret existujeDELETE /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 /secrets404 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 projectId404 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 SaveFE POSTuje na secrets endpoint201 Created; ack message v konverzaci; value není v conversation DB
TC-011: Secret vytvořen přes APIkubectl 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 /secretsmaskedPreview = "sk_l***1234" (první 4 + *** + poslední 4)
TC-013: Projekt má 2 secrets ve dvou prostředíchAgent context fetch pro daný projektclaudeMdContent 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)


**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)

---

Was this page helpful?

Thanks for the feedback.