Skip to content

Deploy

Как worker поднимается и загружается. Worker — это руки агента: песочница по запросу (bash · fs · браузер), реализованная на Kubernetes как короткоживущий Job, а не постоянный Deployment. Ничего «всегда-онлайн» на каждого агента нет.

Это шаг 1 жизненного цикла → дальше Use → затем Destroy.

Кто его создаёт

Приложение worker не планирует себя само. Менеджер рантайма внутри api (слайс runtime/worker) владеет k8s-клиентом и жизненным циклом сессии:

orchestrator → tasks (BullMQ) → runtime manager → k8s Job (worker pod)
                                     │ mints a short-lived session token (user/auth)
                                     │ builds the manifest from a RuntimeProfile

                             worker pod boots → WS to Core → ready → runs → uploads → exits
                                     │ heartbeats reset the idle timer

                             idle reaper deletes the Job (see Destroy)

В очередь ставится только «агенту нужны руки»

Обычный чат-турн без тулов никогда не задействует очередь (слайс runtime/task). В очередь ставятся только worker/tool-задачи:

message → orchestrator (load agent + memory + chat, tool-analysis)
   ├─ no tools  → llm.stream() answers directly → save → done      ← NO queue
   └─ tools     → tasks (queue) → dispatcher → worker Job → stream  ← queue

Очередь оправдывает себя ради backpressure + таймеров + надёжности, а не ради сырой пропускной способности:

НазначениеЧто делаетЧто ломается без неё
Backpressureдозирует создание Job под ёмкость нодвсплеск из N задач → N Pending-подов → давление на scheduler/etcd
Конкурентность / справедливостьper-team-лимиты, глобальные потолки, премиум-приоритетодин тенант голодает всех остальных
Надёжностьretry-with-backoff + dead-letterпотерянная задача просто исчезает
Таймерыdelayed-джобы = idle-reaper; repeatable = agent/cronнет чистого idle-teardown'а или запусков по расписанию
Decouplingзадача переживает то, какая stateless-реплика api её поставила в очередьrescale HPA роняет работу в полёте

Консьюмер — это dispatcher

Worker'ы эфемерны — один k8s Job на задачу — поэтому консьюмер очереди — это не пул worker'ов. Это dispatcher (менеджер runtime/worker в api): он берёт задачу, применяет лимиты конкурентности, выбирает RuntimeProfile и создаёт Job. Worker порождается на задачу и стримит результаты назад через events (Redis pub/sub → SSE/WS); сам он никогда не является консьюмером очереди.

Тип JobМеханизмДля чего
Task dispatchstandard jobвыполнить обязательство-тул в worker Job
Idle timersdelayed jobreaper усыпляет/сносит idle-сессию (Destroy)
Scheduled / cronrepeatable jobзапуски agent/cron → тот же путь orchestrator
Ingestionstandard job (budgeted)индексация документов LightRAG

Engine: BullMQ на Redis (даёт delayed- и repeatable-джобы и pub/sub для events). Открытая альтернатива — только-Postgres pg-boss + LISTEN/NOTIFY — держится за слайсом runtime/task, так что engine заменяем. См. Решения по очереди задач.

Режимы рантайма → k8s-примитивы

Форма ресурсов берётся из RuntimeProfile (каталог пресетов):

РежимТриггерk8s-примитивРесурсыIdle
Noneобычный чат, без туловбез пода (только LLM + память)n/a
Lightbash, скрипты, файловые операцииJob (restartPolicy: Never, ttlSecondsAfterFinished)~0.5 CPU / 512Mi60–120с
Browserиспользован браузер-тулJob с headless Chromium in-pod (Playwright, per-tenant --user-data-dir)на задачудо простоя браузера
Heavyскрейпинг, тяжёлые вычисленияJob на нодпуле workers, высокие лимиты2+ CPU / 2Gi+30–60с
WarmпремиумLight/Heavy Job живёт дольше idleпо профилю10–30 мин

Манифест Job (иллюстративно)

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: agent-{sessionId}
  namespace: tenant-{teamId}            # namespace (or labels) per tenant
  labels: { app: agentfy-worker, team: "{teamId}", session: "{sessionId}" }
