Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions charts/agent-controller/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions charts/agent-controller/charts/temporal-engine/Chart.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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 }}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
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.
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"]
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand All @@ -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 }}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }}
Expand Down
114 changes: 114 additions & 0 deletions charts/agent-controller/charts/temporal-engine/templates/rbac.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
Loading