Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8a86e3f
feat(discordbot): Discord chat ingress as a clone of slackbotv2
0xdiid Jun 3, 2026
48e337a
build(discordbot): deploy plumbing mirroring slackbotv2 on api-rs
0xdiid Jun 3, 2026
bb12162
fix(chart): allow discordbot → api-rs in the api-rs NetworkPolicy ing…
0xdiid Jun 2, 2026
3c90981
fix(chart): allow discordbot → postgres in the postgres NetworkPolicy
0xdiid Jun 2, 2026
be01503
fix(discordbot): cap gateway listener duration to setTimeout's 32-bit…
0xdiid Jun 2, 2026
8f17518
feat(discordbot): friendlier streaming placeholder ("✨ thinking...")
0xdiid Jun 4, 2026
6e21a23
perf(discordbot): post "✨ thinking..." instantly, run execute in the …
0xdiid Jun 4, 2026
5d83b82
feat(discordbot): absorb upstream slackbotv2 stream-safety fixes
0xdiid Jun 5, 2026
4c91c34
feat(api-rs): serve tools + gerard overlay to agent sandboxes
0xdiid Jun 5, 2026
d94927d
build(discordbot): drop the workspace-wide tslib hoist — no longer ne…
0xdiid Jun 5, 2026
d68cb2d
feat(discordbot): scope session event streams to the execution (#422 …
0xdiid Jun 5, 2026
dbe5b1c
feat(discordbot): absorb slackbotv2's Postgres resilience hardening (…
0xdiid Jun 5, 2026
7f5311c
fix(discordbot): drop details from command-execution task chunks (port)
0xdiid Jun 5, 2026
a23bb65
feat(discordbot): honor plain text render requests (port)
0xdiid Jun 5, 2026
4e9b1fe
feat(discordbot): surface thread starter messages and embeds to the a…
0xdiid Jun 6, 2026
3988bd3
fix(api-rs): stage tools-bootstrap copy outside /app/tools
0xdiid Jun 6, 2026
144e46f
feat(discordbot): split renders into a progress timeline + answer mes…
0xdiid Jun 7, 2026
16b7fb8
feat(discordbot): narrate runs with reactions + reasoning messages
0xdiid Jun 7, 2026
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
12 changes: 9 additions & 3 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ build:
just _build-all-sequential
else
pids=()
for recipe in _build-api _build-api-rs _build-iron-proxy _build-slackbot _build-slackbotv2 _build-agent; do
for recipe in _build-api _build-api-rs _build-iron-proxy _build-slackbot _build-slackbotv2 _build-discordbot _build-agent; do
just "$recipe" &
pids+=("$!")
done
Expand All @@ -48,6 +48,7 @@ _build-all-sequential:
just _build-iron-proxy
just _build-slackbot
just _build-slackbotv2
just _build-discordbot
just _build-agent

build-one service:
Expand All @@ -59,6 +60,7 @@ build-one service:
iron-proxy) just _build-iron-proxy ;;
slackbot) just _build-slackbot ;;
slackbotv2) just _build-slackbotv2 ;;
discordbot) just _build-discordbot ;;
agent|sandbox) just _build-agent ;;
agent-thin|sandbox-thin) just _build-agent-thin ;;
*) echo "unknown service: {{service}}" >&2; exit 2 ;;
Expand All @@ -79,6 +81,9 @@ _build-slackbot:
_build-slackbotv2:
docker build -t centaur-slackbotv2:latest -f services/slackbotv2/Dockerfile .

_build-discordbot:
docker build -t centaur-discordbot:latest -f services/discordbot/Dockerfile .

_build-agent:
docker build --target "{{agent_build_target}}" -t "{{agent_image}}" -f "{{agent_dockerfile}}" .