spec:
  backoffLimit: 0                       # no retries — a failed obligation fails the task
  ttlSecondsAfterFinished: 60           # k8s auto-deletes the finished Job
  activeDeadlineSeconds: 1020           # dialSeconds + maxExecSeconds — НЕ один maxExecSeconds:
                                        # k8s считает это от старта JOB, то есть включая холодный
                                        # старт. См. /ru/worker/protocol#deadlines
  template:
    spec:
      restartPolicy: Never
      automountServiceAccountToken: false
      nodeSelector: { node-role: workers }
      tolerations: [{ key: node-role, value: workers, effect: NoSchedule }]
      # `fsGroup` is what makes the 0440 pass file readable by the image's
      # `USER 1000:1000`: a Secret volume's files are owned by root.
      securityContext: { runAsNonRoot: true, fsGroup: 1000, seccompProfile: { type: RuntimeDefault } }
      containers:
        - name: worker
          image: registry/agentfy-worker:{tag}
          resources:
            requests: { cpu: "500m", memory: "512Mi" }
            limits:   { cpu: "1", memory: "1Gi", ephemeral-storage: "5Gi" }
          env:
            - { name: SESSION_ID,  value: "{sessionId}" }
            - { name: CONTROL_URL, value: "wss://core/ws/runtime" }
            - { name: TOOL_ALLOWLIST, value: "bash,fs,browser" }
            # Where `http` / `web_fetch` may go. Hosts, or `*.host` — never a URL and
            # never `*`. ABSENT MEANS EMPTY, and empty reaches nowhere.
            - { name: EGRESS_ALLOWLIST, value: "example.com,*.example.com" }
            # Где `browser_play` может ВПИСЫВАТЬ и НАЖИМАТЬ (AGNT2-221). ВТОРОЙ
            # список, а не прочтение предыдущего: разрешение читать сайт не есть
            # разрешение на нём нажимать. Только точные имена хостов — ни `*.`,
            # ни `internet:unrestricted`. ОТСУТСТВУЕТ — ЗНАЧИТ ПУСТО, а пусто не
            # трогает ничего.
            - { name: BROWSER_PLAY_ALLOWLIST, value: "shop.example.com" }
            # The pass arrives as a FILE, never as a value in the environment
            # (006 T104). This names the PATH; the Secret behind it is owned by
            # this Job, so the cluster reaps it when the Job goes.
            - { name: WORKER_TOKEN_FILE, value: /var/run/agentfy/token }
          volumeMounts:
            - { name: workspace, mountPath: /workspace }
            - { name: session-pass, mountPath: /var/run/agentfy, readOnly: true }
          securityContext:
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities: { drop: ["ALL"] }
      volumes:
        - name: workspace
          emptyDir: { sizeLimit: "5Gi" }                 # ephemeral; wiped when the pod dies
        - name: session-pass
          projected:
            defaultMode: 0440                            # root:1000 — readable by the pod's user
            sources:
              - secret:
                  name: worker-{jobName}-pass
                  items: [{ key: token, path: token }]

Boot & регистрация

Под стартует с SESSION_ID, CONTROL_URL, короткоживущим session-токеном (JWT, scoped на эту одну AgentRuntimeSession, инжектится как projected/short-TTL Secret — никаких долгоживущих секретов в поде), allowlist'ом тулов и скоупом хранилища. Затем он дозванивается до Core по WebSocket и сигнализирует ready. Полный handshake и протокол сообщений живут в Use; сам контракт провода — каждый кадр, его ответ, его таймаут и правило версий — это Канал инструментов.

Масштабирование

  • Scale-to-zero встроен: Job существует только пока идёт задача, потом удаляется. KEDA/Knative для MVP не нужны.
  • Конкурентность ограничена нодпулом workers + per-tenant ResourceQuota/LimitRange; cluster-autoscaler растит нодпул под нагрузкой.
  • Worker несёт Chromium → холодный старт браузера = спин-ап Job'а + запуск Chrome. Пре-пулленные образы worker'а (и пара тёплых нод workers / Warm-профиль) прячут эту задержку.

Чеклист безопасности

  • Неймспейс (или NetworkPolicy + labels) на тенанта/сессию.
  • NetworkPolicy egress-allowlist (по умолчанию deny-all; разрешить Core WS + нужные домены).
  • Лимиты CPU/RAM/storage из профиля; read-only root fs, drop all capabilities, non-root, automountServiceAccountToken: false.
  • Только временный emptyDir-workspace; никаких постоянных секретов в образе; креды короткоживущие, истекают вместе с сессией.
  • activeDeadlineSeconds — жёсткий потолок на runaway-рантаймы, размером dialSeconds + maxExecSeconds, потому что k8s начинает считать от Job, а не от процесса (канал инструментов); аудит-лог каждой сессии через admin/audit.

Где живёт код

  • Менеджер (control): api → слайс runtime/workersessions · k8s (сборка манифеста + create/delete) · browser (жизненный цикл Chromium in-pod) · idle (reaper) · profiles.
  • Очередь: api → слайс runtime/task (BullMQ).
  • Песочница (образ): верхнеуровневое приложение worker — цикл агента + bash/fs/browser исполнители + WS-клиент. Тул-исполнители берём из cleanslice/runtime, паттерны k8s/манифеста — из Ranch.
  • Кластер: Hetzner k8s, нодпул workers (tainted). См. Resources.

См. также

  • Use — как мозг управляет живым worker'ом (сессии, протокол, тулы).
  • Канал инструментов — контракт провода: кадры, ответы, молчание, версии.
  • Destroy — idle-reaper, release, TTL и очистка.
  • Implementation — build-промпт для этой подсистемы.
  • Cluster & nodes — backpressure + autoscaler, который кормит очередь.