Перейти к содержанию

24. Data и Privacy

Зачем это нужно?

Агент работает с персональными данными пользователей (email, телефон, адрес). Эти данные попадают в логи и отправляются в LLM API. Без защиты данных вы нарушаете GDPR и рискуете утечкой данных.

Реальный кейс

Ситуация: Агент обрабатывает запрос пользователя: "Мой email john@example.com, телефон +7-999-123-4567. Создай тикет".

Проблема: PII попадает в логи, трейсы и вывод инструментов без маскирования. При утечке логов персональные данные попадают в чужие руки.

Решение: Обнаружение и маскирование PII на границе логов, трассировок и вывода инструментов (вход модели не трогаем — агенту нужны данные), redaction по умолчанию, защита секретов, TTL для хранения.

Junior employee model. Data privacy для агента — это те же data-policies, что для нового сотрудника. Он подписывает NDA, не пересылает клиентские email-ы наружу, не складывает скриншоты с PII в публичный канал, удаляет персональные данные после отчётного периода (retention). Агент так же: не пишет PII в спаны/логи как есть (см. Гл. 19, правило про redacted-атрибуты), не передаёт лишнее в LLM-провайдера, удаляет данные по TTL. Принципиально нового тут ничего нет — это адаптация знакомых GDPR-практик к коду. Подробная рамка — Предисловие → Ментальная модель и Гл. 17: Security и Governance.

Теория простыми словами

Что такое PII?

PII (Personally Identifiable Information) — это данные, которые позволяют идентифицировать человека: email, телефон, адрес, паспорт.

Что такое redaction?

Redaction — это удаление чувствительных данных из логов перед сохранением.

Как это работает (пошагово)

Шаг 1: Обнаружение и маскирование PII — на границе, не на входе модели

Вход модели НЕ маскируем. Если пользователь пишет «Мой email john@example.com, создай тикет» — агенту нужен этот email, чтобы тикет создать. sanitizePII на входе модели деградирует задачу: агент видит [EMAIL_REDACTED] и не может выполнить запрос. Маскирование применяется на границе: логи, трейсы, вывод инструментов — везде, где данные покидают процесс и попадают в хранилище, которое могут прочитать.

Сама функция маскирования выглядит так:

import "regexp"

func sanitizePII(text string) string {
    // Маскируем email
    emailRegex := regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b`)
    text = emailRegex.ReplaceAllString(text, "[EMAIL_REDACTED]")

    // Маскируем телефон (российский и международный формат)
    phoneRegex := regexp.MustCompile(`[\+]?[78]\s?[\(-]?\d{3}[\)-]?\s?\d{3}[-]?\d{2}[-]?\d{2}`)
    text = phoneRegex.ReplaceAllString(text, "[PHONE_REDACTED]")

    return text
}

Примечание: Регулярки выше — упрощённый пример для обучения. В production используйте специализированные библиотеки: Microsoft Presidio для PII detection, truffleHog или detect-secrets для поиска секретов в коде. Они покрывают десятки форматов данных и регулярно обновляются.

Шаг 2: Защита секретов

Никогда не логируйте секреты:

func sanitizeSecrets(text string) string {
    // Удаляем паттерны типа "password: ..."
    secretRegex := regexp.MustCompile(`(?i)(password|api_key|token|secret)\s*[:=]\s*[\w-]+`)
    text = secretRegex.ReplaceAllString(text, "[SECRET_REDACTED]")

    return text
}

Шаг 3: Redaction логов

Удаляйте чувствительные данные из логов:

func logWithRedaction(runID string, data map[string]any) {
    sanitized := make(map[string]any)
    for k, v := range data {
        if str, ok := v.(string); ok {
            sanitized[k] = sanitizePII(sanitizeSecrets(str))
        } else {
            sanitized[k] = v
        }
    }

    logJSON, _ := json.Marshal(sanitized)
    log.Printf("AGENT_RUN: %s", string(logJSON))
}

Где это встраивать в нашем коде

Точка интеграции: граница логов, а не вход модели

В labs/lab05-human-interaction/main.go вход пользователя уходит в модель как есть — агенту нужны данные для задачи. Маскирование — в момент записи в лог/трейс:

// Вход модели НЕ маскируем — иначе агент потеряет email клиента
messages = append(messages, llm.Message{Role: "user", Content: rawInput})

// Маскируем на границе логов/трейсов (redacted-атрибуты — гл. 19)
logWithRedaction(runID, map[string]any{"user_input": rawInput})

Границы редакции: сводка

Граница Что делаем
Вход модели Не маскируем — маскирование входа деградирует задачу
Логи Redaction по умолчанию: санитизация перед записью (Шаг 3)
Трейсы Redacted-атрибуты + хэш для корреляции; реализация в спанах — гл. 19
Вывод инструментов Маскирование по умолчанию: инструмент не должен возвращать PII/секреты в открытом виде (гл. 16, гл. 17)
Хранение TTL/retention для логов и трейсов (Ошибка 3 ниже; гл. 19 пишет трейсы в то же хранилище)
Сборка Секреты — из vault/CI-секретов в момент сборки и деплоя, не из кода и не из env образа
GDPR, право на удаление Лог с correlation ID (хэш идентификатора пользователя) → все трейсы/логи пользователя находятся поиском по ID и удаляются по запросу

Пара пояснений к таблице:

  • Vault при сборке. API-ключи и пароли не живут в main.go и не запекаются в образ. CI берёт их из vault (или GitHub/GitLab secrets) и выдаёт агенту в рантайме. Тогда утечка образа ≠ утечка секретов.
  • GDPR и correlation ID. «Забыть меня» требует найти все записи о пользователе. Если каждый лог и трейс несёт user_hash = hash(userID), удаление — это один запрос к хранилищу по этому ID, а не археология по логам.

Типовые ошибки

Ошибка 1: PII попадает в логи

Симптом: Email и телефоны пользователей видны в логах.

Решение: Маскируйте PII перед логированием.

Ошибка 2: Секреты логируются

Симптом: API ключи и пароли попадают в логи.

Решение: Удаляйте секреты из логов через redaction.

Ошибка 3: Нет Data Retention Policy

Симптом: Логи растут бесконечно, дисковое пространство заканчивается. Старые логи содержат PII, но никто их не чистит.

Причина: Нет политики хранения данных. Логи и трассировки записываются без TTL и ротации.

Решение:

// ПЛОХО: логи без ограничения хранения
func writeLog(entry LogEntry) {
    file.Write(entry) // Файл растёт бесконечно
}

// ХОРОШО: TTL и ротация
type RetentionPolicy struct {
    MaxAge    time.Duration // Максимальный срок хранения
    MaxSizeMB int          // Максимальный размер в MB
}

func (p *RetentionPolicy) Cleanup(logDir string) error {
    entries, _ := os.ReadDir(logDir)
    for _, entry := range entries {
        info, _ := entry.Info()
        if time.Since(info.ModTime()) > p.MaxAge {
            os.Remove(filepath.Join(logDir, entry.Name()))
        }
    }
    return nil
}

Ошибка 4: Отсутствие шифрования в Transit

Симптом: Данные между агентом и LLM API передаются по незащищённому каналу. Man-in-the-middle атака перехватывает запросы с PII.

Причина: HTTP вместо HTTPS, отсутствие TLS-верификации, самоподписанные сертификаты без проверки.

Решение:

// ПЛОХО: HTTP без шифрования
client := &http.Client{}
resp, _ := client.Post("http://api.llm.example.com/v1/chat", ...)

// ХОРОШО: HTTPS + проверка сертификатов
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            MinVersion: tls.VersionTLS12,
        },
    },
}
resp, _ := client.Post("https://api.llm.example.com/v1/chat", ...)

Ошибка 5: PII в трассировках

Симптом: OpenTelemetry трассировки содержат пользовательские данные. Dashboards и алерты показывают email и телефоны.

Причина: Span-атрибуты и логи добавляются без фильтрации.

Решение: Это каноническое правило курса по редакции: redaction по умолчанию на границе трейсов. Реализация в OpenTelemetry-спанах (redacted-атрибуты + хэш для корреляции) — гл. 19:

// ПЛОХО: PII попадает в span-атрибуты
span.SetAttributes(
    attribute.String("user.input", userMessage), // Содержит PII!
)

// ХОРОШО: санитизация перед добавлением в трассировку
span.SetAttributes(
    attribute.String("user.input", sanitizePII(userMessage)),
    attribute.String("user.input_hash", hashForCorrelation(userMessage)),
)

Мини-упражнения

Упражнение 1: Детектор PII

Реализуйте детектор PII, который находит email и телефоны в тексте и возвращает список найденных совпадений:

type PIIMatch struct {
    Type  string // "email", "phone"
    Value string // Найденное значение
    Start int    // Позиция начала
    End   int    // Позиция конца
}

func detectPII(text string) []PIIMatch {
    // Реализуйте поиск email и телефонов
    // Поддержите форматы: user@example.com, +7-999-123-4567, 8 (999) 123-45-67
}

Ожидаемый результат:

  • Находит email-адреса в произвольном тексте
  • Находит телефоны в разных форматах (с +7, 8, скобками, дефисами)
  • Возвращает позиции совпадений для точечной замены

Упражнение 2: Middleware для Redaction логов

Создайте middleware, который автоматически маскирует PII во всех логах агента:

type RedactionMiddleware struct {
    next     slog.Handler
    patterns []RedactionPattern
}

type RedactionPattern struct {
    Name    string
    Regex   *regexp.Regexp
    Replace string
}

func NewRedactionMiddleware(next slog.Handler) *RedactionMiddleware {
    return &RedactionMiddleware{
        next: next,
        patterns: []RedactionPattern{
            {Name: "email", Regex: regexp.MustCompile(`\b[\w.+-]+@[\w.-]+\.\w{2,}\b`), Replace: "[EMAIL]"},
            {Name: "phone", Regex: regexp.MustCompile(`[\+]?[78]\s?[\(-]?\d{3}[\)-]?\s?\d{3}[-]?\d{2}[-]?\d{2}`), Replace: "[PHONE]"},
        },
    }
}

func (m *RedactionMiddleware) Handle(ctx context.Context, r slog.Record) error {
    // Реализуйте фильтрацию всех атрибутов записи
}

Ожидаемый результат:

  • Все строковые атрибуты проходят через redaction
  • Паттерны легко расширяются (добавить ИНН, паспорт, карту)
  • Middleware прозрачен для остального кода

Критерии сдачи / Чек-лист

Сдано:

  • Вход модели не маскируется (агент получает данные задачи)
  • PII маскируется на границе логов/трейсов (redaction по умолчанию)
  • Вывод инструментов маскируется по умолчанию
  • Секреты не логируются и выдаются из vault при сборке, не из кода
  • Логи и трейсы хранятся с TTL/retention
  • Есть correlation ID для GDPR-права на удаление

Не сдано:

  • PII попадает в логи/трейсы в открытом виде
  • Вход модели маскируется (агент теряет данные задачи)
  • Секреты логируются или запечены в образ

Связь с другими главами

Что дальше?

После понимания Data и Privacy переходите к: