Skip to content

Commit 389cdaf

Browse files
authored
Merge pull request #25 from openfort-xyz/sync/upstream-2026-06-07
chore: sync upstream paradigmxyz/centaur (63 commits)
2 parents a1aaf6e + 6a57131 commit 389cdaf

160 files changed

Lines changed: 16471 additions & 3472 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,12 @@ Tool credentials (e.g., `ANTHROPIC_API_KEY`, `AMP_API_KEY`) are never materializ
524524

525525
For local development, infra secrets are stored in Kubernetes Secrets created by `just bootstrap-secrets`; application secrets continue to come from 1Password.
526526

527+
### iron-control
528+
529+
[iron-control](https://github.com/ironsh/iron-control) is an optional Rails control plane for authenticated API access and encrypted secret storage. It is off by default; enable it with `--set ironControl.enabled=true` (or set `ironControl.enabled: true` in a values file). When enabled, it runs against a dedicated `iron_control_production` database on the bundled Postgres (a separate logical DB so its Rails `schema_migrations` table never collides with the API's dbmate table), created by an idempotent init container.
530+
531+
`just bootstrap-secrets` seeds the required keys into `centaur-infra-env`: the three ActiveRecord encryption keys, `SECRET_KEY_BASE`, and the initial admin password/API key are auto-generated (only when absent, never rotated in place). `IRON_CONTROL_DATABASE_URL` defaults to the bundled Postgres server with no database path (so Rails resolves each connection's database name from the image's `database.yml`); export it before running `just bootstrap-secrets` to point at an external server. Override the admin email with `IRON_CONTROL_INITIAL_USER_EMAIL` (default `admin@centaur.local`).
532+
527533
## Observability & Audit Logs
528534

529535
### Architecture

Justfile

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ release := env_var_or_default("CENTAUR_RELEASE", "centaur")
55
source := env_var_or_default("CENTAUR_IMAGE_SOURCE", "local")
66
chart := "contrib/chart"
77
dev_values := "contrib/chart/values.dev.yaml"
8+
# Command used to import images into k3s's containerd. Override for rootless or
9+
# remote setups, e.g. CENTAUR_K3S_CTR="k3s ctr" or "ssh host sudo k3s ctr".
10+
k3s_ctr := env_var_or_default("CENTAUR_K3S_CTR", "sudo k3s ctr")
811

912
default:
1013
just --list
@@ -61,6 +64,17 @@ _build-chatbot:
6164
_build-agent:
6265
docker build --target sandbox -t centaur-agent:latest -f services/sandbox/Dockerfile .
6366

67+
# Import locally-built images into k3s's containerd. k3s uses containerd, not
68+
# the Docker daemon, so `docker build` images are otherwise invisible to it
69+
# (pods ImagePullBackOff on the :latest tags). Used by `just up k3s`.
70+
_import-k3s:
71+
#!/usr/bin/env bash
72+
set -euo pipefail
73+
for img in centaur-api centaur-iron-proxy centaur-slackbot centaur-agent; do
74+
echo "importing ${img}:latest into k3s containerd..."
75+
docker save "${img}:latest" | {{k3s_ctr}} images import -
76+
done
77+
6478
bootstrap-secrets *args:
6579
contrib/scripts/bootstrap-k8s-secrets.sh --namespace {{namespace}} {{args}}
6680

@@ -73,10 +87,10 @@ deploy:
7387
local) ;;
7488
ghcr)
7589
extra_args+=(
76-
--set api.image.repository=ghcr.io/paradigmxyz/centaur-api
77-
--set ironProxy.image.repository=ghcr.io/paradigmxyz/centaur-iron-proxy
78-
--set slackbot.image.repository=ghcr.io/paradigmxyz/centaur-slackbot
79-
--set sandbox.image.repository=ghcr.io/paradigmxyz/centaur-agent
90+
--set api.image.repository=ghcr.io/paradigmxyz/centaur/centaur-api
91+
--set ironProxy.image.repository=ghcr.io/paradigmxyz/centaur/centaur-iron-proxy
92+
--set slackbot.image.repository=ghcr.io/paradigmxyz/centaur/centaur-slackbot
93+
--set sandbox.image.repository=ghcr.io/paradigmxyz/centaur/centaur-agent
8094
)
8195
;;
8296
*) echo "unknown source: {{source}} (expected local or ghcr)" >&2; exit 2 ;;
@@ -92,14 +106,28 @@ deploy:
92106
--set sandbox.extraEnv.CODEX_AUTH_MODE=${CODEX_AUTH_MODE}
93107
)
94108
fi
109+
if [[ -n "${CLAUDE_CODE_AUTH_MODE:-}" ]]; then
110+
extra_args+=(
111+
--set sandbox.extraEnv.CLAUDE_CODE_AUTH_MODE=${CLAUDE_CODE_AUTH_MODE}
112+
)
113+
fi
95114
helm upgrade --install {{release}} {{chart}} -n {{namespace}} --create-namespace -f {{dev_values}} ${extra_args[@]+"${extra_args[@]}"}
96115

97-
up:
116+
# Bring up the dev stack; pass `k3s` (just up k3s) to import local images into k3s's containerd.
117+
up import="":
98118
#!/usr/bin/env bash
99119
set -euo pipefail
120+
if [[ -n "{{import}}" && "{{import}}" != "k3s" ]]; then
121+
echo "unknown argument: {{import}} (expected nothing or 'k3s')" >&2; exit 2
122+
fi
100123
just bootstrap-secrets
101124
case "{{source}}" in
102-
local) just build ;;
125+
local)
126+
just build
127+
if [[ "{{import}}" == "k3s" ]]; then
128+
just _import-k3s
129+
fi
130+
;;
103131
ghcr) ;;
104132
*) echo "unknown source: {{source}} (expected local or ghcr)" >&2; exit 2 ;;
105133
esac
@@ -163,28 +191,33 @@ cleanup-orphan-proxy-services mode="dry-run":
163191
shell component:
164192
kubectl exec -it -n {{namespace}} deploy/{{release}}-centaur-{{component}} -- sh
165193

166-
smoke:
194+
smoke harness="codex":
167195
#!/usr/bin/env bash
168196
set -euo pipefail
169197
THREAD_KEY="smoke-$(date +%s)"
170198
API_DEPLOY="deploy/{{release}}-centaur-api"
199+
SMOKE_HARNESS="{{harness}}"
200+
api_curl() {
201+
kubectl exec -n {{namespace}} "$API_DEPLOY" -c api -- \
202+
sh -lc 'curl -s -H "x-api-key: ${SLACKBOT_API_KEY:?SLACKBOT_API_KEY is not set}" "$@"' sh "$@"
203+
}
171204

172-
SPAWN=$(kubectl exec -n {{namespace}} "$API_DEPLOY" -- curl -s -X POST http://localhost:8000/agent/spawn \
205+
SPAWN=$(api_curl -X POST http://localhost:8000/agent/spawn \
173206
-H "Content-Type: application/json" \
174-
-d "{\"thread_key\":\"${THREAD_KEY}\"}")
207+
-d "{\"thread_key\":\"${THREAD_KEY}\",\"harness\":\"${SMOKE_HARNESS}\"}")
175208
ASSIGNMENT_GENERATION=$(printf '%s' "$SPAWN" | jq -r '.assignment_generation')
176209

177-
kubectl exec -n {{namespace}} "$API_DEPLOY" -- curl -s -X POST http://localhost:8000/agent/message \
210+
api_curl -X POST http://localhost:8000/agent/message \
178211
-H "Content-Type: application/json" \
179212
-d "{\"thread_key\":\"${THREAD_KEY}\",\"assignment_generation\":${ASSIGNMENT_GENERATION},\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"text\":\"Reply with exactly PONG and nothing else.\"}]}" >/dev/null
180213

181-
EXECUTE=$(kubectl exec -n {{namespace}} "$API_DEPLOY" -- curl -s -X POST http://localhost:8000/agent/execute \
214+
EXECUTE=$(api_curl -X POST http://localhost:8000/agent/execute \
182215
-H "Content-Type: application/json" \
183216
-d "{\"thread_key\":\"${THREAD_KEY}\",\"assignment_generation\":${ASSIGNMENT_GENERATION},\"delivery\":{\"platform\":\"dev\"}}")
184217
EXECUTION_ID=$(printf '%s' "$EXECUTE" | jq -r '.execution_id')
185218

186219
for _ in $(seq 1 60); do
187-
STATE=$(kubectl exec -n {{namespace}} "$API_DEPLOY" -- curl -s "http://localhost:8000/agent/executions/${EXECUTION_ID}")
220+
STATE=$(api_curl "http://localhost:8000/agent/executions/${EXECUTION_ID}")
188221
STATUS=$(printf '%s' "$STATE" | jq -r '.status // empty')
189222
case "$STATUS" in
190223
completed)
@@ -200,6 +233,6 @@ smoke:
200233
sleep 2
201234
done
202235

203-
kubectl exec -n {{namespace}} "$API_DEPLOY" -- curl -s "http://localhost:8000/agent/executions/${EXECUTION_ID}" | jq
236+
api_curl "http://localhost:8000/agent/executions/${EXECUTION_ID}" | jq
204237
echo "smoke timed out waiting for execution ${EXECUTION_ID}" >&2
205238
exit 1

contrib/chart/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: centaur
33
description: Helm chart for the trusted Centaur control plane
44
type: application
5-
version: 0.1.41
5+
version: 0.1.49
66
appVersion: "0.1.0"
77
dependencies:
88
- name: connect

contrib/chart/templates/_helpers.tpl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,22 @@ registered refresh_token OAuthTokenSecrets by the API server at startup.
141141
{{- printf "http://%s:%v" (include "centaur.tokenBrokerHost" .) .Values.tokenBroker.service.httpPort -}}
142142
{{- end -}}
143143

144+
{{- /*
145+
iron-control — Rails control plane for authenticated API access and encrypted
146+
secret storage. Flag-gated (ironControl.enabled), in-cluster ClusterIP Service.
147+
*/ -}}
148+
{{- define "centaur.ironControlName" -}}
149+
{{- include "centaur.componentName" (dict "root" . "component" "iron-control") -}}
150+
{{- end -}}
151+
152+
{{- define "centaur.ironControlHost" -}}
153+
{{- include "centaur.ironControlName" . -}}
154+
{{- end -}}
155+
156+
{{- define "centaur.ironControlUrl" -}}
157+
{{- printf "http://%s:%v" (include "centaur.ironControlHost" .) .Values.ironControl.service.httpPort -}}
158+
{{- end -}}
159+
144160
{{- define "centaur.laminarNoProxyHosts" -}}
145161
{{- if .Values.laminar.enabled -}}
146162
{{- printf ",%s,%s,%s,%s,%s,%s" (include "centaur.componentName" (dict "root" . "component" "laminar-app-server")) (include "centaur.componentName" (dict "root" . "component" "laminar-frontend")) (include "centaur.componentName" (dict "root" . "component" "laminar-postgres")) (include "centaur.componentName" (dict "root" . "component" "laminar-clickhouse")) (include "centaur.componentName" (dict "root" . "component" "laminar-query-engine")) (include "centaur.componentName" (dict "root" . "component" "laminar-quickwit")) -}}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
{{- if .Values.ironControl.enabled }}
2+
{{- $secretEnv := include "centaur.secretEnvName" . }}
3+
{{- $prefix := .Values.secretManager.envPrefix }}
4+
# iron-control — Rails control plane for authenticated API access and encrypted
5+
# secret storage. The chart owns the Deployment shape (image, port, env,
6+
# security context) so operators tune it via `helm upgrade`. It runs against a
7+
# dedicated `iron_control_production` database on the bundled Postgres (a separate logical
8+
# DB so its Rails schema_migrations table never collides with the API's dbmate
9+
# table); the create-db init container provisions that DB idempotently. All
10+
# secret material — the primary DB URL, the bootstrap user/API-key, the three
11+
# ActiveRecord encryption keys, and SECRET_KEY_BASE — comes from the shared
12+
# infra Secret via explicit secretKeyRefs (NOT envFrom, which would leak the
13+
# API's ai_v2 DATABASE_URL into iron-control's env).
14+
apiVersion: v1
15+
kind: Service
16+
metadata:
17+
name: {{ include "centaur.ironControlName" . }}
18+
labels:
19+
{{ include "centaur.componentLabels" (dict "root" . "component" "iron-control") | nindent 4 }}
20+
spec:
21+
selector:
22+
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "iron-control") | nindent 4 }}
23+
ports:
24+
- name: http
25+
port: {{ .Values.ironControl.service.httpPort }}
26+
targetPort: {{ .Values.ironControl.service.httpPort }}
27+
---
28+
apiVersion: apps/v1
29+
kind: Deployment
30+
metadata:
31+
name: {{ include "centaur.ironControlName" . }}
32+
labels:
33+
{{ include "centaur.componentLabels" (dict "root" . "component" "iron-control") | nindent 4 }}
34+
spec:
35+
replicas: {{ .Values.ironControl.replicaCount }}
36+
selector:
37+
matchLabels:
38+
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "iron-control") | nindent 6 }}
39+
template:
40+
metadata:
41+
annotations:
42+
checksum/infra-secrets: {{ include "centaur.infraSecretsChecksum" . }}
43+
labels:
44+
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "iron-control") | nindent 8 }}
45+
spec:
46+
automountServiceAccountToken: false
47+
{{- with .Values.global.imagePullSecrets }}
48+
imagePullSecrets:
49+
{{ toYaml . | nindent 8 }}
50+
{{- end }}
51+
initContainers:
52+
# Create the dedicated logical DB on the bundled Postgres. Idempotent:
53+
# guarded by a pg_database existence check, and the CREATE tolerates a
54+
# lost race against a concurrent replica. Connects as the admin (ai_v2)
55+
# DSN, whose `tempo` superuser can create databases.
56+
- name: create-db
57+
image: {{ printf "%s:%s" .Values.postgres.image.repository .Values.postgres.image.tag | quote }}
58+
imagePullPolicy: {{ .Values.postgres.image.pullPolicy }}
59+
command:
60+
- /bin/sh
61+
- -ec
62+
- |
63+
exists=$(psql "$ADMIN_DATABASE_URL" -tAc \
64+
"SELECT 1 FROM pg_database WHERE datname = '$DB_NAME'")
65+
if [ "$exists" != "1" ]; then
66+
psql "$ADMIN_DATABASE_URL" -c "CREATE DATABASE \"$DB_NAME\"" || true
67+
fi
68+
env:
69+
- name: ADMIN_DATABASE_URL
70+
valueFrom:
71+
secretKeyRef:
72+
name: {{ $secretEnv }}
73+
key: {{ printf "%sDATABASE_URL" $prefix }}
74+
- name: DB_NAME
75+
value: {{ .Values.ironControl.database.name | quote }}
76+
securityContext:
77+
{{ toYaml .Values.containerSecurityContext | nindent 12 }}
78+
seccompProfile:
79+
type: RuntimeDefault
80+
containers:
81+
- name: iron-control
82+
image: {{ printf "%s:%s" .Values.ironControl.image.repository .Values.ironControl.image.tag | quote }}
83+
imagePullPolicy: {{ .Values.ironControl.image.pullPolicy }}
84+
env:
85+
# Explicit secretKeyRefs only — never envFrom the shared Secret, or
86+
# its DATABASE_URL (ai_v2) would leak into iron-control's env. Env
87+
# var names match the IRON_CONTROL_* secret keys 1:1 (except
88+
# SECRET_KEY_BASE, which Rails reads by its standard name). The DB
89+
# URL carries connection info only (no database path) so each Rails
90+
# connection's db name comes from the image's database.yml.
91+
- name: IRON_CONTROL_DATABASE_URL
92+
valueFrom:
93+
secretKeyRef:
94+
name: {{ $secretEnv }}
95+
key: {{ printf "%sIRON_CONTROL_DATABASE_URL" $prefix }}
96+
- name: IRON_CONTROL_INITIAL_USER_EMAIL
97+
valueFrom:
98+
secretKeyRef:
99+
name: {{ $secretEnv }}
100+
key: {{ printf "%sIRON_CONTROL_INITIAL_USER_EMAIL" $prefix }}
101+
- name: IRON_CONTROL_INITIAL_USER_PASSWORD
102+
valueFrom:
103+
secretKeyRef:
104+
name: {{ $secretEnv }}
105+
key: {{ printf "%sIRON_CONTROL_INITIAL_USER_PASSWORD" $prefix }}
106+
- name: IRON_CONTROL_INITIAL_API_KEY
107+
valueFrom:
108+
secretKeyRef:
109+
name: {{ $secretEnv }}
110+
key: {{ printf "%sIRON_CONTROL_INITIAL_API_KEY" $prefix }}
111+
- name: IRON_CONTROL_AR_ENCRYPTION_PRIMARY_KEY
112+
valueFrom:
113+
secretKeyRef:
114+
name: {{ $secretEnv }}
115+
key: {{ printf "%sIRON_CONTROL_AR_ENCRYPTION_PRIMARY_KEY" $prefix }}
116+
- name: IRON_CONTROL_AR_ENCRYPTION_DETERMINISTIC_KEY
117+
valueFrom:
118+
secretKeyRef:
119+
name: {{ $secretEnv }}
120+
key: {{ printf "%sIRON_CONTROL_AR_ENCRYPTION_DETERMINISTIC_KEY" $prefix }}
121+
- name: IRON_CONTROL_AR_ENCRYPTION_KEY_DERIVATION_SALT
122+
valueFrom:
123+
secretKeyRef:
124+
name: {{ $secretEnv }}
125+
key: {{ printf "%sIRON_CONTROL_AR_ENCRYPTION_KEY_DERIVATION_SALT" $prefix }}
126+
- name: SECRET_KEY_BASE
127+
valueFrom:
128+
secretKeyRef:
129+
name: {{ $secretEnv }}
130+
key: {{ printf "%sIRON_CONTROL_SECRET_KEY_BASE" $prefix }}
131+
- name: RAILS_ENV
132+
value: {{ .Values.ironControl.railsEnv | quote }}
133+
- name: PORT
134+
value: {{ .Values.ironControl.service.httpPort | quote }}
135+
- name: RAILS_LOG_TO_STDOUT
136+
value: "1"
137+
- name: RAILS_SERVE_STATIC_FILES
138+
value: "1"
139+
ports:
140+
- containerPort: {{ .Values.ironControl.service.httpPort }}
141+
name: http
142+
startupProbe:
143+
httpGet:
144+
path: /up
145+
port: http
146+
failureThreshold: 30
147+
periodSeconds: 10
148+
timeoutSeconds: 5
149+
readinessProbe:
150+
httpGet:
151+
path: /up
152+
port: http
153+
failureThreshold: 6
154+
periodSeconds: 10
155+
timeoutSeconds: 5
156+
livenessProbe:
157+
httpGet:
158+
path: /up
159+
port: http
160+
failureThreshold: 6
161+
periodSeconds: 10
162+
timeoutSeconds: 5
163+
securityContext:
164+
{{ toYaml .Values.containerSecurityContext | nindent 12 }}
165+
# No readOnlyRootFilesystem — Rails writes to tmp/ at runtime.
166+
seccompProfile:
167+
type: RuntimeDefault
168+
resources:
169+
{{ toYaml .Values.ironControl.resources | nindent 12 }}
170+
{{- if .Values.networkPolicy.enabled }}
171+
---
172+
apiVersion: networking.k8s.io/v1
173+
kind: NetworkPolicy
174+
metadata:
175+
name: {{ include "centaur.ironControlName" . }}
176+
labels:
177+
{{ include "centaur.componentLabels" (dict "root" . "component" "iron-control") | nindent 4 }}
178+
spec:
179+
podSelector:
180+
matchLabels:
181+
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "iron-control") | nindent 6 }}
182+
policyTypes:
183+
- Ingress
184+
- Egress
185+
ingress:
186+
# The API is the natural control-plane caller. Widen this when a concrete
187+
# client integrates. (kubectl port-forward works regardless — it arrives
188+
# via the kubelet, not pod-to-pod.)
189+
- from:
190+
- podSelector:
191+
matchLabels:
192+
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "api") | nindent 14 }}
193+
ports:
194+
- protocol: TCP
195+
port: {{ .Values.ironControl.service.httpPort }}
196+
egress:
197+
{{- if .Values.postgres.enabled }}
198+
- to:
199+
- podSelector:
200+
matchLabels:
201+
{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "postgres") | nindent 14 }}
202+
ports:
203+
- protocol: TCP
204+
port: 5432
205+
{{- end }}
206+
# Outbound HTTPS (external DB DSNs, IdPs, etc.).
207+
- ports:
208+
- protocol: TCP
209+
port: 443
210+
{{- end }}
211+
{{- end }}

0 commit comments

Comments
 (0)