Skip to content

Architecture refactor #8

Closed
Closed

Description

@laurivosandi

Run controller suite per cluster in its own namespace

  • Branch out scalable webhook handler into Go/Rust binary, include best effort key routing (email thread ID, GitHub server-repo-issue/PR, Gitea server-repo-issue/PR etc)
  • Branch out IMAP crawler into separate deployment with leader election (?), have MCP endpoint on that deployment for reading mail for the agents. Possibly combine with SMTP outbound mail so sent mail gets recorded in the IMAP sent outbox. Possibly generalize as mailbox service for agents, so IMAP and SMTP do not have to be usable simultanously. IMAP long poll translates incoming mails to notifications just like webhook handler does attempting to normalize to same key format.
  • Branch out web UI and make it optional. User can see/modify only Agents whose CR RBAC permits it.
  • Adopt RabbitMQ, EMQX or similar for webhook submission, persistence and deduplication
  • Controller runs Deployment which consumes events from RabbitMQ, evaluates agent state and figures out what to do next (add Task, remove Task, abort Task) and when worker pod is up and running submits prompts to it from message queue
  • Multiple keys (issue, email and corresponding PR) might refer to same Task at hand, maintain sensible mapping

Declarative Agent instances via Kube native CRD-s:

  • Agent represents privilege domain, which MCP-s are enabled and which credentials are accessible
  • Agent state is tracked in Agent CRD .status field
  • Task CRD tracks Worker state, what model is used, should worker be running or off etc
  • Each agent shall have a Kubernetes namespace for it's workers
  • Agent CRD namespaced declaration lives in the workers namespace. You can have multiple Agents spawn its workers in same namespace if security domain aligns
  • Worker pod has it's ownerReferences pointint to Task CR, thus deleting Task CR deletes all pods assocated with it
  • Task CRD has ownerReferences pointing to the Agent CR, thus deleting Agent deletes all Tasks associated with it
  • Workspace PVC it's ownerReference pointing to the Task CR. Deleting Task CR deletes the workspace PVC associated with it

Open ended questions:

  • How are secrets handled? In web UI I should be able to set up GH, SMTP, IMAP tokens, ideally mutating existing secret? In the workers namespace? Those should be easily configurable with kubectl as well.

Come up with admin vs developer vs customer support Kube RBAC and Agent CR scheme which would satisfy the requirements described in the README. Factor in one more scenario where Agent is for personal mailbox, reads mail and populates calendar.

Make sure the setup covers multiple Giteas, multiple Githubs (GitHub enterprise), multiple Agents CR-s per cluster, multiple Agents CR-s per namespace. Orchestration logic keep in one namespace

Don't execute before confirmation, start by commenting how the Agent, Task CR-s would look like and how the RBAC would be organized without dumping RBAC YAML

