From 0a2599cfa6255f1a98856d2fd09dc51a2119f525 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 13:38:57 +0300 Subject: [PATCH] fix(chat): block accidental AI secrets --- cmd/creator-web/chat_test.go | 2 +- cmd/creator-web/static/assets/chat.css | 1 + cmd/creator-web/static/assets/chat.js | 17 ++++++++++++++ cmd/creator-web/static/chat.html | 1 + dev/ACTIVE_TASK.json | 4 ++-- .../DEV-000082-chat-ai-data-safety-guard.json | 23 +++++++++++++++++++ 6 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 dev/tasks/DEV-000082-chat-ai-data-safety-guard.json diff --git a/cmd/creator-web/chat_test.go b/cmd/creator-web/chat_test.go index a1b856e..736ae89 100644 --- a/cmd/creator-web/chat_test.go +++ b/cmd/creator-web/chat_test.go @@ -25,7 +25,7 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) { t.Fatalf("expected /chat status 200, got %d", recorder.Code) } body := recorder.Body.String() - for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку"} { + for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли"} { if !strings.Contains(body, required) { t.Fatalf("/chat is missing human status marker %q", required) } diff --git a/cmd/creator-web/static/assets/chat.css b/cmd/creator-web/static/assets/chat.css index c2eb6b7..e4b040a 100644 --- a/cmd/creator-web/static/assets/chat.css +++ b/cmd/creator-web/static/assets/chat.css @@ -79,6 +79,7 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; .ai-data-form { margin-top: 24px; } .ai-data-form textarea { min-height: 150px; resize: vertical; } .ai-data-form button { width: 100%; min-height: 42px; margin-top: 12px; border: 0; color: #11150f; background: var(--signal); font-weight: 700; } +.ai-data-error { margin: 10px 0 0; color: var(--bad); font-size: 12px; line-height: 1.4; } .side-console-link { display: flex; justify-content: space-between; margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--signal) !important; font: 11px/1.3 var(--mono) !important; text-transform: uppercase; letter-spacing: .06em; } .attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; } .surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; } diff --git a/cmd/creator-web/static/assets/chat.js b/cmd/creator-web/static/assets/chat.js index 988cbb2..9c7e694 100644 --- a/cmd/creator-web/static/assets/chat.js +++ b/cmd/creator-web/static/assets/chat.js @@ -17,12 +17,23 @@ const navTabs = Array.from(document.querySelectorAll(".nav-tab")); const conversationStarters = Array.from(document.querySelectorAll("[data-starter]")); const aiDataForm = document.querySelector("#ai-data-form"); const aiDataInput = document.querySelector("#ai-data"); +const aiDataError = document.querySelector("#ai-data-error"); let token = sessionStorage.getItem("domovoy.chat.token") || ""; let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || ""; let localHistory = []; let cognitiveStatusTimer = null; +function containsSecretShapedText(value) { + return /(-----BEGIN [A-Z ]+PRIVATE KEY-----|\b(?:password|passwd|пароль|token|токен|api[_ -]?key|секрет|ключ)\s*[:=]|\bsk-[a-zA-Z0-9_-]{12,})/i.test(value); +} + +function showAIDataError(message = "") { + if (!aiDataError) return; + aiDataError.textContent = message; + aiDataError.hidden = !message; +} + function renderCognitiveStatus(state) { if (!cognitiveStatus) return; const normalized = String(state || "UNKNOWN").toUpperCase(); @@ -300,10 +311,16 @@ if (aiDataForm) { event.preventDefault(); const details = aiDataInput.value.trim(); if (!details || sendButton.disabled) return; + if (containsSecretShapedText(details)) { + showAIDataError("Не отправляю секреты. Удалите пароль, токен, API-ключ или приватный ключ и оставьте только описание модели."); + return; + } + showAIDataError(); aiDataInput.value = ""; await sendChatMessage(`Передаю сведения о локальном ИИ для текущего разговора:\n${details}`); }); } +if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError()); logoutButton.addEventListener("click", () => { token = ""; diff --git a/cmd/creator-web/static/chat.html b/cmd/creator-web/static/chat.html index ec7b181..8ffbc45 100644 --- a/cmd/creator-web/static/chat.html +++ b/cmd/creator-web/static/chat.html @@ -79,6 +79,7 @@ + Это отправится как обычное сообщение в чат. Открыть консоль настройки → diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 5185248..b656726 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000081-chat-header-final-polish.json", - "task_id": "DEV-000081", + "task_file": "dev/tasks/DEV-000082-chat-ai-data-safety-guard.json", + "task_id": "DEV-000082", "task_revision": 1 } diff --git a/dev/tasks/DEV-000082-chat-ai-data-safety-guard.json b/dev/tasks/DEV-000082-chat-ai-data-safety-guard.json new file mode 100644 index 0000000..8d0eb0d --- /dev/null +++ b/dev/tasks/DEV-000082-chat-ai-data-safety-guard.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000082", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "ae9933ee846254f970e4ef3f3e46d63e7a01db5a", + "risk_tier": "E", + "intent": "Prevent accidental transmission of obvious credentials through the AI information form while preserving ordinary model notes as chat messages.", + "requirement_ids": ["AI notes form warns about secrets", "Obvious secret-shaped input is blocked before chat submission", "Safe model descriptions continue through the normal chat flow"], + "allowed_change_scope": {"modules": ["cmd/creator-web"], "paths": ["cmd/creator-web/chat_test.go", "cmd/creator-web/static/chat.html", "cmd/creator-web/static/assets/chat.css", "cmd/creator-web/static/assets/chat.js", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000082-chat-ai-data-safety-guard.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing chat message endpoint"], "may_change": []}, + "semantic_owner": "none-interface-adapter", + "canonical_state": [], + "state_transitions": ["Secret-shaped text is rejected locally", "Non-secret AI notes continue as a normal conversation message"], + "side_effects_and_privileges": ["No new privilege", "No transmission when the guard detects a secret-shaped value"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["User can edit and retry after removing the sensitive value"], + "assumptions": ["The guard is a convenience barrier, not a complete secret scanner", "Users must still avoid pasting secrets"], + "acceptance_scenarios": ["API-key and private-key-shaped input is blocked", "Password/token labels are blocked", "Ordinary model notes are submitted", "The warning is visible and accessible"], + "required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +} -- 2.54.0