Files
sless/doc/architecture/agent-handoff-2026-03-10.md
“Naeel” 7d6f8d6079 docs: добавлен анализ GPT-5.4 и Opus 4.6
- agent-handoff-2026-03-10.md — GPT-5.4 code review (lifecycle issues, invocation history gap)
- opus-pragmatic-review-2026-03-10.md — Opus прагматичный review для небольшого провайдера
- Opus: gVisor/LLM validation — overkill для MVP, фокус на быстрые фиксы + ResourceQuota/NetworkPolicy
- Обновлён progress.md с новыми документами
- .gitignore — добавлен test.token
2026-03-10 08:56:59 +04:00

28 KiB
Raw Permalink Blame History

Подробный handoff для следующего агента

Последнее обновление: 2026-03-10

Назначение документа

Этот документ нужен не для презентации проекта, а для практической передачи контекста следующему агенту. Цель: после чтения файла должно быть понятно:

  1. Что в проекте уже сделано и действительно работает.
  2. Где архитектура сильная.
  3. Где есть реальные дефекты, а где просто незавершённые v2-задачи.
  4. Что нужно делать следующим шагом и в каком порядке.
  5. Как проверять изменения с учётом ограничений среды (VPN, возможные TLS timeout).

Документ основан на анализе текущего кода, существующей документации и структуры проекта. Сетевые E2E-проверки намеренно не делались в рамках этого разбора, потому что среда может шуметь из-за VPN и TLS timeout.


Краткий вывод

Проект находится в хорошем состоянии для рабочего MVP managed serverless platform, но ещё не находится в состоянии production-grade managed cloud service.

Что важно понимать сразу:

  1. Основа выбрана правильно: Kubernetes operator + CRD + build pipeline через S3 + kaniko.
  2. Пользовательский путь уже частично доказан существующими Terraform examples и E2E заметками.
  3. Главные проблемы сейчас не в том, что код "вообще не работает", а в том, что системные свойства платформы пока неполные: event lifecycle, observability, auth/tenancy, строгая валидация контрактов, тестовое покрытие.

Итоговая инженерная оценка:

  1. Как технический фундамент для v1: хорошо.
  2. Как demo/MVP для внутренней обкатки: хорошо.
  3. Как настоящий managed cloud service: пока рано, есть несколько архитектурно значимых дыр.

Что уже реализовано и выглядит здраво

1. Правильный control plane фундамент

Текущая модель сервиса строится через CRD и operator-подход:

  1. Function описывает функцию и её runtime/config.
  2. Trigger описывает способ вызова.
  3. FunctionJob описывает одноразовый запуск.

Ключевые файлы:

  1. api/v1alpha1/function_types.go
  2. api/v1alpha1/trigger_types.go
  3. api/v1alpha1/job_types.go
  4. controllers/function_controller.go
  5. controllers/trigger_controller.go
  6. controllers/functionjob_controller.go

Это хорошее решение для системы, которая управляет lifecycle k8s-ресурсов, потому что:

  1. Desired state вынесен в CRD.
  2. Reconcile loop естественно ложится на build/deploy/cleanup.
  3. Kubernetes остаётся источником правды по текущему фактическому состоянию ресурсов.

2. Build pipeline собран прагматично и без лишней магии

Фактически реализована понятная цепочка:

  1. API принимает zip.
  2. Upload handler генерирует Dockerfile и tar.gz контекст.
  3. Контекст кладётся в S3.
  4. Function controller запускает kaniko Job.
  5. После успешной сборки создаётся/обновляется Deployment функции.

Ключевые файлы:

  1. internal/api/handler/upload.go
  2. internal/builder/builder.go
  3. controllers/function_controller.go

Сильные стороны текущего решения:

  1. Пользователь не думает про Dockerfile.
  2. Сборка отделена от API и не делается внутри процесса оператора.
  3. Используется idempotency guard через аннотацию last-built-s3key.
  4. Используются version-like image refs через hash от S3 key, а не один общий latest для результата сборки.

3. API и provider уже образуют реальный пользовательский путь

Есть не только внутренние CRD, но и внешний контракт:

  1. REST API на gorilla/mux.
  2. Terraform provider в отдельном модуле.
  3. Набор examples, которые уже реально гонялись.

Ключевые файлы:

  1. internal/api/router.go
  2. internal/api/handler/functions.go
  3. internal/api/handler/triggers.go
  4. internal/api/handler/jobs.go
  5. terraform/provider/...
  6. examples/...

Это важно: проект уже живёт не только как "внутренний оператор", а как зачаток полноценного managed продукта.

4. Видно, что проект уже проходил через реальные эксплуатационные проблемы

Это видно по документам:

  1. doc/errors/log.md
  2. doc/decisions/log.md
  3. doc/progress.md

Плюс в том, что проект не застрял на happy path. Уже обнаружены и исправлялись:

  1. бесконечные build loops,
  2. cleanup после destroy,
  3. cross-namespace проблемы с owner references,
  4. image pull / registry проблемы,
  5. несогласованность Terraform state.

Это хороший признак инженерной зрелости даже при сырой архитектуре.


Текущая фактическая архитектура

1. Один бинарник совмещает operator manager и REST API

Файл: main.go

Что происходит:

  1. Загружается env-конфиг.
  2. Поднимается PostgreSQL store.
  3. Выполняются миграции.
  4. Поднимается S3 client.
  5. Создаётся controller-runtime manager.
  6. Регистрируются Function, Trigger и FunctionJob reconcilers.
  7. Параллельно запускается HTTP API.

Это нормальное решение для раннего v1. Оно упрощает деплой и снижает количество moving parts.

Ограничение: по мере роста системы API и control plane логика будут мешать друг другу по масштабированию, отказам и ответственности. Но для текущей стадии это приемлемо.

2. Данные хранятся в двух разных источниках правды

Реально сейчас:

  1. Состояние lifecycle функции и триггеров живёт в Kubernetes CRD/status.
  2. Invocation history задумана в PostgreSQL.
  3. Исходный код и build context живут в S3.
  4. Docker image живёт во внешнем registry.

Это нормальная модель, но только если границы между источниками правды строго определены. Сейчас эта модель задумана верно, но реализована не до конца, особенно вокруг invocation history.

3. HTTP вызов функции сейчас устроен через публичный прокси /fn/

Ключевые файлы:

  1. internal/api/router.go
  2. internal/api/handler/invoke.go
  3. controllers/trigger_controller.go

Схема:

  1. Trigger типа http формирует URL вида /fn/{namespace}/{name} через ExternalURL.
  2. API-сервер принимает запрос.
  3. InvokeFunction проксирует его во внутренний Service функции.

Для текущего окружения это разумный обход ограничения wildcard DNS.

Минус: это превращает API-процесс в data plane proxy для пользовательского трафика. Для MVP годится, для production это создаст узкое место и дополнительные требования к auth, rate limiting, tracing и HA.


Подтверждённые проблемы и архитектурные долги

Ниже перечислены именно подтверждённые проблемы по текущему коду, а не абстрактные придирки.

Критичность A — нужно чинить в ближайших итерациях

A1. TriggerReconciler рассчитывает на события Function, но реально не подписан на них

Файл: controllers/trigger_controller.go

Проблема:

  1. В Reconcile есть ветка: если Function ещё не Ready, Trigger пишет статус waiting и выходит.
  2. Комментарий говорит, что повторный reconcile придёт, когда Function изменится.
  3. Но SetupWithManager регистрирует только For(&Trigger{}), без watch на Function.

Следствие:

  1. Trigger, созданный раньше готовности Function, может застрять в промежуточном состоянии.
  2. Пересчёт будет зависеть не от правильного события, а от случайного следующего изменения Trigger.

Почему это важно:

Для operator-системы это уже логический дефект event model, а не просто TODO.

Что делать:

  1. Добавить watch на Function.
  2. Обеспечить маппинг Function -> Trigger по namespace + FunctionRef.
  3. Покрыть тестом сценарий "Trigger создан до готовности Function".

A2. FunctionJobReconciler имеет ту же проблему ожидания готовности Function

Файл: controllers/functionjob_controller.go

Проблема:

  1. Если Function не Ready, FunctionJob уходит в Pending и выходит.
  2. В комментарии подразумевается, что reconcile придёт позже.
  3. Но SetupWithManager также подписан только на FunctionJob.

Следствие:

  1. Job может зависнуть в Pending без события-пробуждения.
  2. Фактическое выполнение зависит от внешнего изменения или ручного повторного reconcile.

Что делать:

  1. Добавить watch на Function.
  2. Либо ввести RequeueAfter polling для ожидания Function Ready.
  3. Предпочтительнее watch, если можно аккуратно сматчить зависимости.

A3. Invocation history заявлена, но фактически не пишется

Файлы:

  1. migrations/001_initial.sql
  2. internal/storage/postgres/store.go
  3. internal/api/handler/invocations.go
  4. internal/api/handler/invoke.go

Подтверждённый факт:

  1. Есть таблица invocations.
  2. Есть SaveInvocation и ListInvocations.
  3. Есть API-эндпоинт чтения истории.
  4. В текущем коде нет места, где SaveInvocation реально вызывается.

Следствие:

  1. API истории вызовов обещает поведение, которого фактически нет.
  2. Документация и UX вводят в заблуждение: кажется, что история есть, но она пустая не потому, что вызовов не было, а потому что они не сохраняются.

Что делать:

  1. Встроить запись инвокации в HTTP proxy path и, отдельно, в FunctionJob execution path.
  2. Определить, что именно считается logs/result/status/duration.
  3. Синхронизировать это решение с API design и examples.

A4. Multi-tenant модель ещё не реализована, а API уже строится вокруг namespace из URL

Файлы:

  1. internal/api/router.go
  2. internal/api/middleware/auth.go
  3. doc/progress.md
  4. doc/api/design.md

Проблема:

  1. Namespace передаётся пользователем в URL.
  2. Auth пока основан на одном статическом токене.
  3. В документации прямо написано, что настоящая namespace isolation по токену ещё не сделана.

Следствие:

  1. Контракт API пока demo-only.
  2. При переходе к реальному tenant-aware auth почти наверняка придётся менять или сильно прятать namespace от клиента.

Что делать:

  1. Зафиксировать как архитектурное решение: namespace выводится из identity, а не приходит от клиента.
  2. Добавить whoami/identity resolution слой.
  3. Планировать миграцию API аккуратно, пока пользователей мало.

Критичность B — не ломает demo, но делает платформу хрупкой

B1. Важная часть конфигурации декларативна только на бумаге, но не используется последовательно

Файлы:

  1. internal/config/config.go
  2. main.go
  3. controllers/function_controller.go
  4. controllers/trigger_controller.go
  5. controllers/functionjob_controller.go

Проблема:

  1. Есть FunctionNamespacePrefix в конфиге.
  2. Но контроллеры и wiring жёстко используют sless и sless-fn-.

Следствие:

  1. Конфиг частично ложный: кажется, что поведение настраивается, но реально нет.
  2. Перенос и переиспользование сервиса усложняются.

Что делать:

  1. Либо реально протащить конфиг до всех мест использования.
  2. Либо убрать мнимо-настраиваемое поле, если в v1 это сознательный hardcode.

B2. TimeoutSec описан в модели, но по сути не обеспечивается runtime-слоем

Файлы:

  1. api/v1alpha1/function_types.go
  2. internal/api/handler/functions.go
  3. runtimes/python3.11/server.py
  4. runtimes/nodejs20/server.js

Проблема:

  1. Поле timeout есть в API/CRD.
  2. Но runtime-обёртка не ограничивает время выполнения функции.
  3. HTTP proxy timeout клиента не равен execution timeout функции как платформенной гарантии.

Следствие:

  1. Контракт платформы частично ложный.
  2. Длительный обработчик может вести себя непредсказуемо с точки зрения пользователя.

Что делать:

  1. Либо честно задокументировать, что timeout пока informational.
  2. Либо реализовать enforcement через runtime/process/k8s job semantics.

B3. Публичный /fn/ endpoint не отделён от control plane auth модели

Файлы:

  1. internal/api/router.go
  2. internal/api/middleware/auth.go
  3. internal/api/handler/invoke.go

Проблема:

  1. /v1 защищён статическим token.
  2. /fn/ публичен.
  3. Никакой tenant-aware authz на invoke path сейчас нет.

Для MVP это допустимо, но следующий агент должен понимать:

  1. Это не production-ready security model.
  2. Любая работа по auth должна учитывать отдельно control plane и data plane.

B4. Тестов ядра почти нет

Файлы:

  1. controllers/suite_test.go
  2. остальная кодовая база tests почти отсутствуют

Фактическое состояние:

  1. Есть bootstrap envtest.
  2. Нет содержательных тестов на reconcile lifecycle, cleanup, event propagation, API handler behaviour.

Следствие:

  1. Реальная надёжность сейчас в основном держится на ручных E2E и Terraform examples.
  2. Регрессии в controller logic будут ловиться поздно.

Критичность C — скорее незавершённость и техдолг, чем немедленная поломка

C1. Статусная модель богаче реальной логики

Файлы:

  1. api/v1alpha1/function_types.go
  2. api/v1alpha1/trigger_types.go

Замечание:

  1. Conditions объявлены, но не ведутся как системная модель статусов.
  2. LastScheduleTime есть, но не видно логики обновления.
  3. PreWarmSeconds есть, но отмечен как нереализованный.

Это не срочный баг, но следующий агент не должен тратить время, думая что вся эта модель уже рабочая.

C2. Cleanup build contexts и registry artefacts не выглядит завершённым

Замечание:

  1. При удалении Function чистятся Deployment/Service/Ingress.
  2. Не видно системного cleanup для S3 contexts и образов registry.

Для разработки допустимо. Для managed среды это приведёт к накоплению мусора.


Что не является проблемой само по себе

Следующий агент не должен тратить время на преждевременное "улучшательство".

1. Один бинарник для API и operator — нормально для текущей стадии

Не надо сейчас автоматически распиливать на микросервисы. Это не даст пропорциональной пользы на текущем этапе.

2. Выбор gorilla/mux не является узким местом проекта

Главные риски проекта не в роутере и не в HTTP фреймворке.

3. Простые runtime wrappers — нормальны для v1

Python/Node runtime обёртки в текущем виде упрощают систему. Их ограничения понятны, но они не являются главной ближайшей проблемой по сравнению с event model и invocation persistence.


Приоритетный план работ для следующего агента

Ниже порядок работ, который имеет смысл соблюдать.

Этап 1. Починить lifecycle зависимости Trigger и FunctionJob от Function

Цель:

  1. Trigger и Job не должны зависать, если были созданы до готовности Function.

Задачи:

  1. Добавить watch/mapping из Function в связанные Trigger.
  2. Добавить watch/mapping из Function в связанные FunctionJob или явный polling для ожидания Ready.
  3. Убедиться, что reconcile повторяется по правильным событиям, а не случайно.

Файлы-кандидаты:

  1. controllers/trigger_controller.go
  2. controllers/functionjob_controller.go

Критерии готовности:

  1. Trigger, созданный раньше Function Ready, автоматически становится Active после готовности функции.
  2. FunctionJob, созданный раньше Function Ready, сам продолжает lifecycle без ручного тычка.
  3. Поведение покрыто тестами или хотя бы воспроизводимым локальным сценарием.

Этап 2. Реально включить запись invocation history

Цель:

  1. Сделать так, чтобы API истории вызовов соответствовал реальному поведению платформы.

Задачи:

  1. На HTTP invoke path записывать status, duration, http_status и response/error.
  2. На FunctionJob path записывать результат и статус отдельно как job-triggered invocation либо честно развести две сущности.
  3. Обновить doc/api/design.md и doc/progress.md по факту.

Файлы-кандидаты:

  1. internal/api/handler/invoke.go
  2. internal/storage/postgres/store.go
  3. controllers/functionjob_controller.go
  4. internal/api/handler/invocations.go

Критерии готовности:

  1. После успешного и неуспешного вызова в PostgreSQL появляется запись.
  2. GET invocations возвращает не пустой декоративный список, а реальные записи.

Этап 3. Синхронизировать контракт timeout/status/validation

Цель:

  1. Убрать ложные обещания платформы и несогласованность API.

