diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7653e65e..7b32f34f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,6 +137,43 @@ jobs: - name: Test run: go test ./... + temporal-engine-chart: + name: Helm temporal-engine (lint, render) + runs-on: ubuntu-latest + defaults: + run: + working-directory: charts/agent-controller/charts/temporal-engine + steps: + - uses: actions/checkout@v5 + + - uses: azure/setup-helm@v4 + with: + version: v3.18.4 + + # Rendered standalone rather than through the umbrella chart: the parent + # pulls open-webui and nats from remote repos, so templating it needs a + # `helm dependency build` and a network round trip to validate a subchart + # that has neither. + - name: Lint + run: helm lint . --set qdrant.host=q --set nats.url=nats://n:4222 + + # Both shapes matter. Defaults must render the minimum (no catalog-sync + # without Qdrant, no AgentRun grant without the NATS bridge), and + # everything-on must render at all -- an optional block that only breaks + # when enabled is the one nobody notices. + - name: Render with defaults + run: helm template t . | python3 -c "import sys,yaml; list(yaml.safe_load_all(sys.stdin))" + + - name: Render with every option enabled + run: | + helm template t . \ + --set qdrant.host=qdrant \ + --set nats.url=nats://nats:4222 \ + --set gateway.senderAssertion.secretName=sa \ + --set identityLink.gatewayUrl=http://integration-gateway:8080 \ + --set identityLink.tokenSecretName=ig-token \ + | python3 -c "import sys,yaml; list(yaml.safe_load_all(sys.stdin))" + core-controller: name: Go core-controller (lint, test, build) runs-on: ubuntu-latest diff --git a/charts/agent-controller/Chart.yaml b/charts/agent-controller/Chart.yaml index f908f9e6..70976a88 100644 --- a/charts/agent-controller/Chart.yaml +++ b/charts/agent-controller/Chart.yaml @@ -38,6 +38,15 @@ dependencies: version: "0.1.0" repository: "file://charts/core-controller" condition: core-controller.enabled + # The Temporal-workflow agent engine (engines/temporal), docs/adr/0036. OFF + # by default: agent-orchestrator runs its in-process LangGraph loop unless + # AGENT_ENGINE=temporal selects this instead, so enabling the subchart alone + # changes no behaviour. Takes a Temporal address rather than bundling a + # server, so it adds no stateful component. + - name: temporal-engine + version: "0.1.0" + repository: "file://charts/temporal-engine" + condition: temporal-engine.enabled # GitHub Issues adapter (apps/integration-gateway) -- phase 1 of # docs/integrations-gateway.md's conversational path. Off by default; # calls the agent-orchestrator subchart's invoke Service, so enable that diff --git a/charts/agent-controller/charts/temporal-engine/Chart.yaml b/charts/agent-controller/charts/temporal-engine/Chart.yaml new file mode 100644 index 00000000..209ab520 --- /dev/null +++ b/charts/agent-controller/charts/temporal-engine/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: temporal-engine +description: >- + The Temporal-workflow agent engine (engines/temporal) — worker, gateway and + catalog-sync. Off by default: agent-orchestrator runs its in-process + LangGraph loop unless AGENT_ENGINE=temporal selects this instead + (docs/adr/0036). Assumes a Temporal cluster is already reachable and takes + its address, so enabling this adds no stateful component. +type: application +version: 0.1.0 +appVersion: "0.1.0" +home: https://github.com/imaustink/agent-controller +sources: + - https://github.com/imaustink/agent-controller +keywords: + - agent + - temporal + - workflow + - durable-execution +maintainers: + - name: controller-agent diff --git a/engines/temporal/charts/durable-agents/templates/_helpers.tpl b/charts/agent-controller/charts/temporal-engine/templates/_helpers.tpl similarity index 72% rename from engines/temporal/charts/durable-agents/templates/_helpers.tpl rename to charts/agent-controller/charts/temporal-engine/templates/_helpers.tpl index ae4f10cb..d387a89e 100644 --- a/engines/temporal/charts/durable-agents/templates/_helpers.tpl +++ b/charts/agent-controller/charts/temporal-engine/templates/_helpers.tpl @@ -1,10 +1,10 @@ -{{- define "durable-agents.labels" -}} +{{- define "temporal-engine.labels" -}} app.kubernetes.io/name: {{ .Chart.Name }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} -{{- define "durable-agents.temporalEnv" -}} +{{- define "temporal-engine.temporalEnv" -}} - name: TEMPORAL_ADDRESS value: {{ .Values.temporal.address | quote }} - name: TEMPORAL_NAMESPACE @@ -13,7 +13,7 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} value: {{ .Values.taskQueue | quote }} {{- end }} -{{- define "durable-agents.qdrantEnv" -}} +{{- define "temporal-engine.qdrantEnv" -}} - name: QDRANT_HOST value: {{ .Values.qdrant.host | quote }} - name: QDRANT_PORT @@ -24,10 +24,10 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{- end }} -{{- define "durable-agents.callbackBaseURL" -}} +{{- define "temporal-engine.callbackBaseURL" -}} {{- if .Values.callback.baseURL -}} {{ .Values.callback.baseURL }} {{- else -}} -http://{{ .Release.Name }}-gateway-callback.{{ .Release.Namespace }}.svc:8081 +http://{{ .Release.Name }}-temporal-engine-gateway-callback.{{ .Release.Namespace }}.svc:8081 {{- end -}} {{- end }} diff --git a/engines/temporal/charts/durable-agents/templates/catalog-sync.yaml b/charts/agent-controller/charts/temporal-engine/templates/catalog-sync.yaml similarity index 72% rename from engines/temporal/charts/durable-agents/templates/catalog-sync.yaml rename to charts/agent-controller/charts/temporal-engine/templates/catalog-sync.yaml index a8d3a9cf..f5a77bad 100644 --- a/engines/temporal/charts/durable-agents/templates/catalog-sync.yaml +++ b/charts/agent-controller/charts/temporal-engine/templates/catalog-sync.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ .Release.Name }}-catalog-sync + name: {{ .Release.Name }}-temporal-engine-catalog-sync labels: - {{- include "durable-agents.labels" . | nindent 4 }} + {{- include "temporal-engine.labels" . | nindent 4 }} --- # Read access to the catalog CRs, granted in the namespace where # agent-controller keeps them. @@ -12,9 +12,9 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: {{ .Release.Name }}-catalog-reader - namespace: {{ .Values.catalog.namespace }} + namespace: {{ .Values.catalog.namespace | default .Release.Namespace }} labels: - {{- include "durable-agents.labels" . | nindent 4 }} + {{- include "temporal-engine.labels" . | nindent 4 }} rules: - apiGroups: ["core.controller-agent.dev"] resources: ["tools", "skills", "agents"] @@ -24,25 +24,25 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ .Release.Name }}-catalog-reader - namespace: {{ .Values.catalog.namespace }} + namespace: {{ .Values.catalog.namespace | default .Release.Namespace }} labels: - {{- include "durable-agents.labels" . | nindent 4 }} + {{- include "temporal-engine.labels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ .Release.Name }}-catalog-reader subjects: - kind: ServiceAccount - name: {{ .Release.Name }}-catalog-sync + name: {{ .Release.Name }}-temporal-engine-catalog-sync namespace: {{ .Release.Namespace }} --- {{- if .Values.catalogSync.image.tag }} apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Release.Name }}-catalog-sync + name: {{ .Release.Name }}-temporal-engine-catalog-sync labels: - {{- include "durable-agents.labels" . | nindent 4 }} + {{- include "temporal-engine.labels" . | nindent 4 }} app.kubernetes.io/component: catalog-sync spec: replicas: 1 @@ -54,10 +54,10 @@ spec: template: metadata: labels: - {{- include "durable-agents.labels" . | nindent 8 }} + {{- include "temporal-engine.labels" . | nindent 8 }} app.kubernetes.io/component: catalog-sync spec: - serviceAccountName: {{ .Release.Name }}-catalog-sync + serviceAccountName: {{ .Release.Name }}-temporal-engine-catalog-sync securityContext: runAsNonRoot: true runAsUser: 65532 @@ -75,8 +75,8 @@ spec: drop: ["ALL"] env: - name: CATALOG_NAMESPACE - value: {{ .Values.catalog.namespace | quote }} - {{- include "durable-agents.qdrantEnv" . | nindent 12 }} + value: {{ .Values.catalog.namespace | default .Release.Namespace | quote }} + {{- include "temporal-engine.qdrantEnv" . | nindent 12 }} - name: OPENAI_BASE_URL value: {{ .Values.llm.baseURL | quote }} - name: OPENAI_EMBED_MODEL diff --git a/engines/temporal/charts/durable-agents/templates/gateway-deployment.yaml b/charts/agent-controller/charts/temporal-engine/templates/gateway-deployment.yaml similarity index 53% rename from engines/temporal/charts/durable-agents/templates/gateway-deployment.yaml rename to charts/agent-controller/charts/temporal-engine/templates/gateway-deployment.yaml index c6409cd6..ac6a82a0 100644 --- a/engines/temporal/charts/durable-agents/templates/gateway-deployment.yaml +++ b/charts/agent-controller/charts/temporal-engine/templates/gateway-deployment.yaml @@ -2,9 +2,9 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Release.Name }}-gateway + name: {{ .Release.Name }}-temporal-engine-gateway labels: - {{- include "durable-agents.labels" . | nindent 4 }} + {{- include "temporal-engine.labels" . | nindent 4 }} app.kubernetes.io/component: gateway spec: replicas: {{ .Values.gateway.replicas }} @@ -16,9 +16,10 @@ spec: template: metadata: labels: - {{- include "durable-agents.labels" . | nindent 8 }} + {{- include "temporal-engine.labels" . | nindent 8 }} app.kubernetes.io/component: gateway spec: + serviceAccountName: {{ .Release.Name }}-temporal-engine-gateway securityContext: runAsNonRoot: true runAsUser: 65532 @@ -35,7 +36,7 @@ spec: capabilities: drop: ["ALL"] env: - {{- include "durable-agents.temporalEnv" . | nindent 12 }} + {{- include "temporal-engine.temporalEnv" . | nindent 12 }} - name: GATEWAY_ADDR value: ":8080" {{- with .Values.gateway.identity.staticIdentities }} @@ -59,6 +60,37 @@ spec: name: {{ .Values.callback.secretName }} key: {{ .Values.callback.secretKey }} {{- end }} + {{- with .Values.gateway.senderAssertion.secretName }} + # Signed sender assertion (docs/adr/0030 §6). With this set, a sender + # login is accepted ONLY from a verified assertion and the unsigned + # body field is ignored entirely — which is also how + # agent-orchestrator vouches for a login when it forwards a turn + # here, rather than the hop being trusted for being internal. Unset + # is a supported weaker mode that both ends announce at startup. + - name: GATEWAY_SENDER_ASSERTION_SECRET + valueFrom: + secretKeyRef: + name: {{ . }} + key: {{ $.Values.gateway.senderAssertion.secretKey }} + {{- end }} + # The gateway watches IntegrationRoute CRs for deterministic + # dispatch, so it needs to know where the catalog lives. + - name: CATALOG_NAMESPACE + value: {{ .Values.catalog.namespace | default .Release.Namespace | quote }} + {{- if .Values.qdrant.host }} + # Caller-supplied tools (docs/adr/0035) are ranked here, in their own + # collection. + {{- include "temporal-engine.qdrantEnv" . | nindent 12 }} + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.llm.secretName }} + key: OPENAI_API_KEY + - name: OPENAI_BASE_URL + value: {{ .Values.llm.baseURL | quote }} + - name: OPENAI_EMBED_MODEL + value: {{ .Values.llm.embedModel | quote }} + {{- end }} ports: - name: http containerPort: 8080 diff --git a/engines/temporal/charts/durable-agents/templates/gateway-service.yaml b/charts/agent-controller/charts/temporal-engine/templates/gateway-service.yaml similarity index 77% rename from engines/temporal/charts/durable-agents/templates/gateway-service.yaml rename to charts/agent-controller/charts/temporal-engine/templates/gateway-service.yaml index 9cd51bec..fbbdb9f3 100644 --- a/engines/temporal/charts/durable-agents/templates/gateway-service.yaml +++ b/charts/agent-controller/charts/temporal-engine/templates/gateway-service.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: {{ .Release.Name }}-gateway + name: {{ .Release.Name }}-temporal-engine-gateway labels: - {{- include "durable-agents.labels" . | nindent 4 }} + {{- include "temporal-engine.labels" . | nindent 4 }} app.kubernetes.io/component: gateway spec: type: {{ .Values.gateway.service.type }} diff --git a/charts/agent-controller/charts/temporal-engine/templates/rbac.yaml b/charts/agent-controller/charts/temporal-engine/templates/rbac.yaml new file mode 100644 index 00000000..2462f09b --- /dev/null +++ b/charts/agent-controller/charts/temporal-engine/templates/rbac.yaml @@ -0,0 +1,114 @@ +{{- $catalogNs := .Values.catalog.namespace | default .Release.Namespace }} +{{- if .Values.toolrun.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Release.Name }}-temporal-engine-worker + labels: + {{- include "temporal-engine.labels" . | nindent 4 }} +--- +# The worker creates ToolRun and AgentRun CRs and reads their mirrored status — +# never Jobs directly, which the core-controller owns. Same shape as +# agent-orchestrator's own grants, so enabling this engine does not widen the +# release's blast radius; it moves an equivalent one to a second workload. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-temporal-engine-runner + namespace: {{ $catalogNs }} + labels: + {{- include "temporal-engine.labels" . | nindent 4 }} +rules: + - apiGroups: ["core.controller-agent.dev"] + resources: ["toolruns"] + verbs: ["create", "get"] + {{- if .Values.nats.url }} + # Bridged pod agents (docs/pod-agents.md) launch the ordinary AgentRun an + # unmodified upstream agent expects. Only granted when the bridge is enabled. + - apiGroups: ["core.controller-agent.dev"] + resources: ["agentruns"] + verbs: ["create", "get"] + {{- end }} + # Per-run credential Secrets (docs/adr/0030 §3, engines/temporal internal/authz). + # + # This grant is what keeps a credential out of Temporal's event history: the + # authorization pre-flight writes resolved values here and returns only the + # object's NAME, so nothing a workflow can see holds credential material. The + # launcher references the Secret and the kubelet is the only reader. + - apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "get", "update", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Release.Name }}-temporal-engine-runner + namespace: {{ $catalogNs }} + labels: + {{- include "temporal-engine.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Release.Name }}-temporal-engine-runner +subjects: + - kind: ServiceAccount + name: {{ .Release.Name }}-temporal-engine-worker + namespace: {{ .Release.Namespace }} +--- +# Cluster-internal service for the callback listener; tool Jobs post here. +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-temporal-engine-gateway-callback + labels: + {{- include "temporal-engine.labels" . | nindent 4 }} + app.kubernetes.io/component: gateway +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: gateway + ports: + - name: callback + port: 8081 + targetPort: callback +{{- end }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Release.Name }}-temporal-engine-gateway + labels: + {{- include "temporal-engine.labels" . | nindent 4 }} +--- +# The gateway watches IntegrationRoute CRs for deterministic event dispatch +# (docs/adr/0024). Read-only, and the same shape agent-orchestrator already has +# for the same resource. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-temporal-engine-route-reader + namespace: {{ $catalogNs }} + labels: + {{- include "temporal-engine.labels" . | nindent 4 }} +rules: + - apiGroups: ["core.controller-agent.dev"] + resources: ["integrationroutes"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Release.Name }}-temporal-engine-route-reader + namespace: {{ $catalogNs }} + labels: + {{- include "temporal-engine.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Release.Name }}-temporal-engine-route-reader +subjects: + - kind: ServiceAccount + name: {{ .Release.Name }}-temporal-engine-gateway + namespace: {{ .Release.Namespace }} diff --git a/engines/temporal/charts/durable-agents/templates/worker-deployment.yaml b/charts/agent-controller/charts/temporal-engine/templates/worker-deployment.yaml similarity index 58% rename from engines/temporal/charts/durable-agents/templates/worker-deployment.yaml rename to charts/agent-controller/charts/temporal-engine/templates/worker-deployment.yaml index e6c9cc11..f83e0ff4 100644 --- a/engines/temporal/charts/durable-agents/templates/worker-deployment.yaml +++ b/charts/agent-controller/charts/temporal-engine/templates/worker-deployment.yaml @@ -2,9 +2,9 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Release.Name }}-worker + name: {{ .Release.Name }}-temporal-engine-worker labels: - {{- include "durable-agents.labels" . | nindent 4 }} + {{- include "temporal-engine.labels" . | nindent 4 }} app.kubernetes.io/component: worker spec: replicas: {{ .Values.worker.replicas }} @@ -16,11 +16,11 @@ spec: template: metadata: labels: - {{- include "durable-agents.labels" . | nindent 8 }} + {{- include "temporal-engine.labels" . | nindent 8 }} app.kubernetes.io/component: worker spec: {{- if .Values.toolrun.enabled }} - serviceAccountName: {{ .Release.Name }}-worker + serviceAccountName: {{ .Release.Name }}-temporal-engine-worker {{- end }} securityContext: runAsNonRoot: true @@ -38,7 +38,7 @@ spec: capabilities: drop: ["ALL"] env: - {{- include "durable-agents.temporalEnv" . | nindent 12 }} + {{- include "temporal-engine.temporalEnv" . | nindent 12 }} - name: OPENAI_BASE_URL value: {{ .Values.llm.baseURL | quote }} - name: OPENAI_MODEL @@ -46,20 +46,40 @@ spec: - name: OPENAI_EMBED_MODEL value: {{ .Values.llm.embedModel | quote }} {{- if .Values.qdrant.host }} - {{- include "durable-agents.qdrantEnv" . | nindent 12 }} + {{- include "temporal-engine.qdrantEnv" . | nindent 12 }} {{- end }} {{- if .Values.toolrun.enabled }} - name: TOOLRUN_MODE value: "k8s" - name: TOOLRUN_NAMESPACE - value: {{ .Values.catalog.namespace | quote }} + value: {{ .Values.catalog.namespace | default .Release.Namespace | quote }} + - name: CATALOG_NAMESPACE + value: {{ .Values.catalog.namespace | default .Release.Namespace | quote }} - name: CALLBACK_BASE_URL - value: {{ include "durable-agents.callbackBaseURL" . | quote }} + value: {{ include "temporal-engine.callbackBaseURL" . | quote }} - name: CALLBACK_SECRET_NAME value: {{ .Values.callback.secretName | quote }} - name: CALLBACK_SECRET_KEY value: {{ .Values.callback.secretKey | quote }} {{- end }} + {{- with .Values.nats.url }} + # Bridged pod agents: an unmodified AgentRun over the existing + # protocol (docs/pod-agents.md). Absent leaves the declarative loop + # and checkpoint-resume step tools. + - name: AGENT_NATS_URL + value: {{ . | quote }} + {{- end }} + {{- with .Values.identityLink.gatewayUrl }} + - name: IDENTITY_LINK_GATEWAY_URL + value: {{ . | quote }} + {{- end }} + {{- with .Values.identityLink.tokenSecretName }} + - name: IDENTITY_LINK_GATEWAY_TOKEN + valueFrom: + secretKeyRef: + name: {{ . }} + key: {{ $.Values.identityLink.tokenSecretKey }} + {{- end }} - name: OPENAI_API_KEY valueFrom: secretKeyRef: diff --git a/charts/agent-controller/charts/temporal-engine/values.yaml b/charts/agent-controller/charts/temporal-engine/values.yaml new file mode 100644 index 00000000..b981ef39 --- /dev/null +++ b/charts/agent-controller/charts/temporal-engine/values.yaml @@ -0,0 +1,124 @@ +# The Temporal agent engine (engines/temporal), off by default. +# +# agent-orchestrator runs its in-process LangGraph loop unless +# AGENT_ENGINE=temporal selects this instead (docs/adr/0036). Enabling this +# subchart alone changes nothing: the orchestrator has to be told to use it. + +# A Temporal cluster is assumed to exist — this takes its address rather than +# bundling a server, so enabling the engine adds no stateful component. +temporal: + # Matches the temporalio/temporal chart's frontend Service. + address: temporal-frontend.temporal.svc:7233 + namespace: default + +taskQueue: agent-controller + +llm: + baseURL: https://api.openai.com/v1 + model: gpt-4o-2024-08-06 + embedModel: text-embedding-3-small + # Secret holding OPENAI_API_KEY. Defaults to the orchestrator's own secret so + # one key serves both engines rather than being duplicated per engine. + secretName: agent-controller-agent-orchestrator + +# Qdrant backs the catalog RAG index. Leave host empty to disable retrieval +# (plain-conversation mode). +qdrant: + host: "" + port: 6334 + # REQUIRED when sharing a Qdrant instance with agent-orchestrator. Both index + # tools/skills/agents, and their payload schemas differ — sharing a collection + # would have each engine reading records it cannot decode. A prefix keeps them + # side by side in one instance instead. + collectionPrefix: te- + +# Where the Tool/Skill/Agent CRs live, and where ToolRun/AgentRun CRs are +# created so tool Jobs run next to their definitions. +catalog: + namespace: "" # empty -> the release namespace + +toolrun: + enabled: true + +callback: + # Secret holding the HMAC key. Two copies of the same value are needed when + # catalog.namespace differs from the release namespace: the gateway verifies + # signatures, and the controller injects the key into tool Jobs, which sign. + secretName: agent-controller-callback + secretKey: AGENT_CALLBACK_SECRET + # Empty derives the in-cluster Service URL for this release. + baseURL: "" + +# Bridged pod agents (docs/pod-agents.md): drives an UNMODIFIED AgentRun over +# the existing bidirectional NATS protocol, so claude-code-swe-agent and +# opencode-swe-agent run without image changes. Empty disables it, leaving the +# declarative loop and checkpoint-resume step tools. +# +# Point this at the same NATS the orchestrator uses — the agents publish to +# subjects keyed by AgentRun id, so both engines speak to the same place. +nats: + url: "" + +# Per-user credentials (docs/adr/0022/0027/0030). Empty falls back to an +# in-memory dev fake, which is announced at startup and must not be relied on. +identityLink: + gatewayUrl: "" + # Secret + key holding the bearer token for that gateway. + tokenSecretName: "" + tokenSecretKey: GATEWAY_IDENTITY_LINK_TOKEN + +catalogSync: + enabled: true + image: + repository: temporal-engine-catalog-sync + tag: latest + pullPolicy: IfNotPresent + resources: + requests: + cpu: 25m + memory: 48Mi + limits: + memory: 192Mi + +worker: + replicas: 1 + image: + repository: temporal-engine-worker + tag: latest + pullPolicy: IfNotPresent + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi + +gateway: + replicas: 1 + image: + repository: temporal-engine-gateway + tag: latest + pullPolicy: IfNotPresent + service: + type: ClusterIP + port: 8080 + # Shared secret with integration-gateway for the signed sender assertion + # (docs/adr/0030 §6). Also used by agent-orchestrator when it forwards a turn + # here, so an internal hop does not have to trust an unsigned login. Unset is + # a supported, loudly-announced weaker mode. + senderAssertion: + secretName: "" + secretKey: GATEWAY_SENDER_ASSERTION_SECRET + # Dev-grade identity (engines/temporal/internal/rbac): a bearer-token map plus + # an optional fallback. All empty fails closed to zero capabilities. + identity: + # JSON: {"": {"subject": "user:x", "roles": ["reader"]}} + staticIdentities: "" + defaultSubject: "" + defaultRoles: "" + resources: + requests: + cpu: 25m + memory: 32Mi + limits: + memory: 128Mi diff --git a/charts/agent-controller/values.yaml b/charts/agent-controller/values.yaml index 375bad88..7607b59b 100644 --- a/charts/agent-controller/values.yaml +++ b/charts/agent-controller/values.yaml @@ -37,6 +37,29 @@ core-controller: # scripts and docs reference it directly). fullnameOverride: core-controller +# --------------------------------------------------------------------------- +# The Temporal-workflow agent engine (charts/temporal-engine, docs/adr/0036). +# +# OFF by default, and enabling it is TWO steps on purpose: +# 1. temporal-engine.enabled=true -- deploy worker + gateway +# 2. agent-orchestrator.config.agentEngine=temporal +# -- route turns to it +# +# Step 1 alone changes no behaviour, which is what makes the rollout +# reversible: the engine can be deployed, watched, and rolled back before any +# turn depends on it. Everything outside the agent loop (the OpenAI facade, +# /invoke, identity, RBAC, credentials, both launchers) stays in +# agent-orchestrator either way. +# +# Assumes a reachable Temporal cluster; no server is bundled. +# --------------------------------------------------------------------------- +temporal-engine: + enabled: false + # Common overrides live under here, e.g.: + # temporal: { address: temporal-frontend.temporal.svc:7233 } + # qdrant: { host: agent-controller-qdrant, collectionPrefix: te- } + # nats: { url: nats://agent-controller-nats:4222 } + # --------------------------------------------------------------------------- # GitHub Issues adapter (charts/integration-gateway) -- phase 1 of # docs/integrations-gateway.md's conversational path. Off by default: enable diff --git a/docs/temporal-engine-pr-sequence.md b/docs/temporal-engine-pr-sequence.md new file mode 100644 index 00000000..68693948 --- /dev/null +++ b/docs/temporal-engine-pr-sequence.md @@ -0,0 +1,122 @@ +# Landing the Temporal engine: the PR sequence + +> Working notes for [ADR 0036](adr/0036-temporal-execution-engine.md). Delete +> this file once the sequence has landed. + +Five PRs, in order. The first four exist as branches; the fifth deliberately +does not — see below. + +Nothing changes behaviour until PR 5. PRs 2–4 add code and configuration that +sit inert behind `AGENT_ENGINE=langgraph` (the default) and +`temporal-engine.enabled=false` (also the default). + +| # | Branch | Base | Changes behaviour? | +| - | ------ | ---- | ------------------ | +| 1 | `pr/1-adr-temporal-engine` | `main` | No — docs only | +| 2 | `pr/2-import-engine` | `main` | No | +| 3 | `pr/3-engine-switch` | PR 2 | No (default unchanged) | +| 4 | `pr/4-engine-chart` | PR 3 | No (subchart off) | +| 5 | — not written — | PR 4 | **Yes** | + +--- + +## PR 1 — ADR 0036, on its own + +**`pr/1-adr-temporal-engine`** · 1 commit · 2 files, +181 + +Docs only, and deliberately **not** in the dependency chain: it can be reviewed +and merged in parallel with, or instead of, everything below. + +That ordering is a change from the original plan, which put the ADR fourth +alongside the e2e run. Putting it first means the design can be rejected before +anyone reads 17,000 lines of Go. If the answer is "we don't want a second agent +loop", this is the only PR that needed to exist. + +CI skips this PR entirely (`paths-ignore: **/*.md`), which is correct. + +## PR 2 — Import the engine + +**`pr/2-import-engine`** · base `main` · 108 files, +17,396 + +The engine arrives by `git subtree`, so its seven milestone commits and its own +two ADRs are real history rather than one opaque drop — `git log engines/temporal` +reads as what it is. Also: module renamed to this repo's convention, a CI job, +and three images in the release matrix. + +**Touches no TypeScript at all**, so the LangGraph path cannot be affected. Worth +saying in the PR description, because it is the cheapest thing for a reviewer to +verify and it makes the rest of the review lower-stakes. + +Reviewing 17k lines is not the ask. The ask is: does this belong in the repo, is +it wired into CI correctly, and is the history intact. The code itself was +reviewed as it was written (see its own ADRs) and its tests run in CI from this +PR forward. + +## PR 3 — The `AGENT_ENGINE` switch + +**`pr/3-engine-switch`** · base PR 2 · 6 files, +439 + +`AGENT_ENGINE=langgraph|temporal`, defaulting to `langgraph`. `AgentGraphLike` +was already the Server's dependency, so this is a second implementation of an +existing interface, not a refactor — **581 existing tests pass unchanged**, which +is the claim to check first. + +The substantive review is in `engine/temporal-engine.ts`: it is an HTTP client +rather than an embedded Temporal client (three reasons, documented at the top of +the file), it signs the sender login rather than trusting an internal hop, and it +names an already-matched route target rather than having the engine re-derive it. + +## PR 4 — The subchart + +**`pr/4-engine-chart`** · base PR 3 · 20 files + +`temporal-engine` subchart, `enabled: false`. Takes a Temporal address; bundles +no server. + +The part worth actual scrutiny is the RBAC, because the imported chart's grants +predated the catch-up work: `secrets` (the grant that keeps credentials out of +Temporal's event history), `agentruns` (only when the NATS bridge is enabled), +and `integrationroutes` on a new gateway ServiceAccount. Each mirrors a grant +agent-orchestrator already holds. + +Also adds the first CI job that renders `charts/agent-controller` at all. + +## PR 5 — Flip the default + +**Not written, on purpose.** + +Its precondition is the e2e suite run whole under `AGENT_ENGINE=temporal` +(needs minikube, a reachable Temporal, and the images built). Writing a +"flip the default" PR before that evidence exists would be asserting a result +nobody has. + +Two expectations, judged differently: + +- Everything else must pass **unchanged**. A failure is a real parity gap. +- `resilience` and `rollout-recovery` should **change for the better**. They + encode losses that stop occurring — ADR 0033's "the interrupted turn itself is + still lost" among them — so they need re-baselining rather than passing as + written. That re-baselining is the evidence, not a workaround. + +Also unverified until CI runs: the three engine images have never been built (no +Docker daemon in the environment they were prepared in). Contexts are +self-contained and `go build ./...` passes natively, but PR 2's first CI run is +what proves the Dockerfiles. + +--- + +## Three findings worth acting on independently + +Each stands on its own merits whether or not the engine is ever enabled: + +1. **The identity gate is skipped for sub-agent tool calls** on the LangGraph + path. ADR 0032 §5 gates the planner's `runTool`, but `dispatchResolvedTool` + has no equivalent — so a Tool meant to act as a specific human runs with + whatever static token its template carries. Fixed in the engine; worth fixing + here too. +2. **No CI job renders `charts/agent-controller`.** A broken template in any of + its four subcharts ships silently. PR 4 covers the new one; the other three + remain unguarded. +3. **The two engines' Qdrant payload schemas differ**, so they must never share a + collection. Hence the `te-` prefix default — worth knowing before anyone + points both at one instance. diff --git a/engines/temporal/Makefile b/engines/temporal/Makefile index bb24125c..b1cff960 100644 --- a/engines/temporal/Makefile +++ b/engines/temporal/Makefile @@ -1,23 +1,8 @@ -.PHONY: build test vet docker helm-lint ecr-push +.PHONY: build test vet fmt docker -# Local build-and-push to the platform's ECR (no CI: this repo is private/ -# local, unlike agent-controller which the platform pipeline builds). The -# ECR repos themselves are platform-managed (Crossplane, created by the -# durable-agents Argo app) — merge the platform PR first, then push. -AWS_PROFILE ?= platform -ECR_ACCOUNT ?= 486491621059 -ECR_REGION ?= us-east-1 -ECR := $(ECR_ACCOUNT).dkr.ecr.$(ECR_REGION).amazonaws.com -TAG ?= $(shell git rev-parse --short=12 HEAD) - -ecr-push: - aws --profile $(AWS_PROFILE) ecr get-login-password --region $(ECR_REGION) | docker login --username AWS --password-stdin $(ECR) - for app in gateway worker catalog-sync; do \ - docker build --platform linux/amd64 -f Dockerfile.$$app -t $(ECR)/durable-agents-$$app:$(TAG) . || exit 1; \ - docker push $(ECR)/durable-agents-$$app:$(TAG) || exit 1; \ - done - @echo "" - @echo "pushed tag: $(TAG) → set gateway/worker/catalogSync image.tag in gitops/durable-agents/values.yaml" +# Local checks. CI runs the same four (see .github/workflows/ci.yml's +# temporal-engine job); images are built by release.yml from this directory as +# their context. build: go build ./... @@ -28,10 +13,10 @@ test: vet: go vet ./... -docker: - docker build -f Dockerfile.worker -t durable-agents-worker:latest . - docker build -f Dockerfile.gateway -t durable-agents-gateway:latest . - docker build -f Dockerfile.catalog-sync -t durable-agents-catalog-sync:latest . +fmt: + gofmt -w . -helm-lint: - helm lint charts/durable-agents +docker: + docker build -f Dockerfile.worker -t temporal-engine-worker:latest . + docker build -f Dockerfile.gateway -t temporal-engine-gateway:latest . + docker build -f Dockerfile.catalog-sync -t temporal-engine-catalog-sync:latest . diff --git a/engines/temporal/README.md b/engines/temporal/README.md index b8e721b9..00ef5607 100644 --- a/engines/temporal/README.md +++ b/engines/temporal/README.md @@ -1,4 +1,4 @@ -# durable-agents +# temporal-engine > Setup: [setup-instructions.md](setup-instructions.md) — local dev with > zero cluster, and the full k3s deployment checklist. @@ -31,7 +31,7 @@ design and milestone plan. | `internal/callertools` | Consumer-supplied tools over the OpenAI facade (upstream ADR 0035). | | `internal/temporal/activities` | All non-deterministic work (LLM calls; later: Qdrant, ToolRun CRs, identity). | | `internal/llm` | Minimal OpenAI-compatible chat client (base URL overridable). | -| `charts/durable-agents` | Helm chart: gateway + worker. Assumes Temporal is already installed. | +| `charts/agent-controller/charts/temporal-engine` | Helm subchart (repo root): worker, gateway, catalog-sync. Off by default; takes a Temporal address rather than bundling a server. | ## How a turn flows @@ -140,23 +140,30 @@ make build test vet # checks make docker # build both images ``` -## Deploying (k3s) +## Deploying -Assumes a Temporal cluster (e.g. the `temporalio/temporal` chart) is -installed and reachable at `temporal.address`. +Ships as a subchart of the umbrella `agent-controller` chart, off by default. +Enabling it is deliberately two steps, so the engine can be deployed and +watched before any turn depends on it: -```bash -kubectl create namespace durable-agents -kubectl -n durable-agents create secret generic durable-agents-secrets \ - --from-literal=OPENAI_API_KEY= +```yaml +# charts/agent-controller/values.yaml (or your overlay) +temporal-engine: + enabled: true # 1. deploy worker + gateway + temporal: { address: temporal-frontend.temporal.svc:7233 } + qdrant: { host: agent-controller-qdrant, collectionPrefix: te- } -helm install durable-agents charts/durable-agents -n durable-agents \ - --set temporal.address=temporal-frontend.temporal.svc:7233 +agent-orchestrator: + config: + agentEngine: temporal # 2. route turns to it ``` -Point an OpenAI-compatible client (e.g. Open WebUI, with -`ENABLE_FORWARD_USER_INFO_HEADERS=true` for session continuity) at the -gateway service. +Step 1 alone changes no behaviour. Assumes a reachable Temporal cluster; no +server is bundled, so enabling this adds no stateful component. + +Consumers keep talking to agent-orchestrator either way — its OpenAI facade, +`/invoke`, identity resolution, RBAC and credential store are shared by both +engines. See [docs/adr/0002](docs/adr/0002-upstream-integration.md). ## Roadmap diff --git a/engines/temporal/charts/durable-agents/Chart.yaml b/engines/temporal/charts/durable-agents/Chart.yaml deleted file mode 100644 index b9735472..00000000 --- a/engines/temporal/charts/durable-agents/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -name: durable-agents -description: Temporal-workflow-based AI agents — gateway + worker. Assumes a Temporal cluster is already installed (e.g. via temporalio/temporal helm chart). -type: application -version: 0.1.0 -appVersion: "0.1.0" diff --git a/engines/temporal/charts/durable-agents/templates/onepassworditems.yaml b/engines/temporal/charts/durable-agents/templates/onepassworditems.yaml deleted file mode 100644 index ffde77b7..00000000 --- a/engines/temporal/charts/durable-agents/templates/onepassworditems.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{- range .Values.onePasswordItems }} -apiVersion: onepassword.com/v1 -kind: OnePasswordItem -metadata: - name: {{ .name }} - namespace: {{ $.Release.Namespace }} - labels: - {{- include "durable-agents.labels" $ | nindent 4 }} - annotations: - operator.1password.io/auto-restart: "true" -spec: - itemPath: {{ .itemPath | quote }} ---- -{{- end }} diff --git a/engines/temporal/charts/durable-agents/templates/toolrun-rbac.yaml b/engines/temporal/charts/durable-agents/templates/toolrun-rbac.yaml deleted file mode 100644 index 1bb6145c..00000000 --- a/engines/temporal/charts/durable-agents/templates/toolrun-rbac.yaml +++ /dev/null @@ -1,57 +0,0 @@ -{{- if .Values.toolrun.enabled }} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ .Release.Name }}-worker - labels: - {{- include "durable-agents.labels" . | nindent 4 }} ---- -# The worker creates ToolRun CRs and reads their mirrored status — never -# Jobs directly (the core-controller owns batch/jobs). -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ .Release.Name }}-toolrun-writer - namespace: {{ .Values.catalog.namespace }} - labels: - {{- include "durable-agents.labels" . | nindent 4 }} -rules: - - apiGroups: ["core.controller-agent.dev"] - resources: ["toolruns"] - verbs: ["create", "get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ .Release.Name }}-toolrun-writer - namespace: {{ .Values.catalog.namespace }} - labels: - {{- include "durable-agents.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ .Release.Name }}-toolrun-writer -subjects: - - kind: ServiceAccount - name: {{ .Release.Name }}-worker - namespace: {{ .Release.Namespace }} ---- -# Cluster-internal service for the callback listener; tool Jobs post here. -apiVersion: v1 -kind: Service -metadata: - name: {{ .Release.Name }}-gateway-callback - labels: - {{- include "durable-agents.labels" . | nindent 4 }} - app.kubernetes.io/component: gateway -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: {{ .Chart.Name }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/component: gateway - ports: - - name: callback - port: 8081 - targetPort: callback -{{- end }} diff --git a/engines/temporal/charts/durable-agents/values.yaml b/engines/temporal/charts/durable-agents/values.yaml deleted file mode 100644 index 57aa48f3..00000000 --- a/engines/temporal/charts/durable-agents/values.yaml +++ /dev/null @@ -1,109 +0,0 @@ -temporal: - # Matches the temporalio/temporal chart's frontend service. - address: temporal-frontend.temporal.svc:7233 - namespace: default - -taskQueue: durable-agents - -llm: - baseURL: https://api.openai.com/v1 - model: gpt-4o-2024-08-06 - embedModel: text-embedding-3-small - # Secret must contain an OPENAI_API_KEY key. Create it out of band: - # kubectl -n create secret generic durable-agents-secrets \ - # --from-literal=OPENAI_API_KEY= - secretName: durable-agents-secrets - -# Qdrant backs the catalog RAG index. Leave host empty to disable retrieval -# (plain-conversation mode). Point at your own instance or the qdrant chart. -qdrant: - host: "" - port: 6334 - # Namespaces the collections (prefix+tools/skills/agents). REQUIRED when - # sharing a Qdrant instance with another indexer (e.g. the upstream - # agent-orchestrator) — payload schemas differ, never share collections. - collectionPrefix: "" - -# Where agent-controller's Tool/Skill/Agent CRs live (also where ToolRun CRs -# are created, so tool Jobs run next to their Tool definitions). -catalog: - namespace: controller-agent - -# Tool execution (milestone 3): the worker creates ToolRun CRs; tool Jobs -# post HMAC-signed events to the gateway's callback listener, which signals -# the waiting workflow. -toolrun: - enabled: true - -callback: - # Secret holding the HMAC key under key AGENT_CALLBACK_SECRET. Two copies - # of the same value are needed — one in the release namespace (gateway - # verifies) and one in catalog.namespace (the controller injects it into - # tool Jobs, which sign): - # SECRET=$(openssl rand -hex 32) - # kubectl -n create secret generic durable-agents-callback \ - # --from-literal=AGENT_CALLBACK_SECRET="$SECRET" - # kubectl -n create secret generic durable-agents-callback \ - # --from-literal=AGENT_CALLBACK_SECRET="$SECRET" - secretName: durable-agents-callback - secretKey: AGENT_CALLBACK_SECRET - # Callback URL base as reachable from tool Job pods. Empty derives - # http://-gateway-callback..svc:8081 - baseURL: "" - -# Optional 1Password-operator syncs (platforms running onepassword-connect): -# each entry becomes a OnePasswordItem CR materializing a k8s Secret named -# `name` from the vault item at `itemPath` (item field names become keys). -onePasswordItems: [] -# - name: durable-agents-callback -# itemPath: vaults/bitovi-platform/items/durable-agents-callback - -catalogSync: - enabled: true - image: - repository: durable-agents-catalog-sync - tag: latest - pullPolicy: IfNotPresent - resources: - requests: - cpu: 25m - memory: 48Mi - limits: - memory: 192Mi - -worker: - replicas: 1 - image: - repository: durable-agents-worker - tag: latest - pullPolicy: IfNotPresent - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - memory: 256Mi - -gateway: - replicas: 1 - image: - repository: durable-agents-gateway - tag: latest - pullPolicy: IfNotPresent - service: - type: ClusterIP - port: 8080 - # Dev-grade identity (see internal/rbac): a bearer-token map plus an - # optional fallback for tokenless callers. Leave all empty to fail closed - # to zero capabilities. - identity: - # JSON: {"": {"subject": "user:x", "roles": ["reader"]}} - staticIdentities: "" - defaultSubject: "" - defaultRoles: "" - resources: - requests: - cpu: 25m - memory: 32Mi - limits: - memory: 128Mi diff --git a/engines/temporal/docs/platform/README.md b/engines/temporal/docs/platform/README.md deleted file mode 100644 index 0b947f7b..00000000 --- a/engines/temporal/docs/platform/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# Standing up on bitovi-platform-services - -The platform (`~/developer/bitovi-platform-services`) already runs -everything durable-agents depends on, all ArgoCD-managed: - -| Dependency | Where it already is | -| ---------- | ------------------- | -| agent-controller (CRDs, core-controller, tool images) | `gitops/apps/agent-controller.yaml`, ns `agent-controller`, wave 40 | -| Catalog CRs (recipe-scraper Tool, recipe skill, opencode Agent) | `gitops/apps/agent-catalog.yaml`, wave 42; roles are `reader`/`writer` | -| Temporal (+ CNPG Postgres, `default` ns registered) | `gitops/apps/temporal.yaml`, `temporal-frontend.temporal.svc:7233`, wave 30 | -| Qdrant | `gitops/apps/agent-deps.yaml` → service `agent-qdrant`, wave 41 | -| Secrets machinery | 1Password Connect operator; `agent-orchestrator-secrets` already holds `OPENAI_API_KEY` in ns `agent-controller` | -| Chat front-end | Open WebUI (cluster-internal), bearer `bitovi-openwebui-internal`, forwards chat-id headers | - -durable-agents deploys **beside** the upstream agent-orchestrator, not -instead of it — same namespace, same controller, same catalog: - -- **Qdrant collections are prefixed `da-`** (`qdrant.collectionPrefix`). - The upstream orchestrator owns `tools`/`skills`/`agents` with a different - payload schema; sharing collections would corrupt retrieval for both. -- **ToolRuns coexist**: upstream creates NATS-mode ToolRuns, ours are - HTTP-callback-mode — the core-controller supports both per-CR. -- Same identity token/roles as Open WebUI already uses, so the same chat - UI can drive either backend. - -## Steps - -The ordering matters: the platform owns the ECR repos (Crossplane, created -by the Argo app), and the chart's Deployments are **gated on image.tag** — -so the first merge deploys everything dormant, then images are pushed, then -a tag bump activates it. agent-controller's images build in the pipeline -because that source is public; durable-agents is private/local, so images -build and push from your machine. - -### 1. 1Password item - -Create item `durable-agents-callback` in the `bitovi-platform` vault with a -single field named `AGENT_CALLBACK_SECRET` (value: `openssl rand -hex 32`). -The chart's `onePasswordItems` entry syncs it into the namespace; both the -gateway and the tool Jobs' `secretRef` read this one Secret (everything is -in `agent-controller`, so no dual-namespace copy needed here). - -### 2. PR #1 to bitovi-platform-services (dormant deploy + ECR repos) - -1. Copy `charts/durable-agents/` from this repo into - `bitovi-platform-services/charts/durable-agents/`. -2. Copy `docs/platform/durable-agents-values.yaml` to - `gitops/durable-agents/values.yaml` — leave the three `image.tag` - values empty. -3. Copy `docs/platform/durable-agents-app.yaml` to - `gitops/apps/durable-agents.yaml`. -4. Merge → ArgoCD syncs (wave 43): ECR repos + services + RBAC + the - callback Secret sync exist; no pods yet (tags empty). - -### 3. Push images - -```bash -cd ~/personal/durable-agents -make ecr-push # aws --profile platform; linux/amd64; prints the tag -``` - -### 4. PR #2: set the tag - -Set the printed tag on all three `image.tag` fields in -`gitops/durable-agents/values.yaml`, merge — Argo rolls the Deployments -out. (Later image updates are the same two commands: `make ecr-push`, bump -the tag.) - -### 5. Verify - -```bash -kubectl -n agent-controller logs deploy/durable-agents-catalog-sync | tail -# → "indexed recipe-scraper", "indexed recipe-extraction", … -kubectl -n agent-controller port-forward svc/durable-agents-gateway 8080:8080 & -curl -s localhost:8080/v1/chat/completions \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer bitovi-openwebui-internal' \ - -H 'X-Session-Id: shakedown-1' \ - -d '{"model":"durable-agents","messages":[{"role":"user","content":"Grab the recipe at for me"}]}' -kubectl -n agent-controller get toolruns # the real recipe-scraper Job -kubectl -n temporal port-forward svc/temporal-web 8081:8080 # watch workflows -``` - -### 6. Point Open WebUI at it (optional, after the curl shakedown) - -Open WebUI supports multiple OpenAI endpoints — add ours alongside the -upstream orchestrator in `gitops/apps/agent-orchestrator.yaml`: - -```yaml -extraEnvVars: - - name: ENABLE_FORWARD_USER_INFO_HEADERS - value: "true" - - name: OPENAI_API_BASE_URLS - value: "http://agent-orchestrator:8081/v1;http://durable-agents-gateway:8080/v1" - - name: OPENAI_API_KEYS - value: "bitovi-openwebui-internal;bitovi-openwebui-internal" -``` - -Both backends then appear as models in the picker (`agent-orchestrator` -vs `durable-agents`) — a live side-by-side of the two architectures. - -## Platform-specific gotchas - -- **Do not share Qdrant collections** (see above) — keep - `collectionPrefix: "da-"`. -- The gateway must stay **cluster-internal** (no ingress): the static - identity resolver is dev-grade, same posture as the upstream orchestrator - and the Temporal UI. -- `agent-catalog`'s Agent CR (opencode) is role `writer`, `tier: - privileged`, and speaks the **NATS agent-runtime protocol** — durable- - agents can't delegate to it until the checkpoint-resume adapter exists - (docs/pod-agents.md). Skill/tool turns (recipe-scraper) work end to end - today. Declarative agents need an Agent CR with no image expectations — - seed one via a new CR in agent-catalog when ready. -- durable-agents registers its own Temporal namespace (agent-controller, 72h - retention via TEMPORAL_NAMESPACE_RETENTION) on startup - closed histories - outlive the platform default namespace 1d retention. diff --git a/engines/temporal/docs/platform/durable-agents-app.yaml b/engines/temporal/docs/platform/durable-agents-app.yaml deleted file mode 100644 index 6bbb9576..00000000 --- a/engines/temporal/docs/platform/durable-agents-app.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# DRAFT — copy to bitovi-platform-services/gitops/apps/durable-agents.yaml -# after vendoring the chart (see docs/platform/README.md). Follows the -# agent-orchestrator pattern: workload band of the agent-controller stack. -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: durable-agents - namespace: argocd - annotations: - # Wave 43 — alongside agent-orchestrator: needs the CRDs (40), Qdrant - # (41), and catalog CRs (42). Runs BESIDE the upstream orchestrator, not - # instead of it: separate Qdrant collections (da- prefix), HTTP-callback - # ToolRuns coexist with its NATS-mode ones under the same controller. - argocd.argoproj.io/sync-wave: "43" - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: platform - sources: - - repoURL: https://github.com/bitovi/bitovi-platform-services.git - targetRevision: main - ref: values - - repoURL: https://github.com/bitovi/bitovi-platform-services.git - targetRevision: main - path: charts/durable-agents - helm: - releaseName: durable-agents - valueFiles: - - $values/gitops/durable-agents/values.yaml - # AWS footprint: the three ECR repos, platform-managed (Crossplane). - # Deployments are gated on image.tag, so first sync creates the repos - # with the workloads dormant; images are built/pushed locally (the - # source repo is private — no CI), then image.tag in - # gitops/durable-agents/values.yaml activates them. eso off: nothing to - # push to a GitHub repo. The conventional OIDC push role is unused. - - repoURL: https://github.com/bitovi/bitovi-platform-services.git - targetRevision: main - path: charts/platform-app-resources - helm: - releaseName: durable-agents-aws - valuesObject: - app: - name: durable-agents-gateway - repo: bitovi/bitovi-platform-services - kind: container - crossplane: - enabled: true - awsXR: true - deletionProtection: true - eso: - enabled: false - ecr: - additionalRepositories: - - durable-agents-worker - - durable-agents-catalog-sync - destination: - server: https://kubernetes.default.svc - namespace: agent-controller - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true diff --git a/engines/temporal/docs/platform/durable-agents-values.yaml b/engines/temporal/docs/platform/durable-agents-values.yaml deleted file mode 100644 index 96c22dd7..00000000 --- a/engines/temporal/docs/platform/durable-agents-values.yaml +++ /dev/null @@ -1,85 +0,0 @@ -# DRAFT — copy to bitovi-platform-services/gitops/durable-agents/values.yaml. -# Platform-specific values for the vendored charts/durable-agents. - -temporal: - # The platform's Temporal (gitops/apps/temporal.yaml). durable-agents - # registers this namespace itself on startup (72h retention). - address: temporal-frontend.temporal.svc:7233 - namespace: agent-controller - -taskQueue: durable-agents - -llm: - baseURL: https://api.openai.com/v1 - model: gpt-4o-2024-08-06 - embedModel: text-embedding-3-small - # Reuse the orchestrator's existing 1Password-synced secret — it already - # carries OPENAI_API_KEY and lives in this namespace. - secretName: agent-orchestrator-secrets - -qdrant: - # agent-deps' Qdrant (wave 41). 6334 = gRPC (the qdrant-helm service - # exposes it alongside HTTP 6333; verify with `kubectl -n agent-controller - # get svc agent-qdrant`). - host: agent-qdrant - port: 6334 - # CRITICAL: the upstream orchestrator owns tools/skills/agents in this - # instance with a different payload schema. Never share collections. - collectionPrefix: "da-" - -# CRs live in the same namespace we deploy into on this platform. -catalog: - namespace: agent-controller - -toolrun: - enabled: true - -callback: - # One secret serves both sides here (gateway verify + Job signing) since - # everything is in the agent-controller namespace. Synced from 1Password - # via onePasswordItems below — create the item first (single field - # AGENT_CALLBACK_SECRET, e.g. `openssl rand -hex 32`). - secretName: durable-agents-callback - secretKey: AGENT_CALLBACK_SECRET - baseURL: "" # derives http://durable-agents-gateway-callback.agent-controller.svc:8081 - -onePasswordItems: - - name: durable-agents-callback - itemPath: vaults/bitovi-platform/items/durable-agents-callback - -gateway: - replicas: 1 - image: - repository: 486491621059.dkr.ecr.us-east-1.amazonaws.com/durable-agents-gateway - tag: "" # SET ME — pushed image tag - pullPolicy: IfNotPresent - service: - type: ClusterIP - port: 8080 - identity: - # Same shared bearer token Open WebUI already sends to the upstream - # orchestrator; roles match the catalog CRs' allowedRoles - # (reader/writer). Cluster-internal only — same posture as upstream. - staticIdentities: '{"bitovi-openwebui-internal":{"subject":"open-webui","roles":["reader","writer"]}}' - defaultSubject: "" - defaultRoles: "" - -worker: - replicas: 1 - image: - repository: 486491621059.dkr.ecr.us-east-1.amazonaws.com/durable-agents-worker - tag: "" # SET ME - pullPolicy: IfNotPresent - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - memory: 512Mi - -catalogSync: - enabled: true - image: - repository: 486491621059.dkr.ecr.us-east-1.amazonaws.com/durable-agents-catalog-sync - tag: "" # SET ME - pullPolicy: IfNotPresent