Activity

  1. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Architecture Design: CRDs, RBAC, and Secrets

    Here's the proposed design before we write any code or YAML.


    CRD hierarchy and ownership chain

    Agent CR (namespace-scoped, in workers namespace)
      └── Task CR (same namespace, ownerRef → Agent)
            ├── Worker Pod (ownerRef → Task)
            └── Workspace PVC (ownerRef → Task)
    

    Kubernetes garbage collection handles cleanup cascades automatically through ownerReferences. Deleting an Agent deletes all its Tasks; deleting a Task deletes its Pod(s) and PVC.


    Agent CR design

    Agent represents a privilege domain: which credentials are mounted, which MCPs are enabled, which Git providers are accessible, and what execution namespace the workers run in.

    Key spec fields:

    • credentialRefs — list of Secret names (in the same namespace) the agent can mount into workers. Secrets are referenced, not embedded. Admins create/update them with kubectl; the UI can patch them via a controller-mediated API that requires the same RBAC the admin already has.
    • mcpEndpoints — list of MCP server URLs and optional tokenFile refs. Workers get only what's listed here.
    • gitProviders — typed list: {type: github|gitea, serverURL, credentialSecretRef}. Supports multiple GitHub Enterprise instances and multiple Giteas simultaneously.
    • mailConfig — optional IMAP/SMTP refs for agents that handle mailboxes (e.g. calendar agent).
    • workerImage and model — frozen per Agent, not per Task.
    • workerNamespace — the namespace where this Agent's workers run. Defaults to .metadata.namespace. Multiple Agents can share a namespace if their security domains align.

    Status tracks: phase (Ready/Degraded), activeTaskCount, lastReconciled, credential/MCP readiness per item.


    Task CR design

    Task tracks a single unit of work. It is the anchor for external references (issue URL, PR URL, mail thread IDs).

    Key spec fields:

    • agentRef — name of the Agent CR in the same namespace.
    • objective — prompt/spec text (or a ConfigMap ref for large specs).
    • desiredState — Active | Paused | Cancelled.
    • model — overrides Agent default if set.
    • externalReferences — typed list: {source: github|gitea|mail, ...identifiers}. Multiple refs per Task are fine.
    • completionPolicy — optional: {milestone: PullRequestCreated|VerifiedChecks|Approved|Merged, requiredChecks: [...]}.

    Status tracks: phase (Pending/Running/Waiting/Succeeded/Failed/Cancelled), reason, workerPodName, sessionEpoch, progress.summary, outputs.pullRequestURL, conditions.

    The Task controller (Deployment in the orchestration namespace) watches Task CRs cluster-wide (or namespace-scoped via RBAC), reconciles desired ↔ actual state, and submits prompts via RabbitMQ to the live worker.


    Orchestration namespace vs workers namespace

    One orchestration namespace (agent-system or similar) runs:

    • The controller Deployment (consumes RabbitMQ, reconciles CRs)
    • The webhook handler (Go binary, posts to RabbitMQ)
    • The web UI (optional Deployment, can be omitted)
    • RabbitMQ (or compatible)
    • Redis/Dragonfly (Task state and deduplication)

    Per-security-domain worker namespaces (e.g. dev-agents, support-agents, personal-agents) hold:

    • Agent CRs
    • Worker Pods (ephemeral, ownerRef → Task)
    • Workspace PVCs (ownerRef → Task)
    • Credential Secrets

    The Task CRs can live in either the orchestration namespace (simpler RBAC) or the workers namespace (closer to ownerRef cascade). I lean toward workers namespace so the ownerRef chain stays within one namespace and Kubernetes GC works without cross-namespace complications.


    Secrets handling

    Secrets stay in the workers namespace, created/updated with kubectl by admins. The controller API can expose a patch endpoint for the UI that proxies the mutation — the UI never holds the secret value, only submits the new token. This keeps the UI stateless with respect to credentials.

    For the web UI secret management flow:

    1. UI calls controller API PATCH /agents/{name}/credentials/{key}
    2. Controller (with its ServiceAccount RBAC) patches the Secret in the workers namespace
    3. Worker pods pick up the updated Secret on next mount (restart or new pod)

    This means the UI user needs no direct Secret RBAC — they delegate through the controller API, which is gated by OIDC authentication and group membership.


    RBAC roles (conceptual, no YAML yet)

    Cluster-admin / platform operator

    • Manages the orchestration namespace
    • Creates/deletes worker namespaces
    • Installs CRDs, controller Deployment, RabbitMQ
    • Full RBAC over Agent and Task CRs

    Agent admin (per security domain)

    • CRUD on Agent CRs in their namespace(s)
    • CRUD on Secrets in their namespace(s) (credential management)
    • Read Task CRs in their namespace(s)
    • Cannot touch other namespaces

    Developer agent (the dev-agents namespace scenario from README)

    • Agent has: GitHub token, model key, Driftmower MCP, Skaffold/Kaniko access
    • Workers get namespace-scoped RBAC to apply manifests in their own namespace only
    • No cluster-wide CRD access, no exec into pods, no secret reads outside their workspace
    • Supply-chain exposure is contained: Kaniko runs in its own pod, credentials are per-build

    Customer support agent (support-agents namespace)

    • Agent has: CRM/ticketing API token, limited read-only GitHub
    • No Kubernetes write access in workers
    • Highest external prompt-injection surface — workers run with minimal privilege, no cluster access
    • MCP list is tightly scoped: only support tooling endpoints

    Personal mailbox / calendar agent (personal-agents namespace)

    • Agent has: IMAP/SMTP credentials, calendar API token
    • Workers have no Kubernetes access at all — purely API-driven
    • mailConfig in Agent spec points to the user's mailbox
    • Multiple personal agents possible: one per person, each in their own namespace or sharing a namespace if trust domain matches

    Multi-GitHub-Enterprise / multi-Gitea

    • Agent CR has a typed gitProviders list — each entry has its own serverURL and credentialSecretRef
    • The controller resolves webhook routing by matching serverURL in incoming events against registered providers
    • No global config needed; each Agent declares what it can see

    RabbitMQ event flow (simplified)

    Webhook handler (Go) → RabbitMQ exchange
      ↓ routed by type/key (issue URL, mail thread ID, etc.)
    Controller Deployment (consumer)
      → evaluates against Agent/Task CRs
      → creates/updates Task CRs
      → when worker pod is Ready, publishes prompt to per-Task queue
    Worker pod (consumer)
      → executes Claude/Codex turn
      → publishes result events back
    Controller
      → updates Task CR status
      → posts to GitHub/mail
    

    Deduplication uses RabbitMQ message IDs + Redis receipts. Persistence is RabbitMQ durable queues.


    Open questions I'd like your input on before we proceed

    1. Task CR location: workers namespace (cleaner GC cascade) vs orchestration namespace (simpler controller RBAC)?
    2. UI credential patching: controller-mediated proxy API (as described) vs direct kubectl by admins only (no UI mutation)?
    3. Agent admin role scope: should an agent admin be able to create new namespaces, or only manage Agents within a namespace an operator pre-provisioned for them?
    4. Calendar agent specifics: does it need write access to any Kubernetes resource at all, or is it purely external-API-driven?

    Happy to draft the actual CRD schemas and RBAC ClusterRole/Role structures once we align on these questions.

  2. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    Assume calendar has nothing to do with Kube RBAC. UI creds via web UI or via kubectl should apply similarly. Task CR should also live in the worker namespace. Worker pods themselves dont need to read or mutate Agent or Task CR-s. Assume that namespaces are created in advance and admin populates Agent CR.

    How does the webhook handler URL look like? Provide Agent admin/dev/customer support, Task CR examples

  3. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Follow-up: Webhook URLs, CR examples, and RBAC structure

    Answers to your clarifications: Task CRs live in the workers namespace, calendar is purely external-API-driven, UI and kubectl credential management work the same way through the controller API.


    Webhook handler URL design

    The Go webhook handler runs in the orchestration namespace behind a single Ingress. The URL encodes the target Agent so the handler can look up the right HMAC secret without any routing state:

    POST https://hooks.{host}/github/{namespace}/{agent-name}
    POST https://hooks.{host}/gitea/{namespace}/{agent-name}
    POST https://hooks.{host}/alertmanager/{namespace}/{agent-name}
    

    When registering a GitHub/Gitea repo webhook, the admin sets:

    • URL: https://hooks.example.com/github/dev-agents/backend-dev
    • Secret: the value from the webhookSecret key in that Agent's named Secret

    The handler flow:

    1. Parse {namespace}/{agent-name} from path
    2. Fetch Agent CR → get webhookSecretRef → fetch Secret key
    3. Validate HMAC
    4. Extract routing key from payload: github:{org}/{repo}#{issue-number} or github:{org}/{repo}#pr:{number} or bare github:{org}/{repo} for branch events
    5. Publish to RabbitMQ with that routing key and the raw (but bounded) event

    The controller (not the handler) resolves which Task corresponds to the routing key by scanning Task CR externalReferences. Multiple Agents can register the same repo webhook URL pointing to their own namespace/name — they get independent event streams.


    Agent CR examples

    Developer agent — multi-GitHub, one Gitea, Kaniko builds:

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Agent
    metadata:
      name: backend-dev
      namespace: dev-agents
    spec:
      workerImage: codemowers/agent:v1
      model:
        backend: claude
        credentialSecretRef: anthropic-key
      gitProviders:
        - type: github
          serverURL: https://github.com
          credentialSecretRef: github-public-bot
        - type: github
          serverURL: https://github.mycompany.com  # GHE instance
          credentialSecretRef: github-enterprise-bot
        - type: gitea
          serverURL: https://git.internal
          credentialSecretRef: gitea-internal-bot
      webhookSecretRef: github-webhook-secret
      credentialRefs:
        - anthropic-key
        - github-public-bot
        - github-enterprise-bot
        - gitea-internal-bot
        - github-webhook-secret
      mcpEndpoints:
        - url: http://driftmower-mcp.driftmower.svc.cluster.local:3000/mcp
          tokenFile: /var/run/agent-mcp/driftmower
        - url: https://mcp.codemowers.io/mcp
      workerAccess: developer   # binds a pre-defined ClusterRole to the worker SA in this namespace

    Customer support agent — read-only GitHub, CRM API, no Kubernetes access:

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Agent
    metadata:
      name: support-bot
      namespace: support-agents
    spec:
      workerImage: codemowers/agent:v1
      model:
        backend: claude
        credentialSecretRef: anthropic-key
      gitProviders:
        - type: github
          serverURL: https://github.com
          credentialSecretRef: github-support-readonly
      webhookSecretRef: github-support-webhook-secret
      credentialRefs:
        - anthropic-key
        - github-support-readonly
        - github-support-webhook-secret
        - crm-api-key
      mcpEndpoints:
        - url: http://support-crm-mcp.support-agents.svc.cluster.local:8080/mcp
      workerAccess: none   # worker pod SA has zero Kubernetes RBAC

    Kube admin agent — cluster-wide observability, IaC management:

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Agent
    metadata:
      name: kube-admin
      namespace: admin-agents
    spec:
      workerImage: codemowers/agent:v1
      model:
        backend: claude
        credentialSecretRef: anthropic-key
      gitProviders:
        - type: github
          serverURL: https://github.com
          credentialSecretRef: github-admin-bot
      webhookSecretRef: github-admin-webhook-secret
      credentialRefs:
        - anthropic-key
        - github-admin-bot
        - github-admin-webhook-secret
      mcpEndpoints:
        - url: http://driftmower-mcp.driftmower.svc.cluster.local:3000/mcp
          tokenFile: /var/run/agent-mcp/driftmower
        - url: https://mcp.codemowers.io/mcp
      workerAccess: cluster-readonly   # ClusterRole: get/list/watch most resources, no writes

    Personal calendar agent — mail-driven, no Git, no Kubernetes:

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Agent
    metadata:
      name: alice-personal
      namespace: personal-agents
    spec:
      workerImage: codemowers/agent:v1
      model:
        backend: claude
        credentialSecretRef: anthropic-key
      mailConfig:
        imap:
          host: imap.gmail.com
          port: 993
          credentialSecretRef: alice-gmail-creds
        smtp:
          host: smtp.gmail.com
          port: 465
          credentialSecretRef: alice-gmail-creds
      credentialRefs:
        - anthropic-key
        - alice-gmail-creds
        - alice-calendar-oauth
      mcpEndpoints:
        - url: http://google-calendar-mcp.personal-agents.svc.cluster.local:8080/mcp
      workerAccess: none

    Task CR examples

    Developer Task (created by controller on GitHub issue assignment, or manually):

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Task
    metadata:
      name: task-4a7f
      namespace: dev-agents
      ownerReferences:
        - apiVersion: agents.codemowers.io/v1alpha1
          kind: Agent
          name: backend-dev
          uid: ...
          blockOwnerDeletion: true
    spec:
      agentRef: backend-dev
      objective: |
        Implement pagination for GET /projects.
        Preserve existing response fields and open a pull request.
      desiredState: Active
      externalReferences:
        - source: github
          serverURL: https://github.com
          repositoryOwner: myorg
          repositoryName: backend
          kind: issue
          id: "42"
      completionPolicy:
        milestone: VerifiedChecks
        requiredChecks: [unit-tests, lint]
    status:
      phase: Running
      workerPodName: agent-worker-4a7f-3b1c
      sessionEpoch: 1
      progress:
        summary: "Opened PR #87 implementing cursor-based pagination; CI running."
        updatedAt: "2026-10-15T09:00:00Z"
      outputs:
        pullRequestURL: https://github.com/myorg/backend/pull/87
      conditions:
        - type: WorkerReady
          status: "True"

    Support Task (created from incoming mail or GitHub issue):

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Task
    metadata:
      name: task-9b2e
      namespace: support-agents
      ownerReferences:
        - apiVersion: agents.codemowers.io/v1alpha1
          kind: Agent
          name: support-bot
          uid: ...
          blockOwnerDeletion: true
    spec:
      agentRef: support-bot
      objective: Customer reported issue with refund on order #1234. Investigate and respond.
      desiredState: Active
      externalReferences:
        - source: mail
          messageId: "<ticket-1234.ref@support.example.com>"
          threadId: "thread-abc"

    Calendar Task (triggered by mail polling):

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Task
    metadata:
      name: task-7f3a
      namespace: personal-agents
      ownerReferences:
        - apiVersion: agents.codemowers.io/v1alpha1
          kind: Agent
          name: alice-personal
          uid: ...
          blockOwnerDeletion: true
    spec:
      agentRef: alice-personal
      objective: Meeting request from bob@company.com. Check availability Tue-Thu next week and reply with three options.
      desiredState: Active
      externalReferences:
        - source: mail
          messageId: "<msg-xyz@company.com>"

    RBAC structure (conceptual)

    workerAccess in the Agent CR names a known access profile. The controller binds a pre-existing ClusterRole to the worker's ServiceAccount (created per-Task in the worker namespace) when it creates the Pod:

    workerAccess value What the worker SA can do
    none Nothing — no Kubernetes API access
    developer Create/delete Pods (Kaniko), PVCs, ConfigMaps, Secrets in its own namespace; read push credentials
    cluster-readonly Get/list/watch most resource types cluster-wide; no writes

    The controller ServiceAccount (in the orchestration namespace) needs:

    • Cluster-wide watch/list/get/update on Agent and Task CRs
    • Create/delete Pods and PVCs in any registered worker namespace
    • Create/delete ServiceAccounts and RoleBindings in worker namespaces (to set up per-Task SAs)
    • Read Secrets in worker namespaces (to validate credentialRefs at admission time, not to forward values)

    Agent admin users (humans) get a Role in their worker namespace:

    • CRUD on Agent CRs in that namespace
    • CRUD on Secrets in that namespace (credential management)
    • Read/patch Task CRs in that namespace
    • No cross-namespace access

    Neither the worker Pods nor the webhook handler needs access to Secrets — credentials are mounted by the controller at pod creation time from the credentialRefs list.


    Does this match your intentions? If yes, I'll draft the actual CRD OpenAPI schemas next.

  4. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    agentRef seems unnecessary as ownerReferences are there
    desiredState doesnt make sense because its dependant on the codex/claude outcome
    workerAccess seems also unnecessary because the Kube RBAC would confine it to namespace

    note that admission policy could be used to enforce stuff per ns

  5. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Revised design: simplified Task CR

    Three good corrections — here's how the spec cleans up.


    agentRef dropped

    The Task's ownerReferences already carry the Agent name and UID. The controller resolves the owning Agent from ownerReferences when it needs the worker image, credentials, and MCP list. No redundant field needed.


    desiredState dropped — replaced with suspend

    desiredState: Active/Paused/Cancelled imposed a state machine on the spec side, which doesn't fit: the task lifecycle is driven by external events (GitHub issue closed, completionPolicy milestone reached, worker turn completed) not by a field you set. The only legitimate human-settable control is whether the controller should schedule new worker pods. Borrowing from CronJob:

    spec:
      suspend: true   # default false; set true to stop scheduling new turns

    Pause = suspend: true. Cancel = delete the Task CR (worker pod and PVC go with it via ownerRefs). Resume = suspend: false. The controller drives status.phase from observed state: Running, Waiting, Succeeded, Failed.


    workerAccess dropped

    The namespace admission policies already enforce what pods in that namespace can do. The developer namespace has policies permitting Kaniko pods and broader workload creation; the support namespace has stricter policies. The Agent CR doesn't need to duplicate that — it just says which credentials to mount and which MCP endpoints to configure. Privilege confinement is a namespace/admission concern, not an Agent spec field.


    Revised Agent CR (all four personas)

    Developer:

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Agent
    metadata:
      name: backend-dev
      namespace: dev-agents
    spec:
      workerImage: codemowers/agent:v1
      model:
        backend: claude
        credentialSecretRef: anthropic-key
      gitProviders:
        - type: github
          serverURL: https://github.com
          credentialSecretRef: github-public-bot
        - type: github
          serverURL: https://github.mycompany.com
          credentialSecretRef: github-enterprise-bot
        - type: gitea
          serverURL: https://git.internal
          credentialSecretRef: gitea-internal-bot
      webhookSecretRef: github-webhook-secret
      credentialRefs:
        - anthropic-key
        - github-public-bot
        - github-enterprise-bot
        - gitea-internal-bot
        - github-webhook-secret
      mcpEndpoints:
        - url: http://driftmower-mcp.driftmower.svc.cluster.local:3000/mcp
          tokenFile: /var/run/agent-mcp/driftmower
        - url: https://mcp.codemowers.io/mcp

    Customer support:

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Agent
    metadata:
      name: support-bot
      namespace: support-agents
    spec:
      workerImage: codemowers/agent:v1
      model:
        backend: claude
        credentialSecretRef: anthropic-key
      gitProviders:
        - type: github
          serverURL: https://github.com
          credentialSecretRef: github-support-readonly
      webhookSecretRef: github-support-webhook-secret
      credentialRefs:
        - anthropic-key
        - github-support-readonly
        - github-support-webhook-secret
        - crm-api-key
      mcpEndpoints:
        - url: http://support-crm-mcp.support-agents.svc.cluster.local:8080/mcp

    Kube admin:

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Agent
    metadata:
      name: kube-admin
      namespace: admin-agents
    spec:
      workerImage: codemowers/agent:v1
      model:
        backend: claude
        credentialSecretRef: anthropic-key
      gitProviders:
        - type: github
          serverURL: https://github.com
          credentialSecretRef: github-admin-bot
      webhookSecretRef: github-admin-webhook-secret
      credentialRefs:
        - anthropic-key
        - github-admin-bot
        - github-admin-webhook-secret
      mcpEndpoints:
        - url: http://driftmower-mcp.driftmower.svc.cluster.local:3000/mcp
          tokenFile: /var/run/agent-mcp/driftmower
        - url: https://mcp.codemowers.io/mcp

    Personal calendar (no git, no webhooks, mail-driven):

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Agent
    metadata:
      name: alice-personal
      namespace: personal-agents
    spec:
      workerImage: codemowers/agent:v1
      model:
        backend: claude
        credentialSecretRef: anthropic-key
      mailConfig:
        imap:
          host: imap.gmail.com
          credentialSecretRef: alice-gmail-creds
        smtp:
          host: smtp.gmail.com
          credentialSecretRef: alice-gmail-creds
      credentialRefs:
        - anthropic-key
        - alice-gmail-creds
        - alice-calendar-oauth
      mcpEndpoints:
        - url: http://google-calendar-mcp.personal-agents.svc.cluster.local:8080/mcp

    Revised Task CR

    apiVersion: agents.codemowers.io/v1alpha1
    kind: Task
    metadata:
      name: task-4a7f
      namespace: dev-agents
      ownerReferences:
        - apiVersion: agents.codemowers.io/v1alpha1
          kind: Agent
          name: backend-dev
          uid: <uid>
          blockOwnerDeletion: true
    spec:
      objective: |
        Implement pagination for GET /projects.
        Preserve existing response fields and open a pull request.
      suspend: false
      externalReferences:
        - source: github
          serverURL: https://github.com
          repositoryOwner: myorg
          repositoryName: backend
          kind: issue
          id: "42"
      completionPolicy:
        milestone: VerifiedChecks
        requiredChecks: [unit-tests, lint]
    status:
      phase: Running          # Pending | Running | Waiting | Succeeded | Failed
      reason: WorkerActive
      workerPodName: agent-worker-4a7f-3b1c
      sessionEpoch: 1
      progress:
        summary: "Opened PR #87; CI running."
        updatedAt: "2026-10-15T09:00:00Z"
      outputs:
        pullRequestURL: https://github.com/myorg/backend/pull/87
      conditions:
        - type: WorkerReady
          status: "True"
          lastTransitionTime: "2026-10-15T08:55:00Z"

    Ready to draft the CRD OpenAPI schemas if this looks right.

  6. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    What about the case where controller accesses the mailbox via IMAP/SMTP but we dont really want to expose those creds in the namespace (a'la exploited Kaniko build)?

  7. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Mail credential isolation

    Good catch. The threat model: a compromised Kaniko build in the worker namespace can read any Secret the worker SA can see. If IMAP/SMTP creds live there, they're at risk.

    The key observation is that mail credentials are never needed by the worker pod. The controller owns the IMAP IDLE connection and sends mail. Workers send mail exclusively through the Agent MCP send_email tool, which calls back to the controller over HTTP — the worker never touches SMTP directly.

    So mail credentials belong in the orchestration namespace, not the worker namespace. The controller reads them from its own namespace at reconcile time. Workers in the personal-agents or any other namespace never see them.

    This means mailConfig.credentialSecretRef resolves in the orchestration namespace, while credentialRefs (things mounted into worker pods) resolve in the worker namespace. The distinction maps cleanly onto who consumes the credential:

    Credential type Where it lives Who reads it
    Model API key (credentialRefs) Worker namespace Worker pod (Claude/Codex CLI)
    Git tokens (credentialRefs) Worker namespace Worker pod (gh, git)
    Registry push secret (credentialRefs) Worker namespace Worker pod (Kaniko/Skaffold)
    IMAP/SMTP creds (mailConfig) Orchestration namespace Controller only
    Webhook HMAC secrets (webhookSecretRef) Worker namespace Webhook handler → fetched by handler SA with scoped read

    The Agent CR references mail credentials by name, and the controller resolves them from its own namespace. The agent admin creates those secrets in the orchestration namespace (not the worker namespace) via kubectl or the controller API. A compromised Kaniko build in personal-agents has no path to the orchestration namespace.

    For webhookSecretRef, the handler reads it from the worker namespace using a scoped SA with get-only access to specifically named Secrets (similar to the current Kaniko push secret pattern) — narrow enough that a compromised build cannot enumerate other Secrets.

    Updated Agent spec:

    spec:
      mailConfig:
        imap:
          host: imap.gmail.com
          credentialSecretRef: alice-gmail-creds    # resolved in orchestration namespace
        smtp:
          host: smtp.gmail.com
          credentialSecretRef: alice-gmail-creds    # same
      credentialRefs:                               # all resolved in worker namespace
        - anthropic-key
        - alice-calendar-oauth

    No namespace field needed on the references — the consuming component (controller vs worker) determines which namespace to look in. This is an invariant enforced in the operator code, not in the CR schema.

  8. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    Explore the option of using admission policy for confinement? Can we do something like matching ownerReferences (is Task or Agent) vs secrets to be mounted?

  9. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Admission policy for Secret confinement

    Good direction. Let's be precise about what CEL admission policies can and can't enforce here.

    What CEL can see at pod admission time

    ValidatingAdmissionPolicy with CEL gets the incoming Pod object and the namespace object. It cannot fetch arbitrary other resources (Secrets, CRs) during evaluation — no cross-resource lookups. So you can't write a policy that says "fetch the Task CR and check that this pod's volumes are a subset of its credentialRefs."

    What you can do:


    Policy 1: Only controller-managed pods can mount Secrets

    The controller labels every pod it creates with agents.codemowers.io/managed: "true". A CEL policy on the namespace rejects any pod that mounts Secrets but lacks that label:

    object.spec.volumes.exists(v, has(v.secret))
      implies
    object.metadata.labels.exists(k, k == "agents.codemowers.io/managed")
    

    This blocks ad-hoc pods from reading Secrets — only the controller can create pods that reference them. The controller has already validated credentialRefs before creating the pod.


    Policy 2: Kaniko pods mount only the designated push secret

    The skaffold-kaniko label is already required for Kaniko admission. Extend that policy: a Kaniko pod may only reference Secrets whose names match the expected push-secret name (known at namespace setup time, or encoded in a namespace label like agents.codemowers.io/push-secret: zot-registry-cluster-push-kaniko):

    has(object.metadata.labels["skaffold-kaniko"])
      implies
    object.spec.volumes.all(v,
      !has(v.secret) ||
      v.secret.secretName == namespaceObject.metadata.labels["agents.codemowers.io/push-secret"]
    )
    

    This is the Kaniko isolation you were thinking about — a build job can't read model API keys or GitHub tokens even if they're in the same namespace, because the admission policy rejects it at scheduling time.


    Cross-Agent isolation within a shared namespace

    CEL can't enforce "Agent A's pod only mounts Agent A's Secrets" without naming conventions or separate namespaces:

    Option A — naming convention: Secrets are named {agent-name}-{key}. CEL checks that pod labeled agents.codemowers.io/agent: backend-dev only mounts secrets prefixed backend-dev-. Brittle but zero external lookups.

    Option B — separate namespace per Agent: Pods can only reference Secrets in their own namespace. Cross-Agent isolation is structural. This is the right choice when security domains genuinely differ (dev vs support vs personal). Multiple Agents in one namespace is fine when trust domains truly overlap.

    Option C — controller trust boundary: If two Agents share a namespace because their trust domain is the same (e.g., two dev bots for the same team), cross-Agent Secret isolation isn't a security concern — the same admin manages both. The controller enforces credentialRefs; admission policy enforces controller-managed-only. Good enough.


    Practical policy set for the namespace

    Policy Mechanism Blocks
    Only controller pods mount Secrets CEL label check Ad-hoc kubectl-created pods reading Secrets
    Kaniko pods only mount push secret CEL label + namespace label for push-secret name Compromised build reading model/git tokens
    Worker SA has no Secret list/get RBAC Worker code calling Kubernetes API to enumerate Secrets
    Mail creds in orchestration namespace Placement Any worker-namespace compromise reaching mail creds
    Separate namespaces for different trust levels Structural Cross-domain Secret access entirely

    The Kaniko policy (Policy 2) directly addresses the original concern: an exploited build can't exfiltrate GitHub tokens or model API keys because the admission policy rejects the pod if it tries to mount them. The build pod is structurally limited to the push secret.

  10. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    What prevents prompt injected Claude/Codex worker pod from creating pod with elevated privileges (wrong label)

  11. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Addressing the label forgery attack

    You're right to call this out. Labels are caller-controlled metadata — anyone who can create a pod can set any label. A prompt-injected worker that has Pod create permission could set agents.codemowers.io/managed: "true" itself and bypass Policy 1 entirely. Labels cannot be a security boundary.

    The fix: base the admission check on request.userInfo, not on pod labels. In ValidatingAdmissionPolicy, request.userInfo.username is set by Kubernetes from the authenticated caller's identity — that's system:serviceaccount:<ns>:<sa-name>, unforgeable by the pod being created.

    Revised policy structure

    Policy: Secret mounts require controller SA identity

    Only pods submitted by the controller ServiceAccount can reference arbitrary Secrets (the credentialRefs list). No label check — identity check:

    object.spec.volumes.exists(v, has(v.secret))
      implies
    request.userInfo.username == "system:serviceaccount:agent-system:agent-controller"
    

    A compromised worker calling kubectl create pod with agents.codemowers.io/managed: "true" is rejected because the request comes from the worker SA, not the controller SA.

    Policy: Worker SAs creating pods (for Kaniko) can only mount the push secret

    Developer-type workers need to create Kaniko pods via Skaffold. Their SA has Pod create permission, but:

    !object.spec.volumes.exists(v, has(v.secret)) ||
    request.userInfo.username == "system:serviceaccount:agent-system:agent-controller" ||
    object.spec.volumes.all(v,
      !has(v.secret) ||
      v.secret.secretName == namespaceObject.metadata.labels["agents.codemowers.io/push-secret"]
    )
    

    This allows worker SAs to create pods that only mount the push registry credential. Even a fully compromised prompt-injected worker can't create a pod that mounts GitHub tokens or the model API key.

    Layer 2: Worker SA has no Secret read RBAC

    Even without volume mounts, a worker could call the Kubernetes API directly. The worker SA Role has no get/list/watch on Secrets — this is the RBAC layer stopping API-based exfiltration.

    Layer 3: Pod Security Standards on the worker namespace

    PSS restricted profile (or admission policy equivalent) prevents privilege escalation even if a worker creates a pod: no hostNetwork, no privileged, no hostPath, hostUsers: false. A worker can't bootstrap to a more privileged execution environment.


    Full confinement chain against a prompt-injected worker

    1. Worker SA has no Secret read RBAC → can't enumerate or read Secrets via API
    2. Admission policy (identity-based) → pods it creates can only mount the push secret, not model/git/mail credentials
    3. PSS restricted → can't create privileged containers, hostPath mounts, or escape user namespaces
    4. Mail creds in orchestration namespace → structurally unreachable from worker namespace regardless of RBAC

    The only thing a prompt-injected developer worker can do is run a Kaniko build (mounting the push secret) and push a malicious image to the registry. That's an already-accepted risk in any CI system and is addressed at the registry/deploy admission layer, not here.

    For none-access workers (support, calendar): their SA has no Pod create permission either, so they can't create any pods at all — the attack surface is just the worker's own mounted credentials (model key, CRM token, calendar OAuth) and whatever external APIs those reach.

  12. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    With this new approach can we return mail creds into the worker namespace? Ideally I would let user manage secrets in the worker namespace and policies define which pods can use them

  13. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Yes — mail creds can return to the worker namespace

    With identity-based admission policies, the separation is enforced by who creates the pod, not where the Secret lives. Mail creds can safely live in the worker namespace because:

    1. Worker SA has no Secret read RBAC — can't fetch them via the Kubernetes API
    2. Admission policy (SA identity check) — pods created by a worker SA can only mount the push secret; they can't mount mail creds even if they're in the same namespace
    3. Controller SA creates the actual worker pods — and the controller deliberately never includes mail credentials in credentialRefs volume mounts (workers call send_email via MCP, not SMTP directly)
    4. Controller SA reads mail creds — it has Secret get access across worker namespaces to establish IMAP/SMTP connections. The controller is the trust root; its SA being broadly privileged is expected.

    So the admin creates and manages all Secrets in the worker namespace via kubectl or the controller API — model keys, git tokens, push secrets, IMAP/SMTP creds, calendar OAuth — all in one place. Admission policies define which pod types can mount which Secrets:

    Pod creator SA Can mount
    Controller SA Any Secret in the namespace (all credentialRefs)
    Worker SA (developer type) Push secret only
    Worker SA (none type) No Pod create permission at all
    Any other SA / human No Secrets (rejected by admission policy)

    The controller's job when creating a worker pod: look at the Agent's credentialRefs, mount only those. Mail creds are in credentialRefs? Controller still doesn't mount them into the worker pod — the worker doesn't need them. The list controls what could be mounted; the controller decides what is mounted based on what the worker actually needs.

    This is the cleaner model: one namespace per security domain, all secrets there, policies enforce pod-type boundaries.

    Updated Secret namespace placement:

    Credential Namespace Who reads it
    Model API key Worker namespace Worker pod (mounted by controller)
    Git tokens Worker namespace Worker pod (mounted by controller)
    Registry push secret Worker namespace Worker pod/Kaniko (mounted by controller or worker SA)
    IMAP/SMTP creds Worker namespace Controller SA (reads directly; never mounted in worker pod)
    Calendar/CRM OAuth Worker namespace Worker pod (mounted by controller)
    Webhook HMAC secret Worker namespace Webhook handler SA (scoped get)
  14. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    Check the edge case where pods are created by operator (eg Dragonfly or cnpg)? If exploited worker can instantiate these (eg during skaffold deploy), how can you defend against malicious Dragonfly pod spec?

  15. 26 remaining items

  16. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    Pushed local changes c509bd8

  17. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Restructured to match lolcatz Skaffold/Helm layout — PR #10 updated

    Followed the pattern from codemowers/lolcatz: single Go module, one Dockerfile.golang with ARG SERVICE, all components buildable via a single skaffold dev.

    New layout

    codemowers/agent/
    ├── services/
    │   ├── go.mod                        shared Go module
    │   ├── Dockerfile.golang             ARG SERVICE → webhook or operator
    │   ├── webhook/main.go               GitHub/Gitea/Alertmanager → RabbitMQ
    │   └── operator/
    │       ├── main.go
    │       ├── api/v1alpha1/             Agent + Task CRD Go types
    │       └── internal/controller/      AgentReconciler + TaskReconciler
    ├── chart/templates/
    │   ├── webhook-deployment.yaml       Deployment + Service
    │   └── operator-deployment.yaml     SA + ClusterRole + CRB + Deployment
    ├── skaffold.yaml                     4 artifacts: agent, agent-controller, agent-webhook, agent-operator
    └── Dockerfile                        Node.js stages (unchanged)
    

    What skaffold dev builds

    Image Dockerfile What
    codemowers/agent Dockerfile → runner Ubuntu worker runner
    codemowers/agent-controller Dockerfile → controller Node.js controller
    codemowers/agent-webhook services/Dockerfile.golang SERVICE=webhook Go HMAC handler → RabbitMQ
    codemowers/agent-operator services/Dockerfile.golang SERVICE=operator Go controller-runtime operator

    Still needed

    1. go.sum — run cd services && go mod tidy locally or I can wire it into CI. The go.sum is absent so the build will fail without it.
    2. RabbitMQ — crd/rabbitmq-cluster.yaml needs to be applied; the webhook Deployment expects a {release}-rabbitmq-default-user secret (standard topology-operator output).
    3. CRDs — kubectl apply -f crd/agents.codemowers.cloud_*.yaml (see earlier comment).

    Want me to add a GitHub Actions workflow to run go mod tidy and push go.sum, or would you prefer to run it locally and push?

  18. laurivosandi commented on Oct 15, 2026

    @laurivosandi
    ContributorAuthor

    I was referring to structure of skaffold.yaml. For lolcatz shared Go module made sense, I think that is not the case here.

    You're again mixing CRD-s with CR-s. Rabbitmq instantiation should be in chart/templates/ the skaffold.dev/keep keeps it around even if skaffold

    Agent and Task CRDs are installed. You should be able to run skaffold dev already now. Make it possible to have such Helm setup that you can deploy it to your own ns. Dont mess up existing controller

  19. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Build verified — PR #10 updated

    Both Go images build and push via Kaniko. Fixes applied:

    Corrections from feedback:

    • Shared Go module reverted: webhook/ and operator/ each have their own go.mod and Dockerfile with separate build contexts
    • RabbitMQ RabbitmqCluster + topology objects moved to chart/templates/rabbitmq.yaml (guarded by rabbitmq.enabled); skaffold.dev/keep=true is added automatically by the cluster admission policy
    • ClusterRole/CRB names include Release.Namespace so two releases in different namespaces coexist
    • skaffold.yaml validated against v4beta11 schema — valid

    Build results:

    Build [codemowers/agent-webhook] succeeded  ✓
    Build [codemowers/agent-operator] succeeded ✓
    

    Deploying to a test namespace — set SKAFFOLD_NAMESPACE in your local skaffold.env:

    SKAFFOLD_NAMESPACE=my-test-ns skaffold dev  # local, needs Docker + Helm
    # or from worker pod (Kaniko, no Helm needed):
    skaffold build --filename skaffold-build.yaml

    Remaining before skaffold dev works end-to-end on a dev machine:

    1. Helm chart apiResource is now required (required guard added by Lauri) — set it in skaffold.env: AGENT_API_RESOURCE=https://controller.<host>/api
    2. RabbitMQ messaging-topology-operator must be present (it is on this cluster) for the topology objects to reconcile

    What's next on the refactor? The operator currently only manages Pod/PVC lifecycle; the event consumer (controller reading from RabbitMQ → evaluate Agent/Task state) is the next piece.

  20. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    RabbitMQ event consumer wired into operator — PR #10 updated

    EventConsumer added to operator/internal/controller/consumer.go:

    • Connects to AMQP at startup (AMQP_URL from the rabbitmq default-user secret)
    • Consumes from agent.webhook.events queue (QoS=1)
    • Parses routing keys (github:org/repo#issue:N, alertmanager:fp) into structured events
    • Matches events against Task.spec.externalReferences — source / owner / repo / kind / id
    • Annotates matched Tasks with codemowers.cloud/event-received-at to trigger the TaskReconciler
    • Nacks unparseable messages (dead-letter, no infinite loop)
    • Reconnects with exponential backoff (1s → 60s cap)

    Wired via mgr.Add(consumer) so it runs in the same process and shares the manager lifecycle. All four images build clean via Kaniko: codemowers/agent, codemowers/agent-controller, codemowers/agent-webhook, codemowers/agent-operator.

    End-to-end flow now: webhook → HMAC validate → RabbitMQ → operator consumer → Task annotation → TaskReconciler → worker Pod.

    What's next: Task creation for unmatched events (new GitHub issue assignments), or shall I move on to the Node.js controller consuming from the same queue for the existing worker dispatch path?

  21. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Self-deploy status — one admin bootstrap needed

    skaffold run --profile=cluster from the worker pod now reaches Helm apply cleanly. Two things found:

    Fixed in latest commit:

    • Secret name mismatch: webhook + operator looked for agent-rabbitmq-default-user but the RabbitmqCluster is named agent-events, so the secret is agent-events-default-user. Fixed.

    One-time admin step required (worker SA cannot create ServiceAccounts/Roles):

    # From your local machine with cluster-admin on ee-lte-1
    git clone https://github.com/matmlazi/agent -b refactor/architecture && cd agent
    
    helm template agent chart \
      --kube-version=1.36.3 \
      --set host=controller \
      --set "allowedGroups[0]=github.com:codemowers:admins" \
      --set "env.AGENT_SHARING_MODE=public" \
      --set "image=x" --set "workerImage=x" \
      --set "webhook.image=x" --set "operator.image=x" \
      --set rabbitmq.enabled=false \
    | kubectl --context=ee-lte-1.codemowers.io/codemowers-agent-system apply -f -

    This creates: agent-controller, agent-worker, agent-operator ServiceAccounts + ClusterRole/RoleBindings for the operator + controller/worker Roles.

    After that, skaffold run --profile=cluster from the worker handles all future deploys indefinitely without RBAC writes. RabbitMQ, OIDCClient, Ingress, Deployments — all applied by the worker.

  22. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    RBAC bootstrap missing — agent-worker SA has no permissions

    The agent-worker SA currently has no Role/RoleBinding in codemowers-agent-system. Kaniko builds and Helm deploys both fail. Previous bootstrap likely applied only part of the resources.

    Apply just the RBAC objects — run this from your local machine:

    cd agent  # the refactor/architecture checkout
    
    helm template agent chart \
      --kube-version=1.36.3 \
      --set host=controller \
      --set "allowedGroups[0]=github.com:codemowers:admins" \
      --set "env.AGENT_SHARING_MODE=public" \
      --set "env.AGENT_KUBE_CONTEXT=kubernetes/default" \
      --set "image=x" --set "workerImage=x" \
      --set "webhook.image=x" --set "operator.image=x" \
      --set rabbitmq.enabled=false \
      --set bootstrapRbac=true \
      --namespace codemowers-agent-system \
    | python3 -c "
    import sys
    docs = sys.stdin.read().split(chr(10)+chr(45)*3+chr(10))
    rbac_kinds = {'ServiceAccount', 'Role', 'ClusterRole', 'RoleBinding', 'ClusterRoleBinding'}
    for doc in docs:
        for line in doc.splitlines():
            if line.startswith('kind:'):
                if line.split(':',1)[1].strip() in rbac_kinds:
                    print(doc.strip()); print('---')
                break
    " | kubectl --context=ee-lte-1.codemowers.io/codemowers-agent-system apply -f -

    Expected: 3 SAs, 1 ClusterRole, 1 ClusterRoleBinding, 2 Roles, 2 RoleBindings created/configured.

    After that, reply here and I'll run skaffold run --profile=cluster.

  23. matmlazi commented on Oct 15, 2026

    @matmlazi
    Collaborator

    Blocker: RabbitMQ operator not reconciling

    agent-events RabbitmqCluster was created 30+ minutes ago but has no status (empty {}) and no StatefulSet was created. The RabbitMQ operator appears to not be watching this namespace.

    Current state:

    • agent (controller) deployment: ✅ Running
    • agent-operator / agent-webhook: ❌ CreateContainerConfigError: secret "agent-events-default-user" not found — waiting for RabbitMQ
    • agent-events RabbitmqCluster: ❌ No StatefulSet, no status, no pods after 30+ minutes

    The rabbitmq-replicas MutatingAdmissionPolicy applied topology spread constraints with MinDomains: 2 (HA mode). Even if the operator starts reconciling, this may prevent scheduling if the cluster has fewer than 2 zones.

    Question: Is the RabbitMQ operator running on this cluster and watching codemowers-agent-system? Should we use a different message queue (e.g., Redis streams) or is there a way to request single-replica (no-HA) mode for the RabbitmqCluster?

  24. deleted a comment from matmlazi on Oct 16, 2026
  25. deleted a comment from matmlazi on Oct 16, 2026
  26. matmlazi commented on Oct 17, 2026

    @matmlazi
    Collaborator

    Agent and Task CRD design + RBAC organization

    Core ownership chain

    Agent CR (namespace: workers-ns)
      └── Task CR (namespace: workers-ns, ownerRef → Agent)
            ├── Worker Pod (ownerRef → Task) — deleted when Task is deleted
            └── Workspace PVC (ownerRef → Task) — deleted when Task is deleted
    

    Deleting an Agent cascades through Tasks → Pods + PVCs. No orphan cleanup needed.


    Agent CR shape

    The Agent CR lives in the same namespace where its workers run. It declares the full privilege domain: which forge providers, mail accounts, Slack workspaces, MCP servers, and secrets the workers for this agent are allowed to use.

    Key spec fields:

    • gitProviders[] — list of GitHub/GHE/Gitea entries, each with a serverURL, optional apiURL, and a credentialSecretRef pointing to an agent-ctrl-* Secret in the same namespace. Multiple entries support multiple Giteas and GHEs.
    • slackWorkspaces[] — list of Slack workspace entries with workspaceId, botTokenSecretRef (agent-ctrl-*) and signingSecretRef (agent-hook-*).
    • mail — optional; imapSecretRef and smtpSecretRef both point to agent-ctrl-* Secrets; either can be absent independently.
    • mcpServers[] — allowlist of MCP endpoints workers may connect to; the operator injects these into worker Pods.
    • workerCredentials[] — list of agent-cred-* Secret references to mount into worker Pods (model API key, git push token etc).
    • backend / model — default CLI and model; Task spec overrides per-execution.
    • workerImage, storageClass, workspaceSize, budgetUSD — worker provisioning knobs.
    • allowedSubjects[] / allowedGroups[] — who may submit prompts to Tasks under this Agent (enforced by the controller API, mirroring the Task API auth already in place).

    Status: activeTasks, readyConditions[], lastModelCatalogRefresh, connectorStatus (IMAP, SMTP, Slack, per-forge).


    Task CR shape

    Task CRs are created by the controller (automatically from routing events) or by an authenticated user via the API/UI. They are not meant to be hand-written in most flows.

    Key spec fields:

    • agentRef — name of the owning Agent CR in the same namespace.
    • objective — the seeded prompt text; the worker also gets the full spec file on the PVC.
    • externalReferences[] — list of routing anchors: { providerName, kind: issue|pr|mail|slack|alertmanager, key }. Multiple keys per Task handle the issue→PR→linked-mail convergence case.
    • desiredState — Active, Paused, Cancelled.
    • backend / model — frozen at execution creation; changes queue for next turn.

    Status: phase, reason, scratchVolume, lastExecution (Pod name/UID, state, exitCode), progress, routingKeys[], conditions[].

    The controller is the only writer of status. Workers report through the Agent MCP HTTP endpoint, not directly.


    RBAC organization — four roles, no YAML

    Platform admin — ClusterRole over all agents.codemowers.io resources, all namespaces. Installs the operator, creates namespaces, bootstraps initial RBAC. Typically a small ops team.

    Developer — namespace-scoped Role in the team's agent namespace: get/list/watch on Agent CRs (to see config and connector status), full CRUD on Task CRs (create prompts, pause/cancel), read on Worker Pods and the PVC (for debugging). No access to agent-ctrl-* or agent-cred-* Secrets — those are controller-only.

    Customer support — namespace-scoped Role in the support agent namespace: get/list/watch on Task CRs only (to see progress and read the timeline via the web UI). No ability to create Tasks or inspect Agent config. Task creation is driven by ticket-system webhooks, not human prompts.

    Kube admin agent — Its namespace is access-restricted to the platform-admin group only; no developer or support binding. The Agent CR grants its workers a ClusterRole for read across the cluster (events, pods, nodes etc) and targeted write for specific operator-managed resources. Alertmanager routes to this namespace's webhook URL.

    Personal mailbox agent — User alice gets a namespace alice-agents with a Role binding her OIDC subject to full Task CRUD and get on the Agent CR. The Agent CR lists no forge providers, no Driftmower MCP — workers have zero cluster access. Only mail and calendar MCPs are listed. No other binding exists; the namespace is her personal silo.


    Multi-forge, multi-namespace, multi-Agent topology

    Each Agent CR's gitProviders[] list carries the full serverURL, so a cluster can have:

    • github-public pointing to https://github.com in one Agent
    • github-enterprise pointing to https://github.myco.internal in another Agent (or the same one)
    • gitea-ci pointing to https://gitea.myco.internal in a third Agent

    Routing keys embed the server URL to avoid collision:

    github:github.com:org/repo#issue:42
    github:github.myco.internal:org/repo#issue:42
    gitea:gitea.myco.internal:org/repo#issue:42
    

    Multiple Agent CRs can live in the same namespace when the security domain aligns (e.g., frontend-agent and backend-agent both in dev-team-agents). Each has its own Task CRs, ownerRef chain, and credential sets. The operator handles them independently.

    The orchestration operator itself runs in a single system namespace (e.g., agent-system). It holds RabbitMQ, the webhook handler Deployment, the mail connector Deployment, and the core reconciler. It watches Agent/Task CRDs cluster-wide.


    Secret naming, web UI handling

    Three tiers, same as established:

    • agent-hook-* — HMAC signing keys for webhook verification; the webhook handler reads them from the Agent's namespace.
    • agent-ctrl-* — controller-only connector credentials (IMAP, SMTP, Slack bot token, forge crawl tokens); never mounted into worker Pods.
    • agent-cred-* — worker runtime credentials (model API key, git push token); mounted into worker Pods only; admission policy rejects Kaniko pod specs that reference them.

    Web UI secret management: the controller exposes a scoped API: given the user's OIDC subject and the Agent they can access, they can PUT /agents/{ns}/{name}/secrets/{tier}/{key} to upsert the value of a specific Secret key. The raw value is write-only through the UI (the API never returns it). The operator validates and updates the live Secret object in the Agent's namespace. kubectl works equally well — the naming convention is deterministic. No new CRD for Secrets; the Secret stays a native Kubernetes object.


    Open question on the mail connector

    The IMAP crawler as a separate Deployment with leader election fits cleanly. One option is to not make it an independent service, but instead a sidecar of the controller Deployment that acquires a Lease before enabling IDLE. The advantage: it shares the controller's Secret access and RabbitMQ connection without a separate Deployment manifest. The disadvantage: scaling the controller horizontally for other reasons would spin up duplicate IMAP clients competing for the Lease. A separate Deployment with a dedicated Lease and a single replica + PDB is cleaner for independent scaling and restart.

    Ready to start implementing any of these pieces — confirm which component to tackle first.

  27. matmlazi commented on Oct 17, 2026

    @matmlazi
    Collaborator

    Current deployment status

    Previous blockers resolved:

    • ✅ RabbitMQ agent-events cluster is healthy (ALLREPLICASREADY=True)
    • ✅ RBAC bootstrapped — agent-operator-operator and agent-operator-webhook are running
    • ✅ agents.codemowers.cloud CRDs are installed (agents + tasks)
    • ✅ Go operator is running, connected to AMQP, watching Agent/Task CRDs
    • ⚠️ agent-operator (Node.js controller) had transient startup Redis errors that appear to have resolved

    Outstanding: namespace isolation for skaffold dev

    The skaffold.yaml release name is hardcoded as agent. Developers running skaffold dev with SKAFFOLD_NAMESPACE=codemowers-agent-system would collide with the existing production agent release.

    To address your "don't mess up existing controller" requirement, I can:

    1. Add AGENT_RELEASE support: read from skaffold.env and pass as nameOverride Helm value, so each developer's deployment gets a unique name prefix (e.g. laurivosandi)
    2. Update skaffold.env.example to guide developers to use their own namespace + release name

    Should I go ahead with this change? Also: the agent-operator-operator Ingress at sloperator is enabled — should the dashboard require OIDC auth before merging PR #10, or is public access acceptable for now?

  28. laurivosandi commented on Oct 23, 2026

    @laurivosandi
    ContributorAuthor

    Implemented

    • A Go operator, webhook receiver, Device bridge and optional feed crawler in one harness namespace. Agents and Tasks live in namespaces administrators choose.
    • Agent → Task → worker Pod and PVC ownership. Workers report through an execution-bound Agent MCP and never write Agents or Tasks.
    • Per-Agent worker ServiceAccounts confined by RBAC, optional resource-prefix admission and user namespaces.
    • Credentials as individual keys of two fixed Secrets, injected only into worker containers. Kaniko pods mount only the registry push Secret.
    • Webhook protocols, receivers and bindings with CEL routing on canonical keys, covering GitHub, GitHub Enterprise, Gitea, Slack and Alertmanager. TaskPolicies create Tasks for verified assignments. Notifications are coalesced per key, and Tasks complete on close.
    • Mailboxes in their own Pods with IMAP, SMTP and an MCP server, accessed through RBAC verbs.
    • MCP servers, Devices, feeds and Task wakes, all granted through RBAC.
    • The OIDC dashboard, and Prometheus metrics for the operator and workers.

    Not implemented: outbound Slack

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions