fix(chat): block accidental AI secrets #83

Merged
m merged 1 commits from codex/creator-chat-model-status-v1 into main 2026-08-21 10:39:57 +00:00
6 changed files with 45 additions and 3 deletions
+1 -1
View File
@@ -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)
}
+1
View File
@@ -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; }
+17
View File
@@ -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 = "";
+1
View File
@@ -79,6 +79,7 @@
<label for="ai-data">Данные для текущего разговора</label>
<textarea id="ai-data" rows="8" maxlength="12000" placeholder="Например: локальная модель нужна для коротких ответов на русском; контекст ограничен; внешняя сеть запрещена."></textarea>
<button type="submit">Передать Домовому</button>
<p id="ai-data-error" class="ai-data-error" role="alert" hidden></p>
<small>Это отправится как обычное сообщение в чат.</small>
</form>
<a class="side-console-link" href="/admin">Открыть консоль настройки <span>→</span></a>
+2 -2
View File
@@ -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
}
@@ -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"]
}