Expand All @@ -91,7 +96,7 @@ _build-agent-thin:
_push-registry:
#!/usr/bin/env bash
set -euo pipefail
for img in centaur-api centaur-api-rs centaur-iron-proxy centaur-slackbot centaur-slackbotv2 centaur-agent; do
for img in centaur-api centaur-api-rs centaur-iron-proxy centaur-slackbot centaur-slackbotv2 centaur-discordbot centaur-agent; do
target="{{registry}}/library/${img}:latest"
echo "pushing ${img}:latest -> ${target}..."
docker tag "${img}:latest" "${target}"
Expand All @@ -104,7 +109,7 @@ _push-registry:
_import-k3s:
#!/usr/bin/env bash
set -euo pipefail
for img in centaur-api centaur-api-rs centaur-iron-proxy centaur-slackbot centaur-slackbotv2 centaur-agent; do
for img in centaur-api centaur-api-rs centaur-iron-proxy centaur-slackbot centaur-slackbotv2 centaur-discordbot centaur-agent; do
echo "importing ${img}:latest into k3s containerd..."
docker save "${img}:latest" | {{k3s_ctr}} images import -
done
Expand All @@ -126,6 +131,7 @@ deploy:
--set ironProxy.image.repository=ghcr.io/paradigmxyz/centaur/centaur-iron-proxy
--set slackbot.image.repository=ghcr.io/paradigmxyz/centaur/centaur-slackbot
--set slackbotv2.image.repository=ghcr.io/paradigmxyz/centaur/centaur-slackbotv2
--set discordbot.image.repository=ghcr.io/paradigmxyz/centaur/centaur-discordbot
--set sandbox.image.repository=ghcr.io/paradigmxyz/centaur/centaur-agent
)
;;
Expand Down
22 changes: 22 additions & 0 deletions contrib/chart/templates/apirs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,28 @@ spec:
name: {{ include "centaur.secretEnvName" . }}
key: {{ printf "%sIRON_CONTROL_INITIAL_API_KEY" .Values.secretManager.envPrefix }}
{{- end }}
{{- if .Values.toolServer.enabled }}
# Base tools source image (the shared api image). A tools-bootstrap
# init container copies its /app/tools into each sandbox so the agent
# installs the same tool CLI shims api-rs discovers, at the same path.
- name: KUBERNETES_TOOL_SERVER_IMAGE
value: {{ printf "%s:%s" .Values.api.image.repository .Values.api.image.tag | quote }}
- name: KUBERNETES_TOOL_SERVER_IMAGE_PULL_POLICY
value: {{ .Values.api.image.pullPolicy | quote }}
{{- end }}
{{- if .Values.overlay.image.repository }}
# Org overlay (tools/workflows/skills/system-prompt) mounted into
# api-rs sandboxes at overlay.mountPath via an overlay-bootstrap init
# container — the same path api-rs's own TOOL_DIRS points at.
- name: CENTAUR_OVERLAY_IMAGE
value: {{ printf "%s:%s" .Values.overlay.image.repository .Values.overlay.image.tag | quote }}
- name: CENTAUR_OVERLAY_IMAGE_PULL_POLICY
value: {{ .Values.overlay.image.pullPolicy | quote }}
- name: CENTAUR_OVERLAY_IMAGE_SOURCE_PATH
value: {{ .Values.overlay.image.sourcePath | quote }}
- name: CENTAUR_OVERLAY_MOUNT_PATH
value: {{ .Values.overlay.mountPath | quote }}
{{- end }}
{{- range $name, $value := .Values.apiRs.extraEnv }}
- name: {{ $name }}
value: {{ $value | quote }}
Expand Down
125 changes: 125 additions & 0 deletions contrib/chart/templates/discordbot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{{- if .Values.discordbot.enabled }}
{{- $apiRsName := include "centaur.componentName" (dict "root" . "component" "api-rs") -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "centaur.componentName" (dict "root" . "component" "discordbot") }}
labels:
{{ include "centaur.componentLabels" (dict "root" . "component" "discordbot") | nindent 4 }}
spec:
# Exactly one replica: two pods on the same bot token open two Gateway sessions and every message
# is handled twice. Recreate tears the old pod down before the new one logs in, so the token never
# holds two sessions during a rollout. Do NOT add an HPA.
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "discordbot") | nindent 6 }}
template:
metadata:
annotations:
checksum/infra-secrets: {{ include "centaur.infraSecretsChecksum" . }}
labels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "discordbot") | nindent 8 }}
spec:
automountServiceAccountToken: false
# Allow the Gateway to drain and close cleanly on SIGTERM (protects the IDENTIFY budget).
terminationGracePeriodSeconds: 35
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{ toYaml . | nindent 8 }}
{{- end }}
containers:
- name: discordbot
image: {{ printf "%s:%s" .Values.discordbot.image.repository .Values.discordbot.image.tag | quote }}
imagePullPolicy: {{ .Values.discordbot.image.pullPolicy }}
env:
- name: PORT
value: "3001"
- name: CENTAUR_API_URL
value: {{ printf "http://%s:%v" $apiRsName .Values.apiRs.port | quote }}
- name: DISCORD_BOT_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "centaur.secretEnvName" . }}
key: {{ printf "%sDISCORD_BOT_TOKEN" .Values.secretManager.envPrefix }}
- name: DISCORD_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: {{ include "centaur.secretEnvName" . }}
key: {{ printf "%sDISCORD_PUBLIC_KEY" .Values.secretManager.envPrefix }}
- name: DISCORD_APPLICATION_ID
valueFrom:
secretKeyRef:
name: {{ include "centaur.secretEnvName" . }}
key: {{ printf "%sDISCORD_APPLICATION_ID" .Values.secretManager.envPrefix }}
- name: DISCORDBOT_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "centaur.secretEnvName" . }}
key: {{ printf "%sDISCORDBOT_API_KEY" .Values.secretManager.envPrefix }}
# api-rs has no auth middleware yet, but discordbot still sends the key as a header;
# source it from the same infra secret.
- name: CENTAUR_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "centaur.secretEnvName" . }}
key: {{ printf "%sDISCORDBOT_API_KEY" .Values.secretManager.envPrefix }}
- name: DISCORDBOT_DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "centaur.secretEnvName" . }}
key: {{ printf "%sDATABASE_URL" .Values.secretManager.envPrefix }}
- name: DISCORDBOT_USER_NAME
value: {{ .Values.discordbot.userName | quote }}
- name: DISCORDBOT_GUILD_ALLOWLIST
value: {{ .Values.discordbot.guildAllowlist | quote }}
{{- if .Values.discordbot.mentionRoleIds }}
- name: DISCORD_MENTION_ROLE_IDS
value: {{ .Values.discordbot.mentionRoleIds | quote }}
{{- end }}
{{- if not .Values.discordbot.nameThreads }}
- name: DISCORDBOT_NAME_THREADS
value: "false"
{{- end }}
{{- range $name, $value := .Values.discordbot.extraEnv }}
- name: {{ $name }}
value: {{ $value | quote }}
{{- end }}
envFrom:
- secretRef:
name: {{ include "centaur.secretEnvName" . }}
ports:
- containerPort: 3001
name: http
readinessProbe:
httpGet:
path: /health
port: 3001
livenessProbe:
httpGet:
path: /health
port: 3001
# /health reflects the Gateway connection; give the listener time to connect on boot.
initialDelaySeconds: 20
periodSeconds: 15
securityContext:
{{ toYaml .Values.containerSecurityContext | nindent 12 }}
resources:
{{ toYaml .Values.discordbot.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "centaur.componentName" (dict "root" . "component" "discordbot") }}
labels:
{{ include "centaur.componentLabels" (dict "root" . "component" "discordbot") | nindent 4 }}
spec:
selector:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "discordbot") | nindent 4 }}
ports:
- name: http
port: 3001
targetPort: 3001
{{- end }}
61 changes: 61 additions & 0 deletions contrib/chart/templates/networkpolicy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ spec:
matchLabels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "slackbot") | nindent 14 }}
{{- end }}
{{- if .Values.discordbot.enabled }}
- podSelector:
matchLabels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "discordbot") | nindent 14 }}
{{- end }}
{{- if .Values.ironControl.enabled }}
- podSelector:
matchLabels:
Expand Down Expand Up @@ -111,6 +116,11 @@ spec:
- podSelector:
matchLabels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "slackbotv2") | nindent 14 }}
{{- end }}
{{- if .Values.discordbot.enabled }}
- podSelector:
matchLabels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "discordbot") | nindent 14 }}
{{- end }}
# Sandboxes call back into the control plane. agent-k8s labels its pods
# centaur.ai/managed-by=api-rs (MANAGED_BY_VALUE in centaur-sandbox-agent-k8s),
Expand Down Expand Up @@ -221,6 +231,57 @@ spec:
port: 443
---
{{- end }}
{{- if .Values.discordbot.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "centaur.componentName" (dict "root" . "component" "discordbot") }}
labels:
{{ include "centaur.componentLabels" (dict "root" . "component" "discordbot") | nindent 4 }}
spec:
podSelector:
matchLabels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "discordbot") | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
# Only health probes reach discordbot; nothing routes traffic to it (the Gateway is outbound).
- from:
{{- range $ingressSourceNamespaces }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 3001
egress:
{{- if .Values.apiRs.enabled }}
- to:
- podSelector:
matchLabels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "api-rs") | nindent 14 }}
ports:
- protocol: TCP
port: {{ .Values.apiRs.port }}
{{- end }}
{{- if .Values.postgres.enabled }}
- to:
- podSelector:
matchLabels:
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "postgres") | nindent 14 }}
ports:
- protocol: TCP
port: 5432
{{- end }}
# Discord Gateway (wss), REST, and CDN all need direct HTTPS egress. The Gateway cannot be
# proxied (discord.js ignores HTTPS_PROXY), so this rule is load-bearing.
- ports:
- protocol: TCP
port: 443
---
{{- end }}
{{- if .Values.api.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
Expand Down
6 changes: 6 additions & 0 deletions contrib/chart/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ slackbotv2:
image:
pullPolicy: IfNotPresent

discordbot:
# Enable once DISCORD_* secrets + a guild allowlist are configured.
enabled: false
image:
pullPolicy: IfNotPresent

apiRs:
image:
pullPolicy: IfNotPresent
Expand Down
19 changes: 19 additions & 0 deletions contrib/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,25 @@ slackbotv2:
extraEnv: {}
resources: {}

# Discord chat ingress — mirrors slackbotv2, forwards to the api-rs control plane (:8080) over a
# persistent Discord Gateway connection. Off by default; needs a Discord app + Message Content
# Intent + a guild allowlist. Always exactly one replica (singleton Gateway session).
discordbot:
enabled: false
image:
repository: centaur-discordbot
tag: latest
pullPolicy: Always
userName: centaur
# Comma/space-separated Discord guild IDs. The bot is INERT (ignores all messages) until set.
guildAllowlist: ""
# Comma/space-separated role IDs whose mentions also trigger the bot.
mentionRoleIds: ""
# Rename auto-created threads to the triggering message; set false to keep generic names.
nameThreads: true
extraEnv: {}
resources: {}

postgres:
enabled: true
image:
Expand Down
18 changes: 18 additions & 0 deletions contrib/scripts/bootstrap-k8s-secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ if secret_exists centaur-infra-env; then
if [[ -n "${LOCAL_DEV_API_KEY:-}" ]]; then
patch_data+=("\"LOCAL_DEV_API_KEY\":\"$(printf '%s' "$LOCAL_DEV_API_KEY" | base64 | tr -d '\n')\"")
fi
# Discord ingress (discordbot) keys: added when DISCORD_BOT_TOKEN is in the env. DISCORD_* are
# overwritten on each run (so rotation works); DISCORDBOT_API_KEY is generated once if absent.
if [[ -n "${DISCORD_BOT_TOKEN:-}" ]]; then
patch_data+=("\"DISCORD_BOT_TOKEN\":\"$(printf '%s' "$DISCORD_BOT_TOKEN" | base64 | tr -d '\n')\"")
patch_data+=("\"DISCORD_PUBLIC_KEY\":\"$(printf '%s' "${DISCORD_PUBLIC_KEY:-}" | base64 | tr -d '\n')\"")
patch_data+=("\"DISCORD_APPLICATION_ID\":\"$(printf '%s' "${DISCORD_APPLICATION_ID:-}" | base64 | tr -d '\n')\"")
if ! secret_key_present DISCORDBOT_API_KEY; then
patch_data+=("\"DISCORDBOT_API_KEY\":\"$(printf '%s' "${DISCORDBOT_API_KEY:-$(rand_hex)}" | base64 | tr -d '\n')\"")
fi
fi
# iron-control keys: top up only when absent so we never rotate them out from
# under a running pod (its ActiveRecord-encrypted data would become
# undecryptable). Generated values mirror the create path.
Expand Down Expand Up @@ -197,6 +207,14 @@ else
--from-literal=IRON_CONTROL_AR_ENCRYPTION_KEY_DERIVATION_SALT="$(rand_hex)"
--from-literal=IRON_CONTROL_SECRET_KEY_BASE="$(rand_hex)$(rand_hex)"
)
if [[ -n "${DISCORD_BOT_TOKEN:-}" ]]; then
secret_args+=(
--from-literal=DISCORD_BOT_TOKEN="$DISCORD_BOT_TOKEN"
--from-literal=DISCORD_PUBLIC_KEY="${DISCORD_PUBLIC_KEY:-}"
--from-literal=DISCORD_APPLICATION_ID="${DISCORD_APPLICATION_ID:-}"
--from-literal=DISCORDBOT_API_KEY="${DISCORDBOT_API_KEY:-$(rand_hex)}"
)
fi
if [[ -n "${OP_CONNECT_TOKEN:-}" ]]; then
secret_args+=(--from-literal=OP_CONNECT_TOKEN="$OP_CONNECT_TOKEN")
fi
Expand Down
Loading
Loading