fix(chat): block accidental AI secrets #83
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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 = "";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user