Задачи:

  1. Решить, что означает TimeoutSec прямо сейчас.
  2. Если не реализуется быстро, явно задокументировать ограничение.
  3. Выправить Create/Update в functions.go и triggers.go так, чтобы значения по умолчанию и validation были последовательными.

Файлы-кандидаты:

  1. internal/api/handler/functions.go
  2. internal/api/handler/triggers.go
  3. api/v1alpha1/function_types.go
  4. doc/api/design.md

Критерии готовности:

  1. API не заставляет клиента задавать поля, которые CRD должен дефолтить.
  2. Update не может случайно испортить spec нулевыми значениями.
  3. Документация не обещает больше, чем реально реализовано.

Этап 4. Уменьшить ложную конфигурируемость

Цель:

  1. Либо сделать конфиг реальным, либо убрать фиктивную настраиваемость.

Задачи:

  1. Разобраться с FunctionNamespacePrefix и хардкодами sless/sless-fn-.
  2. Принять одно из двух решений: а) протащить конфиг через контроллеры и main; б) удалить поле до тех пор, пока оно реально не нужно.

Файлы-кандидаты:

  1. internal/config/config.go
  2. main.go
  3. controllers/function_controller.go
  4. controllers/trigger_controller.go
  5. controllers/functionjob_controller.go

Этап 5. Добавить минимально полезные автоматические тесты

Цель:

  1. Снизить зависимость от ручных прогонов.

Минимальный полезный набор:

  1. Trigger ждёт Function Ready и потом активируется.
  2. Function deletion чистит дочерние ресурсы.
  3. FunctionJob не зависает в Pending/Running навсегда.
  4. Invoke path корректно пишет invocation history.

Важно:

Не надо начинать с больших e2e по сети. Нужны локальные, детерминированные, дешёвые проверки.


Как проверять изменения в этой среде

С учётом замечания пользователя про VPN и возможные TLS timeout, стратегия проверки должна быть осторожной.

Что предпочитать

  1. Локальные Go-тесты.
  2. envtest/контроллерные тесты без внешней сети.
  3. Анализ кода и существующих E2E результатов в examples/ и doc/progress.md.

Что не делать первым шагом

  1. Не начинать с внешних curl/HTTPS smoke test.
  2. Не считать TLS timeout автоматическим подтверждением бага в сервисе.
  3. Не делать выводы по сетевому шуму без локального подтверждения.

Практический порядок проверки

  1. Сначала локальные unit/envtest проверки.
  2. Потом, если нужно, ограниченные сценарии через provider/examples.
  3. Только в самом конце сетевые end-to-end через ingress.

Быстрые ориентиры по файлам

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

  1. main.go
  2. api/v1alpha1/function_types.go
  3. api/v1alpha1/trigger_types.go
  4. api/v1alpha1/job_types.go
  5. controllers/function_controller.go
  6. controllers/trigger_controller.go
  7. controllers/functionjob_controller.go
  8. internal/api/router.go
  9. internal/api/handler/upload.go
  10. internal/api/handler/invoke.go
  11. internal/storage/postgres/store.go
  12. doc/progress.md
  13. doc/errors/log.md
  14. doc/decisions/log.md

Короткий practical summary для следующего агента

Если нужно запомнить только главное:

  1. Не рефакторить всё подряд. Основа проекта нормальная.
  2. Первые реальные проблемы: event model Trigger/FunctionJob и отсутствие записи invocation history.
  3. Не путать v2-идеи с текущими дефектами: не всё незавершённое надо чинить прямо сейчас.
  4. Не полагаться в первую очередь на внешнюю сеть и TLS smoke test: среда может шуметь.
  5. Любые значимые изменения обязательно фиксировать в doc/progress.md и, при необходимости, в doc/errors/log.md или doc/decisions/log.md.

Рекомендуемое следующее действие

Самый рациональный следующий шаг для нового агента:

  1. Сфокусироваться на TriggerReconciler и FunctionJobReconciler.
  2. Починить повторное пробуждение от изменений Function.
  3. Добавить под это минимальные локальные тесты.

Это даст максимальный выигрыш в надёжности control plane без лишнего расширения функциональности.