Feat rebrand armature (#43)
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m34s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / build-and-deploy (push) Successful in 1m55s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #43.
This commit is contained in:
2026-03-31 23:25:37 +00:00
committed by xcaliber
parent fb5284f667
commit 680ec3b897
321 changed files with 956 additions and 1033 deletions

View File

@@ -1,6 +1,6 @@
# .gitea/workflows/ci.yaml # .gitea/workflows/ci.yaml
# ============================================ # ============================================
# Switchboard Core - CI/CD Pipeline (v0.17.3) # Armature - CI/CD Pipeline (v0.17.3)
# ============================================ # ============================================
# Single unified image (Go backend + nginx frontend). # Single unified image (Go backend + nginx frontend).
# v0.1.0: Dropped FE/BE image split per ROADMAP design decision. # v0.1.0: Dropped FE/BE image split per ROADMAP design decision.
@@ -29,22 +29,22 @@
# Tags (v*) → always full pipeline # Tags (v*) → always full pipeline
# #
# Deployment mapping (single domain, path-based): # Deployment mapping (single domain, path-based):
# PR → FE + BE :dev → switchboard.DOMAIN/dev/ (DB wipe + fresh schema) # PR → FE + BE :dev → armature.DOMAIN/dev/ (DB wipe + fresh schema)
# Push to main → FE + BE :test → switchboard.DOMAIN/test/ (migrate only) # Push to main → FE + BE :test → armature.DOMAIN/test/ (migrate only)
# Tag v* → FE + BE :latest → switchboard.DOMAIN/ (migrate only) # Tag v* → FE + BE :latest → armature.DOMAIN/ (migrate only)
# → Unified :latest → Docker Hub (not deployed) # → Unified :latest → Docker Hub (not deployed)
# #
# Database lifecycle: # Database lifecycle:
# CI: bootstrap (admin creds) → create DB + role + extensions # CI: bootstrap (admin creds) → create DB + role + extensions
# CI: dev wipe (app creds) → drop tables for fresh install test # CI: dev wipe (app creds) → drop tables for fresh install test
# BE: auto-migrate on startup → schema_migrations tracking # BE: auto-migrate on startup → schema_migrations tracking
# BE: bootstrap admin from SWITCHBOARD_ADMIN_* env vars (upsert on every restart) # BE: bootstrap admin from ARMATURE_ADMIN_* env vars (upsert on every restart)
# #
# Shared PG safety: # Shared PG safety:
# - Bootstrap uses IF NOT EXISTS (idempotent) # - Bootstrap uses IF NOT EXISTS (idempotent)
# - Dev wipe REFUSES to run on databases not ending in _dev # - Dev wipe REFUSES to run on databases not ending in _dev
# - Admin creds never reach the backend pods # - Admin creds never reach the backend pods
# - Each env has its own DB name (switchboard_core_{dev,test,}) # - Each env has its own DB name (armature_{dev,test,})
# #
# Required Gitea Variables: # Required Gitea Variables:
# REGISTRY, NAMESPACE, DOMAIN, POSTGRES_HOST # REGISTRY, NAMESPACE, DOMAIN, POSTGRES_HOST
@@ -58,7 +58,7 @@
# Required Gitea Secrets: # Required Gitea Secrets:
# POSTGRES_USER, POSTGRES_PASSWORD # POSTGRES_USER, POSTGRES_PASSWORD
# POSTGRES_ADMIN_USER, POSTGRES_ADMIN_PASSWORD # POSTGRES_ADMIN_USER, POSTGRES_ADMIN_PASSWORD
# SWITCHBOARD_ADMIN_USERNAME, SWITCHBOARD_ADMIN_PASSWORD, SWITCHBOARD_ADMIN_EMAIL # ARMATURE_ADMIN_USERNAME, ARMATURE_ADMIN_PASSWORD, ARMATURE_ADMIN_EMAIL
# ENCRYPTION_KEY — AES-256 key for API key encryption (openssl rand -base64 32) # ENCRYPTION_KEY — AES-256 key for API key encryption (openssl rand -base64 32)
# PROVIDER_KEY — API key for live provider integration tests (optional, tests skip if missing) # PROVIDER_KEY — API key for live provider integration tests (optional, tests skip if missing)
# DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (optional) # DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (optional)
@@ -88,8 +88,8 @@ env:
CERT_ISSUER: ${{ vars.CERT_ISSUER_PROD || 'letsencrypt-prod' }} CERT_ISSUER: ${{ vars.CERT_ISSUER_PROD || 'letsencrypt-prod' }}
POSTGRES_HOST: ${{ vars.POSTGRES_HOST || 'postgres-primary.postgres.svc.cluster.local' }} POSTGRES_HOST: ${{ vars.POSTGRES_HOST || 'postgres-primary.postgres.svc.cluster.local' }}
POSTGRES_PORT: ${{ vars.POSTGRES_PORT || '5432' }} POSTGRES_PORT: ${{ vars.POSTGRES_PORT || '5432' }}
IMAGE: ${{ vars.REGISTRY }}/switchboard/core IMAGE: ${{ vars.REGISTRY }}/armature
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'gobha/switchboard-core' }} DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'gobha/armature' }}
jobs: jobs:
# ── Stage 0: Detect Changed Paths ────────────── # ── Stage 0: Detect Changed Paths ──────────────
@@ -319,7 +319,7 @@ jobs:
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }} PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
APP_USER: ${{ secrets.POSTGRES_USER }} APP_USER: ${{ secrets.POSTGRES_USER }}
APP_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} APP_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
DB_NAME: switchboard_core_ci DB_NAME: armature_ci
run: | run: |
echo "━━━ CI Test Database Setup ━━━" echo "━━━ CI Test Database Setup ━━━"
# Create DB for Go integration tests (admin creds) # Create DB for Go integration tests (admin creds)
@@ -363,7 +363,7 @@ jobs:
PGUSER: ${{ secrets.POSTGRES_ADMIN_USER }} PGUSER: ${{ secrets.POSTGRES_ADMIN_USER }}
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }} PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
run: | run: |
psql -c "DROP DATABASE IF EXISTS switchboard_core_ci;" postgres psql -c "DROP DATABASE IF EXISTS armature_ci;" postgres
echo "✓ Dropped CI test database" echo "✓ Dropped CI test database"
# ── Stage 2: Build, Database, Deploy ───────── # ── Stage 2: Build, Database, Deploy ─────────
@@ -395,9 +395,9 @@ jobs:
- name: Determine environment - name: Determine environment
id: setup id: setup
run: | run: |
# All environments share one host: switchboard.DOMAIN # All environments share one host: armature.DOMAIN
# Environments are separated by path prefix (BASE_PATH) # Environments are separated by path prefix (BASE_PATH)
DEPLOY_HOST="switchboard.${DOMAIN}" DEPLOY_HOST="armature.${DOMAIN}"
echo "DEPLOY_HOST=${DEPLOY_HOST}" >> "$GITHUB_OUTPUT" echo "DEPLOY_HOST=${DEPLOY_HOST}" >> "$GITHUB_OUTPUT"
if [[ "${{ gitea.event_name }}" == "pull_request" ]]; then if [[ "${{ gitea.event_name }}" == "pull_request" ]]; then
@@ -406,7 +406,7 @@ jobs:
echo "IMAGE_TAG=dev" >> "$GITHUB_OUTPUT" echo "IMAGE_TAG=dev" >> "$GITHUB_OUTPUT"
echo "BASE_PATH=/dev" >> "$GITHUB_OUTPUT" echo "BASE_PATH=/dev" >> "$GITHUB_OUTPUT"
echo "DEPLOY_SUFFIX=-dev" >> "$GITHUB_OUTPUT" echo "DEPLOY_SUFFIX=-dev" >> "$GITHUB_OUTPUT"
echo "DB_NAME=switchboard_core_dev" >> "$GITHUB_OUTPUT" echo "DB_NAME=armature_dev" >> "$GITHUB_OUTPUT"
echo "DB_WIPE=true" >> "$GITHUB_OUTPUT" echo "DB_WIPE=true" >> "$GITHUB_OUTPUT"
echo "REPLICAS=1" >> "$GITHUB_OUTPUT" echo "REPLICAS=1" >> "$GITHUB_OUTPUT"
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT" echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
@@ -422,7 +422,7 @@ jobs:
echo "EXTRA_TAG=${VERSION}" >> "$GITHUB_OUTPUT" echo "EXTRA_TAG=${VERSION}" >> "$GITHUB_OUTPUT"
echo "BASE_PATH=" >> "$GITHUB_OUTPUT" echo "BASE_PATH=" >> "$GITHUB_OUTPUT"
echo "DEPLOY_SUFFIX=" >> "$GITHUB_OUTPUT" echo "DEPLOY_SUFFIX=" >> "$GITHUB_OUTPUT"
echo "DB_NAME=switchboard_core" >> "$GITHUB_OUTPUT" echo "DB_NAME=armature" >> "$GITHUB_OUTPUT"
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT" echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
echo "REPLICAS=2" >> "$GITHUB_OUTPUT" echo "REPLICAS=2" >> "$GITHUB_OUTPUT"
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT" echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
@@ -437,7 +437,7 @@ jobs:
echo "IMAGE_TAG=test" >> "$GITHUB_OUTPUT" echo "IMAGE_TAG=test" >> "$GITHUB_OUTPUT"
echo "BASE_PATH=/test" >> "$GITHUB_OUTPUT" echo "BASE_PATH=/test" >> "$GITHUB_OUTPUT"
echo "DEPLOY_SUFFIX=-test" >> "$GITHUB_OUTPUT" echo "DEPLOY_SUFFIX=-test" >> "$GITHUB_OUTPUT"
echo "DB_NAME=switchboard_core_test" >> "$GITHUB_OUTPUT" echo "DB_NAME=armature_test" >> "$GITHUB_OUTPUT"
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT" echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
echo "REPLICAS=1" >> "$GITHUB_OUTPUT" echo "REPLICAS=1" >> "$GITHUB_OUTPUT"
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT" echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
@@ -579,14 +579,17 @@ jobs:
# ── Push to Docker Hub (release only) ──────── # ── Push to Docker Hub (release only) ────────
- name: Push to Docker Hub - name: Push to Docker Hub
if: steps.setup.outputs.is_release == 'true' if: steps.setup.outputs.is_release == 'true'
env:
DH_USER: ${{ secrets.DOCKERHUB_USERNAME }}
DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: | run: |
docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:latest docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:latest
docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }} docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }}
if [[ -n "${{ secrets.DOCKERHUB_TOKEN }}" ]]; then if [[ -n "${DH_TOKEN}" ]]; then
echo "${{ secrets.DOCKERHUB_TOKEN }}" | \ echo "${DH_TOKEN}" | docker login -u "${DH_USER}" --password-stdin
docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
docker push ${DOCKERHUB_IMAGE}:latest docker push ${DOCKERHUB_IMAGE}:latest
docker push ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }} docker push ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }}
echo "✓ Pushed to Docker Hub"
else else
echo "⚠ Docker Hub credentials not configured, skipping push" echo "⚠ Docker Hub credentials not configured, skipping push"
fi fi
@@ -600,31 +603,31 @@ jobs:
env: env:
PG_USER: ${{ secrets.POSTGRES_USER }} PG_USER: ${{ secrets.POSTGRES_USER }}
PG_PASS: ${{ secrets.POSTGRES_PASSWORD }} PG_PASS: ${{ secrets.POSTGRES_PASSWORD }}
SW_ADMIN_USER: ${{ secrets.SWITCHBOARD_ADMIN_USERNAME }} SW_ADMIN_USER: ${{ secrets.ARMATURE_ADMIN_USERNAME }}
SW_ADMIN_PASS: ${{ secrets.SWITCHBOARD_ADMIN_PASSWORD }} SW_ADMIN_PASS: ${{ secrets.ARMATURE_ADMIN_PASSWORD }}
SW_ADMIN_EMAIL: ${{ secrets.SWITCHBOARD_ADMIN_EMAIL }} SW_ADMIN_EMAIL: ${{ secrets.ARMATURE_ADMIN_EMAIL }}
SEED_USERS: ${{ vars.SEED_USERS }} SEED_USERS: ${{ vars.SEED_USERS }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
run: | run: |
kubectl create secret generic switchboard-db-credentials \ kubectl create secret generic armature-db-credentials \
--namespace=${NAMESPACE} \ --namespace=${NAMESPACE} \
--from-literal=POSTGRES_USER="${PG_USER}" \ --from-literal=POSTGRES_USER="${PG_USER}" \
--from-literal=POSTGRES_PASSWORD="${PG_PASS}" \ --from-literal=POSTGRES_PASSWORD="${PG_PASS}" \
--dry-run=client -o yaml | kubectl apply -f - --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic switchboard-admin \ kubectl create secret generic armature-admin \
--namespace=${NAMESPACE} \ --namespace=${NAMESPACE} \
--from-literal=username="${SW_ADMIN_USER}" \ --from-literal=username="${SW_ADMIN_USER}" \
--from-literal=password="${SW_ADMIN_PASS}" \ --from-literal=password="${SW_ADMIN_PASS}" \
--from-literal=email="${SW_ADMIN_EMAIL}" \ --from-literal=email="${SW_ADMIN_EMAIL}" \
--dry-run=client -o yaml | kubectl apply -f - --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic switchboard-seed-users \ kubectl create secret generic armature-seed-users \
--namespace=${NAMESPACE} \ --namespace=${NAMESPACE} \
--from-literal=users="${SEED_USERS}" \ --from-literal=users="${SEED_USERS}" \
--dry-run=client -o yaml | kubectl apply -f - --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic switchboard-encryption \ kubectl create secret generic armature-encryption \
--namespace=${NAMESPACE} \ --namespace=${NAMESPACE} \
--from-literal=ENCRYPTION_KEY="${ENCRYPTION_KEY}" \ --from-literal=ENCRYPTION_KEY="${ENCRYPTION_KEY}" \
--dry-run=client -o yaml | kubectl apply -f - --dry-run=client -o yaml | kubectl apply -f -
@@ -639,7 +642,7 @@ jobs:
S3_REGION: ${{ vars.S3_REGION || 'us-east-1' }} S3_REGION: ${{ vars.S3_REGION || 'us-east-1' }}
S3_PREFIX: ${{ vars.S3_PREFIX }} S3_PREFIX: ${{ vars.S3_PREFIX }}
run: | run: |
kubectl create secret generic switchboard-s3 \ kubectl create secret generic armature-s3 \
--namespace=${NAMESPACE} \ --namespace=${NAMESPACE} \
--from-literal=S3_ENDPOINT="${S3_ENDPOINT}" \ --from-literal=S3_ENDPOINT="${S3_ENDPOINT}" \
--from-literal=S3_BUCKET="${S3_BUCKET}" \ --from-literal=S3_BUCKET="${S3_BUCKET}" \
@@ -673,7 +676,7 @@ jobs:
envsubst < k8s/storage-pvc.yaml > /tmp/storage-pvc.yaml envsubst < k8s/storage-pvc.yaml > /tmp/storage-pvc.yaml
fi fi
envsubst < k8s/switchboard.yaml > /tmp/switchboard.yaml envsubst < k8s/armature.yaml > /tmp/armature.yaml
envsubst < k8s/middleware-retry.yaml > /tmp/middleware-retry.yaml envsubst < k8s/middleware-retry.yaml > /tmp/middleware-retry.yaml
envsubst < k8s/ingress.yaml > /tmp/ingress.yaml envsubst < k8s/ingress.yaml > /tmp/ingress.yaml
@@ -687,7 +690,7 @@ jobs:
echo " ⚠ STORAGE_CLASS not set — file storage disabled" echo " ⚠ STORAGE_CLASS not set — file storage disabled"
fi fi
kubectl apply -f /tmp/switchboard.yaml kubectl apply -f /tmp/armature.yaml
# Traefik retry middleware (v0.28.8) — requires RBAC for traefik.io CRDs. # Traefik retry middleware (v0.28.8) — requires RBAC for traefik.io CRDs.
# First-time setup: kubectl apply -f k8s/rbac-traefik.yaml (cluster admin) # First-time setup: kubectl apply -f k8s/rbac-traefik.yaml (cluster admin)
@@ -706,8 +709,8 @@ jobs:
# Traefik invalidates the entire router if it references a missing middleware, # Traefik invalidates the entire router if it references a missing middleware,
# which kills all routes for this path prefix. # which kills all routes for this path prefix.
if [[ "${MW_READY}" == "true" ]]; then if [[ "${MW_READY}" == "true" ]]; then
MW_REF="${NAMESPACE}-switchboard-retry${DEPLOY_SUFFIX}@kubernetescrd" MW_REF="${NAMESPACE}-armature-retry${DEPLOY_SUFFIX}@kubernetescrd"
kubectl annotate ingress "switchboard${DEPLOY_SUFFIX}" \ kubectl annotate ingress "armature${DEPLOY_SUFFIX}" \
"traefik.ingress.kubernetes.io/router.middlewares=${MW_REF}" \ "traefik.ingress.kubernetes.io/router.middlewares=${MW_REF}" \
--namespace="${NAMESPACE}" --overwrite --namespace="${NAMESPACE}" --overwrite
echo " ✓ Ingress annotated with retry middleware" echo " ✓ Ingress annotated with retry middleware"
@@ -718,20 +721,20 @@ jobs:
SUFFIX: ${{ steps.setup.outputs.DEPLOY_SUFFIX }} SUFFIX: ${{ steps.setup.outputs.DEPLOY_SUFFIX }}
ENV: ${{ steps.setup.outputs.ENVIRONMENT }} ENV: ${{ steps.setup.outputs.ENVIRONMENT }}
run: | run: |
kubectl rollout restart deployment/switchboard${SUFFIX} -n ${NAMESPACE} kubectl rollout restart deployment/armature${SUFFIX} -n ${NAMESPACE}
kubectl rollout status deployment/switchboard${SUFFIX} -n ${NAMESPACE} --timeout=180s kubectl rollout status deployment/armature${SUFFIX} -n ${NAMESPACE} --timeout=180s
echo "" echo ""
echo "━━━ Pod Status ━━━" echo "━━━ Pod Status ━━━"
kubectl get pods -n ${NAMESPACE} -l app=switchboard,env=${ENV} kubectl get pods -n ${NAMESPACE} -l app=armature,env=${ENV}
READY=$(kubectl get deployment switchboard${SUFFIX} -n ${NAMESPACE} -o jsonpath='{.status.readyReplicas}') READY=$(kubectl get deployment armature${SUFFIX} -n ${NAMESPACE} -o jsonpath='{.status.readyReplicas}')
if [[ "${READY:-0}" -gt 0 ]]; then if [[ "${READY:-0}" -gt 0 ]]; then
echo "✅ Healthy: ${READY} pods ready" echo "✅ Healthy: ${READY} pods ready"
else else
echo "❌ Unhealthy: ${READY:-0} pods ready" echo "❌ Unhealthy: ${READY:-0} pods ready"
kubectl logs -n ${NAMESPACE} -l app=switchboard,env=${ENV} --tail=30 kubectl logs -n ${NAMESPACE} -l app=armature,env=${ENV} --tail=30
exit 1 exit 1
fi fi

View File

@@ -1,6 +1,6 @@
# Changelog # Changelog
All notable changes to Switchboard Core are documented here. All notable changes to Armature are documented here.
## v0.6.7 — Native mTLS ## v0.6.7 — Native mTLS
@@ -28,7 +28,7 @@ deployments where the Go binary terminates TLS itself.
- **Peer TLS config**: `BuildPeerTLSConfig()` constructs a `*tls.Config` for - **Peer TLS config**: `BuildPeerTLSConfig()` constructs a `*tls.Config` for
outbound node-to-node connections (forward-looking — cluster registry is outbound node-to-node connections (forward-looking — cluster registry is
currently DB-backed with no HTTP peer calls). currently DB-backed with no HTTP peer calls).
- **`switchboard-ca.sh`**: Shell wrapper around openssl for cert provisioning. - **`armature-ca.sh`**: Shell wrapper around openssl for cert provisioning.
Three commands: `init` (CA keypair), `issue-node` (365d, ServerAuth + Three commands: `init` (CA keypair), `issue-node` (365d, ServerAuth +
ClientAuth EKU), `issue-user` (90d, ClientAuth only). All ECDSA P-256, PEM ClientAuth EKU), `issue-user` (90d, ClientAuth only). All ECDSA P-256, PEM
output. output.
@@ -875,7 +875,7 @@ Indexes on `folder_id`, `creator_id`, `pinned`, `updated_at`.
- **Migration 012**: Adds `bundled` to `packages.source` CHECK constraint - **Migration 012**: Adds `bundled` to `packages.source` CHECK constraint
(both Postgres and SQLite). (both Postgres and SQLite).
- **K8s manifest**: `SKIP_BUNDLED_PACKAGES` and `BUNDLED_PACKAGES` env vars - **K8s manifest**: `SKIP_BUNDLED_PACKAGES` and `BUNDLED_PACKAGES` env vars
added to `k8s/switchboard.yaml`. added to `k8s/armature.yaml`.
- **Tests**: 6 handler tests (fresh install, skip existing, missing dir, empty - **Tests**: 6 handler tests (fresh install, skip existing, missing dir, empty
dir, dormant handling, allowlist filtering). dir, dormant handling, allowlist filtering).
@@ -1339,7 +1339,7 @@ Indexes on `folder_id`, `creator_id`, `pinned`, `updated_at`.
(`"triggers": [{"type": "event", "pattern": "workflow.completed", ...}]`). (`"triggers": [{"type": "event", "pattern": "workflow.completed", ...}]`).
Wired via `bus.Subscribe()` on startup. Handlers fire asynchronously. Wired via `bus.Subscribe()` on startup. Handlers fire asynchronously.
- **Webhook triggers**: Inbound HTTP at `/api/v1/hooks/:package_id/:slug`. - **Webhook triggers**: Inbound HTTP at `/api/v1/hooks/:package_id/:slug`.
HMAC-SHA256 verification via `X-Switchboard-Signature` header. Synchronous HMAC-SHA256 verification via `X-Armature-Signature` header. Synchronous
Starlark handler can return custom HTTP status and body. Starlark handler can return custom HTTP status and body.
- **Scheduled tasks**: User-created cron-scheduled Starlark scripts with - **Scheduled tasks**: User-created cron-scheduled Starlark scripts with
restricted sandbox (no raw HTTP, no DB table creation, connections-only restricted sandbox (no raw HTTP, no DB table creation, connections-only
@@ -1492,21 +1492,21 @@ storage, and the Starlark sandbox. Everything else is a package.
- CI deploy: k8s resource quantity vars (`BE_MEMORY_REQUEST``MEMORY_REQUEST`) - CI deploy: k8s resource quantity vars (`BE_MEMORY_REQUEST``MEMORY_REQUEST`)
aligned with CI workflow outputs — `envsubst` was producing empty strings aligned with CI workflow outputs — `envsubst` was producing empty strings
- CI deploy: image var (`BE_IMAGE``IMAGE`) — caused `InvalidImageName` in pods - CI deploy: image var (`BE_IMAGE``IMAGE`) — caused `InvalidImageName` in pods
- CI rollout: deployment name (`switchboard``switchboard-be`) — rollout - CI rollout: deployment name (`switchboard``armature-be`) — rollout
verification was looking for wrong deployment name verification was looking for wrong deployment name
- Nginx BASE_PATH: regex cache-header locations intercepted static asset - Nginx BASE_PATH: regex cache-header locations intercepted static asset
requests before alias could strip the sub-path prefix — moved inside alias block requests before alias could strip the sub-path prefix — moved inside alias block
- Post-login blank page: dead Go template references (`surface-chat`, - Post-login blank page: dead Go template references (`surface-chat`,
`surface-notes`, `surface-projects`) caused html/template to silently `surface-notes`, `surface-projects`) caused html/template to silently
produce Content-Length: 0 responses produce Content-Length: 0 responses
- Login branding: "Chat Switchboard" → "Switchboard Core", updated tagline - Login branding: "Chat Armature" → "Armature", updated tagline
and feature pills to reflect platform pivot and feature pills to reflect platform pivot
### Changed ### Changed
- Go module: `switchboard-core` - Go module: `armature`
- VERSION: `0.1.0` - VERSION: `0.1.0`
- Default DB name: `switchboard_core` - Default DB name: `armature`
- Fresh migrations: 9 files × 2 dialects (postgres + sqlite), 27 tables - Fresh migrations: 9 files × 2 dialects (postgres + sqlite), 27 tables
- Store interfaces: 40 → 20 (13 in interfaces.go + 7 in separate iface files) - Store interfaces: 40 → 20 (13 in interfaces.go + 7 in separate iface files)
- Stage modes: `chat_only` removed, `custom` added - Stage modes: `chat_only` removed, `custom` added
@@ -1514,7 +1514,7 @@ storage, and the Starlark sandbox. Everything else is a package.
- Kernel permissions: 16 → 6 (`extension.use`, `extension.install`, - Kernel permissions: 16 → 6 (`extension.use`, `extension.install`,
`workflow.create`, `workflow.submit`, `admin.view`, `token.unlimited`) `workflow.create`, `workflow.submit`, `admin.view`, `token.unlimited`)
- Everyone group seed: `["extension.use","workflow.submit"]` - Everyone group seed: `["extension.use","workflow.submit"]`
- Global settings seed: site name "Switchboard Core" - Global settings seed: site name "Armature"
- Config: removed 7 dropped fields (SessionExpiryDays, WorkflowStaleHours, - Config: removed 7 dropped fields (SessionExpiryDays, WorkflowStaleHours,
ProviderAutoDisableThreshold, ExtractionConcurrency, etc.) ProviderAutoDisableThreshold, ExtractionConcurrency, etc.)
- Health stores rewritten: kernel-only Prune for stale tickets, counters, presence - Health stores rewritten: kernel-only Prune for stale tickets, counters, presence

View File

@@ -1,4 +1,4 @@
# Contributing to Switchboard Core # Contributing to Armature
## Development Setup ## Development Setup
@@ -8,8 +8,8 @@
```sh ```sh
cd server cd server
go build -o switchboard-core . go build -o armature .
DB_DRIVER=sqlite DATABASE_URL=/tmp/switchboard.db ./switchboard-core DB_DRIVER=sqlite DATABASE_URL=/tmp/armature.db ./armature
``` ```
**Docker (recommended):** **Docker (recommended):**

View File

@@ -1,5 +1,5 @@
# ============================================ # ============================================
# Switchboard Core — Unified Dockerfile # Armature — Unified Dockerfile
# ============================================ # ============================================
# Stage 1: Build Go backend # Stage 1: Build Go backend
# Stage 2: Download JS vendor libs (marked, DOMPurify) # Stage 2: Download JS vendor libs (marked, DOMPurify)
@@ -20,7 +20,7 @@ COPY server/go.mod server/go.sum* ./
COPY server/ . COPY server/ .
COPY VERSION ./ COPY VERSION ./
RUN go mod download && go mod tidy && go mod verify RUN go mod download && go mod tidy && go mod verify
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(cat VERSION)" -o /bin/switchboard . RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(cat VERSION)" -o /bin/armature .
# ── Stage 2: Vendor JS libs ───────────────── # ── Stage 2: Vendor JS libs ─────────────────
FROM node:20-alpine AS vendor FROM node:20-alpine AS vendor
@@ -70,7 +70,7 @@ FROM nginx:1-alpine
RUN apk add --no-cache bash git RUN apk add --no-cache bash git
# Go backend binary # Go backend binary
COPY --from=backend /bin/switchboard /usr/local/bin/switchboard COPY --from=backend /bin/armature /usr/local/bin/armature
COPY --from=backend /app/database/migrations /app/database/migrations COPY --from=backend /app/database/migrations /app/database/migrations
# Frontend static files # Frontend static files

View File

@@ -1,5 +1,5 @@
# ============================================ # ============================================
# Switchboard Core — Builder Image # Armature — Builder Image
# ============================================ # ============================================
# Pre-caches Go modules and Node dependencies # Pre-caches Go modules and Node dependencies
# for faster custom builds. Use as a base in # for faster custom builds. Use as a base in
@@ -7,12 +7,12 @@
# download on every build. # download on every build.
# #
# Usage: # Usage:
# FROM ghcr.io/switchboard-core/builder:latest AS go-builder # FROM ghcr.io/armature/builder:latest AS go-builder
# COPY server/ /app/ # COPY server/ /app/
# RUN cd /app && go build -o /bin/switchboard . # RUN cd /app && go build -o /bin/armature .
# #
# Or build this image locally: # Or build this image locally:
# docker build -f Dockerfile.builder -t switchboard-builder . # docker build -f Dockerfile.builder -t armature-builder .
# ============================================ # ============================================
# ── Go module cache ───────────────────────── # ── Go module cache ─────────────────────────
@@ -61,5 +61,5 @@ RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm
WORKDIR /build WORKDIR /build
LABEL org.opencontainers.image.title="Switchboard Core Builder" LABEL org.opencontainers.image.title="Armature Builder"
LABEL org.opencontainers.image.description="Pre-cached build dependencies for faster custom Switchboard Core builds" LABEL org.opencontainers.image.description="Pre-cached build dependencies for faster custom Armature builds"

View File

@@ -1,11 +1,11 @@
# Switchboard Core # Armature
A self-hosted extension platform. Identity, teams, permissions, workflows, A self-hosted extension platform. Identity, teams, permissions, workflows,
and a package system. Everything else ships as installable extensions. and a package system. Everything else ships as installable extensions.
## What This Is ## What This Is
Switchboard Core is the kernel. It provides the primitives that extensions Armature is the kernel. It provides the primitives that extensions
build on: users, teams, groups, scoped credentials, a Starlark sandbox, build on: users, teams, groups, scoped credentials, a Starlark sandbox,
workflow orchestration, notifications, and a package installer. It does not workflow orchestration, notifications, and a package installer. It does not
include AI chat, providers, personas, or any domain-specific features — include AI chat, providers, personas, or any domain-specific features —
@@ -27,7 +27,7 @@ docker compose up --build
# → http://localhost:3000 (admin/admin) # → http://localhost:3000 (admin/admin)
# Or from source # Or from source
git clone <repo-url> && cd switchboard-core git clone <repo-url> && cd armature
cp server/.env.example server/.env # edit DB credentials cp server/.env.example server/.env # edit DB credentials
cd server && go run . cd server && go run .
# → http://localhost:8080 # → http://localhost:8080

View File

@@ -1,4 +1,4 @@
# Switchboard Core — Roadmap # Armature — Roadmap
## Current: v0.6.7 — Native mTLS ## Current: v0.6.7 — Native mTLS
@@ -135,7 +135,7 @@ End-to-end mutual TLS without a reverse proxy. Targets systemd+podman deployment
| TLS server mode | ✅ | Go binary calls `ListenAndServeTLS` directly. `TLS_CERT`, `TLS_KEY`, `TLS_CA` path config. TLS 1.3 minimum, no fallback. `server/config/tls.go` loader + validation. | | TLS server mode | ✅ | Go binary calls `ListenAndServeTLS` directly. `TLS_CERT`, `TLS_KEY`, `TLS_CA` path config. TLS 1.3 minimum, no fallback. `server/config/tls.go` loader + validation. |
| `MTLSNativeProvider` | ✅ | Reads `r.TLS.PeerCertificates[0]` — no header trust. `Subject.CommonName` → username. `sha256(Raw)``external_id`. Auto-provisions `auth_source=mtls`. Renamed existing `mtls.go``mtls_proxy.go`. Shared helpers in `mtls_helpers.go`. | | `MTLSNativeProvider` | ✅ | Reads `r.TLS.PeerCertificates[0]` — no header trust. `Subject.CommonName` → username. `sha256(Raw)``external_id`. Auto-provisions `auth_source=mtls`. Renamed existing `mtls.go``mtls_proxy.go`. Shared helpers in `mtls_helpers.go`. |
| Node-to-node mTLS | ✅ | `BuildPeerTLSConfig()` constructs outbound TLS config with node cert + CA pool. Forward-looking — cluster registry is DB-backed (PG LISTEN/NOTIFY), no HTTP peer calls yet. | | Node-to-node mTLS | ✅ | `BuildPeerTLSConfig()` constructs outbound TLS config with node cert + CA pool. Forward-looking — cluster registry is DB-backed (PG LISTEN/NOTIFY), no HTTP peer calls yet. |
| `switchboard-ca.sh` | ✅ | Shell wrapper around openssl. Three commands: `init` (CA keypair) · `issue-node --name <n> --san <addrs>` (365d) · `issue-user --cn <name>` (90d). All output PEM. | | `armature-ca.sh` | ✅ | Shell wrapper around openssl. Three commands: `init` (CA keypair) · `issue-node --name <n> --san <addrs>` (365d) · `issue-user --cn <name>` (90d). All output PEM. |
| Unit tests | ✅ | Ephemeral CA via `crypto/x509`. Fabricated `PeerCertificates`. Valid cert → user provisioned · no TLS → `ErrNoCert` · empty certs → `ErrNoCert` · no CN → `ErrInvalidCreds`. | | Unit tests | ✅ | Ephemeral CA via `crypto/x509`. Fabricated `PeerCertificates`. Valid cert → user provisioned · no TLS → `ErrNoCert` · empty certs → `ErrNoCert` · no CN → `ErrInvalidCreds`. |
| Integration tests | ✅ | Real TLS listener on localhost. No cert / wrong CA / expired → TLS handshake rejected. Valid cert → 200. Peer certificate CN + email visibility verified. | | Integration tests | ✅ | Real TLS listener on localhost. No cert / wrong CA / expired → TLS handshake rejected. Valid cert → 200. Peer certificate CN + email visibility verified. |

View File

@@ -1,11 +1,11 @@
apiVersion: v2 apiVersion: v2
name: switchboard name: armature
description: Switchboard Core — self-hosted extension platform description: Armature — self-hosted extension platform
type: application type: application
version: 0.1.0 version: 0.1.0
appVersion: "0.0.0" # Patched at CI/release time from /VERSION appVersion: "0.0.0" # Patched at CI/release time from /VERSION
home: https://gobha.ai home: https://gobha.ai
sources: sources:
- https://git.gobha.me/switchboard/core - https://git.gobha.me/armature/core
maintainers: maintainers:
- name: xcaliber - name: xcaliber

View File

@@ -1,27 +1,27 @@
# Switchboard Core — PrometheusRule alerts (v0.33.0) # Armature Core — PrometheusRule alerts (v0.33.0)
# Source file for the Helm template. Deploy via: # Source file for the Helm template. Deploy via:
# monitoring.prometheusRule.enabled: true # monitoring.prometheusRule.enabled: true
apiVersion: monitoring.coreos.com/v1 apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule kind: PrometheusRule
metadata: metadata:
name: switchboard-alerts name: armature-alerts
spec: spec:
groups: groups:
- name: switchboard.rules - name: armature.rules
rules: rules:
# Pod restart (possible OOM) # Pod restart (possible OOM)
- alert: SwitchboardPodRestart - alert: ArmaturePodRestart
expr: increase(kube_pod_container_status_restarts_total{container="backend"}[1h]) > 0 expr: increase(kube_pod_container_status_restarts_total{container="backend"}[1h]) > 0
for: 0m for: 0m
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: "Switchboard backend pod restarted (possible OOM)" summary: "Armature backend pod restarted (possible OOM)"
description: "Container {{ $labels.container }} in pod {{ $labels.pod }} restarted." description: "Container {{ $labels.container }} in pod {{ $labels.pod }} restarted."
# Provider down for 5+ minutes # Provider down for 5+ minutes
- alert: SwitchboardProviderDown - alert: ArmatureProviderDown
expr: switchboard_provider_status > 2 expr: armature_provider_status > 2
for: 5m for: 5m
labels: labels:
severity: critical severity: critical
@@ -29,8 +29,8 @@ spec:
summary: "Provider {{ $labels.provider_config_id }} is down" summary: "Provider {{ $labels.provider_config_id }} is down"
# DB pool >80% utilized # DB pool >80% utilized
- alert: SwitchboardDBPoolExhaustion - alert: ArmatureDBPoolExhaustion
expr: switchboard_db_in_use_connections / switchboard_db_open_connections > 0.8 expr: armature_db_in_use_connections / armature_db_open_connections > 0.8
for: 5m for: 5m
labels: labels:
severity: warning severity: warning
@@ -38,10 +38,10 @@ spec:
summary: "DB connection pool >80% utilized" summary: "DB connection pool >80% utilized"
# HTTP 5xx error rate >5% # HTTP 5xx error rate >5%
- alert: SwitchboardHighErrorRate - alert: ArmatureHighErrorRate
expr: > expr: >
sum(rate(switchboard_http_requests_total{status=~"5.."}[5m])) sum(rate(armature_http_requests_total{status=~"5.."}[5m]))
/ sum(rate(switchboard_http_requests_total[5m])) > 0.05 / sum(rate(armature_http_requests_total[5m])) > 0.05
for: 5m for: 5m
labels: labels:
severity: warning severity: warning
@@ -49,10 +49,10 @@ spec:
summary: "HTTP 5xx error rate exceeds 5%" summary: "HTTP 5xx error rate exceeds 5%"
# Task failure rate >25% # Task failure rate >25%
- alert: SwitchboardTaskFailureRate - alert: ArmatureTaskFailureRate
expr: > expr: >
rate(switchboard_task_executions_total{status="error"}[15m]) rate(armature_task_executions_total{status="error"}[15m])
/ rate(switchboard_task_executions_total[15m]) > 0.25 / rate(armature_task_executions_total[15m]) > 0.25
for: 10m for: 10m
labels: labels:
severity: warning severity: warning
@@ -60,8 +60,8 @@ spec:
summary: "Task failure rate exceeds 25% over 15 minutes" summary: "Task failure rate exceeds 25% over 15 minutes"
# No completions processed in 15 minutes (canary) # No completions processed in 15 minutes (canary)
- alert: SwitchboardNoCompletions - alert: ArmatureNoCompletions
expr: sum(rate(switchboard_completions_total[10m])) == 0 expr: sum(rate(armature_completions_total[10m])) == 0
for: 15m for: 15m
labels: labels:
severity: critical severity: critical

View File

@@ -17,10 +17,10 @@
{ "type": "panel", "id": "gauge", "name": "Gauge", "version": "" } { "type": "panel", "id": "gauge", "name": "Gauge", "version": "" }
], ],
"id": null, "id": null,
"uid": "switchboard-overview", "uid": "armature-overview",
"title": "Switchboard Core — Overview", "title": "Armature — Overview",
"description": "System overview: request rates, latency, provider health, token usage, DB pool.", "description": "System overview: request rates, latency, provider health, token usage, DB pool.",
"tags": ["switchboard"], "tags": ["armature"],
"timezone": "browser", "timezone": "browser",
"refresh": "30s", "refresh": "30s",
"schemaVersion": 38, "schemaVersion": 38,
@@ -37,7 +37,7 @@
"name": "namespace", "name": "namespace",
"type": "query", "type": "query",
"datasource": { "type": "prometheus", "uid": "${datasource}" }, "datasource": { "type": "prometheus", "uid": "${datasource}" },
"query": "label_values(switchboard_http_requests_total, namespace)", "query": "label_values(armature_http_requests_total, namespace)",
"includeAll": true, "includeAll": true,
"current": { "text": "All", "value": "$__all" } "current": { "text": "All", "value": "$__all" }
}, },
@@ -45,7 +45,7 @@
"name": "pod", "name": "pod",
"type": "query", "type": "query",
"datasource": { "type": "prometheus", "uid": "${datasource}" }, "datasource": { "type": "prometheus", "uid": "${datasource}" },
"query": "label_values(switchboard_http_requests_total{namespace=~\"$namespace\"}, pod)", "query": "label_values(armature_http_requests_total{namespace=~\"$namespace\"}, pod)",
"includeAll": true, "includeAll": true,
"current": { "text": "All", "value": "$__all" } "current": { "text": "All", "value": "$__all" }
} }
@@ -58,7 +58,7 @@
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 0 }, "gridPos": { "h": 8, "w": 6, "x": 0, "y": 0 },
"targets": [ "targets": [
{ {
"expr": "sum(rate(switchboard_http_requests_total{namespace=~\"$namespace\"}[5m]))", "expr": "sum(rate(armature_http_requests_total{namespace=~\"$namespace\"}[5m]))",
"legendFormat": "Total req/s" "legendFormat": "Total req/s"
} }
] ]
@@ -69,7 +69,7 @@
"gridPos": { "h": 8, "w": 6, "x": 6, "y": 0 }, "gridPos": { "h": 8, "w": 6, "x": 6, "y": 0 },
"targets": [ "targets": [
{ {
"expr": "sum(rate(switchboard_http_requests_total{namespace=~\"$namespace\",status=~\"5..\"}[5m])) / sum(rate(switchboard_http_requests_total{namespace=~\"$namespace\"}[5m]))", "expr": "sum(rate(armature_http_requests_total{namespace=~\"$namespace\",status=~\"5..\"}[5m])) / sum(rate(armature_http_requests_total{namespace=~\"$namespace\"}[5m]))",
"legendFormat": "5xx rate" "legendFormat": "5xx rate"
} }
], ],
@@ -93,15 +93,15 @@
"gridPos": { "h": 8, "w": 6, "x": 12, "y": 0 }, "gridPos": { "h": 8, "w": 6, "x": 12, "y": 0 },
"targets": [ "targets": [
{ {
"expr": "histogram_quantile(0.50, sum(rate(switchboard_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))", "expr": "histogram_quantile(0.50, sum(rate(armature_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
"legendFormat": "p50" "legendFormat": "p50"
}, },
{ {
"expr": "histogram_quantile(0.95, sum(rate(switchboard_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))", "expr": "histogram_quantile(0.95, sum(rate(armature_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
"legendFormat": "p95" "legendFormat": "p95"
}, },
{ {
"expr": "histogram_quantile(0.99, sum(rate(switchboard_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))", "expr": "histogram_quantile(0.99, sum(rate(armature_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
"legendFormat": "p99" "legendFormat": "p99"
} }
], ],
@@ -113,7 +113,7 @@
"gridPos": { "h": 8, "w": 6, "x": 18, "y": 0 }, "gridPos": { "h": 8, "w": 6, "x": 18, "y": 0 },
"targets": [ "targets": [
{ {
"expr": "sum(switchboard_websocket_connections{namespace=~\"$namespace\"})", "expr": "sum(armature_websocket_connections{namespace=~\"$namespace\"})",
"legendFormat": "Active" "legendFormat": "Active"
} }
] ]
@@ -124,7 +124,7 @@
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
"targets": [ "targets": [
{ {
"expr": "sum by (provider_config_id) (rate(switchboard_completions_total{namespace=~\"$namespace\"}[5m]))", "expr": "sum by (provider_config_id) (rate(armature_completions_total{namespace=~\"$namespace\"}[5m]))",
"legendFormat": "{{provider_config_id}}" "legendFormat": "{{provider_config_id}}"
} }
] ]
@@ -135,7 +135,7 @@
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
"targets": [ "targets": [
{ {
"expr": "histogram_quantile(0.95, sum by (provider_config_id, le) (rate(switchboard_completion_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])))", "expr": "histogram_quantile(0.95, sum by (provider_config_id, le) (rate(armature_completion_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])))",
"legendFormat": "{{provider_config_id}}" "legendFormat": "{{provider_config_id}}"
} }
], ],
@@ -147,7 +147,7 @@
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
"targets": [ "targets": [
{ {
"expr": "sum by (model_id) (rate(switchboard_completion_tokens_total{namespace=~\"$namespace\"}[5m])) * 60", "expr": "sum by (model_id) (rate(armature_completion_tokens_total{namespace=~\"$namespace\"}[5m])) * 60",
"legendFormat": "{{model_id}}" "legendFormat": "{{model_id}}"
} }
] ]
@@ -158,7 +158,7 @@
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
"targets": [ "targets": [
{ {
"expr": "switchboard_provider_status{namespace=~\"$namespace\"}", "expr": "armature_provider_status{namespace=~\"$namespace\"}",
"legendFormat": "{{provider_config_id}}" "legendFormat": "{{provider_config_id}}"
} }
], ],
@@ -179,15 +179,15 @@
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
"targets": [ "targets": [
{ {
"expr": "switchboard_db_open_connections{namespace=~\"$namespace\"}", "expr": "armature_db_open_connections{namespace=~\"$namespace\"}",
"legendFormat": "Open" "legendFormat": "Open"
}, },
{ {
"expr": "switchboard_db_in_use_connections{namespace=~\"$namespace\"}", "expr": "armature_db_in_use_connections{namespace=~\"$namespace\"}",
"legendFormat": "In Use" "legendFormat": "In Use"
}, },
{ {
"expr": "switchboard_db_idle_connections{namespace=~\"$namespace\"}", "expr": "armature_db_idle_connections{namespace=~\"$namespace\"}",
"legendFormat": "Idle" "legendFormat": "Idle"
} }
] ]
@@ -198,7 +198,7 @@
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
"targets": [ "targets": [
{ {
"expr": "sum by (status) (rate(switchboard_task_executions_total{namespace=~\"$namespace\"}[5m]))", "expr": "sum by (status) (rate(armature_task_executions_total{namespace=~\"$namespace\"}[5m]))",
"legendFormat": "{{status}}" "legendFormat": "{{status}}"
} }
] ]

View File

@@ -1,4 +1,4 @@
Chat Switchboard {{ .Chart.AppVersion }} deployed. Armature {{ .Chart.AppVersion }} deployed.
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
Access the application at: Access the application at:

View File

@@ -1,7 +1,7 @@
{{/* {{/*
Common labels Common labels
*/}} */}}
{{- define "switchboard.labels" -}} {{- define "armature.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }} app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
@@ -12,23 +12,23 @@ helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{/* {{/*
Backend selector labels Backend selector labels
*/}} */}}
{{- define "switchboard.backend.labels" -}} {{- define "armature.backend.labels" -}}
app.kubernetes.io/component: backend app.kubernetes.io/component: backend
{{ include "switchboard.labels" . }} {{ include "armature.labels" . }}
{{- end }} {{- end }}
{{/* {{/*
Frontend selector labels Frontend selector labels
*/}} */}}
{{- define "switchboard.frontend.labels" -}} {{- define "armature.frontend.labels" -}}
app.kubernetes.io/component: frontend app.kubernetes.io/component: frontend
{{ include "switchboard.labels" . }} {{ include "armature.labels" . }}
{{- end }} {{- end }}
{{/* {{/*
Secret name Secret name
*/}} */}}
{{- define "switchboard.secretName" -}} {{- define "armature.secretName" -}}
{{- if .Values.existingSecret -}} {{- if .Values.existingSecret -}}
{{ .Values.existingSecret }} {{ .Values.existingSecret }}
{{- else -}} {{- else -}}
@@ -39,21 +39,21 @@ Secret name
{{/* {{/*
Backend image Backend image
*/}} */}}
{{- define "switchboard.backend.image" -}} {{- define "armature.backend.image" -}}
{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }} {{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}
{{- end }} {{- end }}
{{/* {{/*
Frontend image Frontend image
*/}} */}}
{{- define "switchboard.frontend.image" -}} {{- define "armature.frontend.image" -}}
{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }} {{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}
{{- end }} {{- end }}
{{/* {{/*
Database URL assembled from postgres.* if url is empty Database URL assembled from postgres.* if url is empty
*/}} */}}
{{- define "switchboard.databaseURL" -}} {{- define "armature.databaseURL" -}}
{{- if .Values.database.url -}} {{- if .Values.database.url -}}
{{ .Values.database.url }} {{ .Values.database.url }}
{{- else if eq .Values.database.driver "sqlite" -}} {{- else if eq .Values.database.driver "sqlite" -}}

View File

@@ -3,7 +3,7 @@ kind: ConfigMap
metadata: metadata:
name: {{ .Release.Name }}-config name: {{ .Release.Name }}-config
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
data: data:
PORT: {{ .Values.backend.port | quote }} PORT: {{ .Values.backend.port | quote }}
BASE_PATH: {{ .Values.basePath | quote }} BASE_PATH: {{ .Values.basePath | quote }}

View File

@@ -4,7 +4,7 @@ kind: CronJob
metadata: metadata:
name: {{ .Release.Name }}-backup name: {{ .Release.Name }}-backup
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
app.kubernetes.io/component: backup app.kubernetes.io/component: backup
spec: spec:
schedule: {{ .Values.backup.schedule | quote }} schedule: {{ .Values.backup.schedule | quote }}
@@ -35,7 +35,7 @@ spec:
- | - |
set -e set -e
TIMESTAMP=$(date +%Y%m%d-%H%M%S) TIMESTAMP=$(date +%Y%m%d-%H%M%S)
FILENAME="switchboard-${TIMESTAMP}.sql.gz" FILENAME="armature-${TIMESTAMP}.sql.gz"
echo "Starting backup: ${FILENAME}" echo "Starting backup: ${FILENAME}"
pg_dump "${DATABASE_URL}" | gzip > "/backup/${FILENAME}" pg_dump "${DATABASE_URL}" | gzip > "/backup/${FILENAME}"
@@ -58,22 +58,22 @@ spec:
# Prune old local backups beyond retention # Prune old local backups beyond retention
cd /backup cd /backup
ls -t switchboard-*.sql.gz 2>/dev/null | tail -n +{{ add1 .Values.backup.retention }} | xargs -r rm -v ls -t armature-*.sql.gz 2>/dev/null | tail -n +{{ add1 .Values.backup.retention }} | xargs -r rm -v
echo "Backup complete" echo "Backup complete"
env: env:
- name: DATABASE_URL - name: DATABASE_URL
value: {{ include "switchboard.databaseURL" . | quote }} value: {{ include "armature.databaseURL" . | quote }}
{{- if .Values.backup.s3.enabled }} {{- if .Values.backup.s3.enabled }}
- name: S3_ACCESS_KEY - name: S3_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: {{ include "switchboard.secretName" . }} name: {{ include "armature.secretName" . }}
key: BACKUP_S3_ACCESS_KEY key: BACKUP_S3_ACCESS_KEY
optional: true optional: true
- name: S3_SECRET_KEY - name: S3_SECRET_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: {{ include "switchboard.secretName" . }} name: {{ include "armature.secretName" . }}
key: BACKUP_S3_SECRET_KEY key: BACKUP_S3_SECRET_KEY
optional: true optional: true
{{- end }} {{- end }}

View File

@@ -3,7 +3,7 @@ kind: Deployment
metadata: metadata:
name: {{ .Release.Name }}-backend name: {{ .Release.Name }}-backend
labels: labels:
{{- include "switchboard.backend.labels" . | nindent 4 }} {{- include "armature.backend.labels" . | nindent 4 }}
spec: spec:
replicas: {{ .Values.backend.replicaCount }} replicas: {{ .Values.backend.replicaCount }}
selector: selector:
@@ -38,7 +38,7 @@ spec:
topologyKey: kubernetes.io/hostname topologyKey: kubernetes.io/hostname
containers: containers:
- name: backend - name: backend
image: {{ include "switchboard.backend.image" . }} image: {{ include "armature.backend.image" . }}
imagePullPolicy: {{ .Values.backend.image.pullPolicy }} imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
ports: ports:
- name: http - name: http
@@ -48,10 +48,10 @@ spec:
- configMapRef: - configMapRef:
name: {{ .Release.Name }}-config name: {{ .Release.Name }}-config
- secretRef: - secretRef:
name: {{ include "switchboard.secretName" . }} name: {{ include "armature.secretName" . }}
env: env:
- name: DATABASE_URL - name: DATABASE_URL
value: {{ include "switchboard.databaseURL" . | quote }} value: {{ include "armature.databaseURL" . | quote }}
resources: resources:
{{- toYaml .Values.backend.resources | nindent 12 }} {{- toYaml .Values.backend.resources | nindent 12 }}
{{- if .Values.persistence.enabled }} {{- if .Values.persistence.enabled }}

View File

@@ -3,7 +3,7 @@ kind: Deployment
metadata: metadata:
name: {{ .Release.Name }}-frontend name: {{ .Release.Name }}-frontend
labels: labels:
{{- include "switchboard.frontend.labels" . | nindent 4 }} {{- include "armature.frontend.labels" . | nindent 4 }}
spec: spec:
replicas: {{ .Values.frontend.replicaCount }} replicas: {{ .Values.frontend.replicaCount }}
selector: selector:
@@ -24,7 +24,7 @@ spec:
{{- end }} {{- end }}
containers: containers:
- name: frontend - name: frontend
image: {{ include "switchboard.frontend.image" . }} image: {{ include "armature.frontend.image" . }}
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports: ports:
- name: http - name: http

View File

@@ -2,13 +2,13 @@
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: {{ include "switchboard.fullname" . }}-grafana-dashboard name: {{ include "armature.fullname" . }}-grafana-dashboard
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
{{- with .Values.monitoring.grafanaDashboard.labels }} {{- with .Values.monitoring.grafanaDashboard.labels }}
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}
data: data:
switchboard-overview.json: |- armature-overview.json: |-
{{- .Files.Get "dashboards/switchboard-overview.json" | nindent 4 }} {{- .Files.Get "dashboards/armature-overview.json" | nindent 4 }}
{{- end }} {{- end }}

View File

@@ -4,7 +4,7 @@ kind: Ingress
metadata: metadata:
name: {{ .Release.Name }}-ingress name: {{ .Release.Name }}-ingress
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
{{- if or .Values.ingress.annotations (and (eq (default "traefik" .Values.ingress.className) "traefik") .Values.ingress.retry.annotateIngress) }} {{- if or .Values.ingress.annotations (and (eq (default "traefik" .Values.ingress.className) "traefik") .Values.ingress.retry.annotateIngress) }}
annotations: annotations:
{{- with .Values.ingress.annotations }} {{- with .Values.ingress.annotations }}

View File

@@ -5,7 +5,7 @@ kind: Middleware
metadata: metadata:
name: {{ .Release.Name }}-retry name: {{ .Release.Name }}-retry
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
spec: spec:
retry: retry:
attempts: {{ .Values.ingress.retry.attempts | default 2 }} attempts: {{ .Values.ingress.retry.attempts | default 2 }}

View File

@@ -2,57 +2,57 @@
apiVersion: monitoring.coreos.com/v1 apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule kind: PrometheusRule
metadata: metadata:
name: {{ include "switchboard.fullname" . }}-alerts name: {{ include "armature.fullname" . }}-alerts
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
{{- with .Values.monitoring.prometheusRule.labels }} {{- with .Values.monitoring.prometheusRule.labels }}
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}
spec: spec:
groups: groups:
- name: switchboard.rules - name: armature.rules
rules: rules:
- alert: SwitchboardPodRestart - alert: ArmaturePodRestart
expr: increase(kube_pod_container_status_restarts_total{container="backend"}[1h]) > 0 expr: increase(kube_pod_container_status_restarts_total{container="backend"}[1h]) > 0
for: 0m for: 0m
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: "Switchboard backend pod restarted (possible OOM)" summary: "Armature backend pod restarted (possible OOM)"
- alert: SwitchboardProviderDown - alert: ArmatureProviderDown
expr: switchboard_provider_status > 2 expr: armature_provider_status > 2
for: 5m for: 5m
labels: labels:
severity: critical severity: critical
annotations: annotations:
summary: "Provider {{`{{ $labels.provider_config_id }}`}} is down" summary: "Provider {{`{{ $labels.provider_config_id }}`}} is down"
- alert: SwitchboardDBPoolExhaustion - alert: ArmatureDBPoolExhaustion
expr: switchboard_db_in_use_connections / switchboard_db_open_connections > 0.8 expr: armature_db_in_use_connections / armature_db_open_connections > 0.8
for: 5m for: 5m
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: "DB connection pool >80% utilized" summary: "DB connection pool >80% utilized"
- alert: SwitchboardHighErrorRate - alert: ArmatureHighErrorRate
expr: | expr: |
sum(rate(switchboard_http_requests_total{status=~"5.."}[5m])) sum(rate(armature_http_requests_total{status=~"5.."}[5m]))
/ sum(rate(switchboard_http_requests_total[5m])) > 0.05 / sum(rate(armature_http_requests_total[5m])) > 0.05
for: 5m for: 5m
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: "HTTP 5xx error rate exceeds 5%" summary: "HTTP 5xx error rate exceeds 5%"
- alert: SwitchboardTaskFailureRate - alert: ArmatureTaskFailureRate
expr: | expr: |
rate(switchboard_task_executions_total{status="error"}[15m]) rate(armature_task_executions_total{status="error"}[15m])
/ rate(switchboard_task_executions_total[15m]) > 0.25 / rate(armature_task_executions_total[15m]) > 0.25
for: 10m for: 10m
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: "Task failure rate exceeds 25%" summary: "Task failure rate exceeds 25%"
- alert: SwitchboardNoCompletions - alert: ArmatureNoCompletions
expr: sum(rate(switchboard_completions_total[10m])) == 0 expr: sum(rate(armature_completions_total[10m])) == 0
for: 15m for: 15m
labels: labels:
severity: critical severity: critical

View File

@@ -4,7 +4,7 @@ kind: PersistentVolumeClaim
metadata: metadata:
name: {{ .Release.Name }}-backup name: {{ .Release.Name }}-backup
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
app.kubernetes.io/component: backup app.kubernetes.io/component: backup
spec: spec:
accessModes: accessModes:

View File

@@ -4,7 +4,7 @@ kind: PersistentVolumeClaim
metadata: metadata:
name: {{ .Release.Name }}-data name: {{ .Release.Name }}-data
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
spec: spec:
accessModes: accessModes:
- {{ .Values.persistence.accessMode }} - {{ .Values.persistence.accessMode }}

View File

@@ -4,7 +4,7 @@ kind: Secret
metadata: metadata:
name: {{ .Release.Name }}-secrets name: {{ .Release.Name }}-secrets
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
type: Opaque type: Opaque
stringData: stringData:
JWT_SECRET: {{ required "jwtSecret is required" .Values.jwtSecret | quote }} JWT_SECRET: {{ required "jwtSecret is required" .Values.jwtSecret | quote }}
@@ -15,11 +15,11 @@ stringData:
POSTGRES_PASSWORD: {{ .Values.database.postgres.password | quote }} POSTGRES_PASSWORD: {{ .Values.database.postgres.password | quote }}
{{- end }} {{- end }}
{{- if .Values.admin.password }} {{- if .Values.admin.password }}
SWITCHBOARD_ADMIN_USERNAME: {{ .Values.admin.username | quote }} ARMATURE_ADMIN_USERNAME: {{ .Values.admin.username | quote }}
SWITCHBOARD_ADMIN_PASSWORD: {{ .Values.admin.password | quote }} ARMATURE_ADMIN_PASSWORD: {{ .Values.admin.password | quote }}
{{- end }} {{- end }}
{{- if .Values.admin.email }} {{- if .Values.admin.email }}
SWITCHBOARD_ADMIN_EMAIL: {{ .Values.admin.email | quote }} ARMATURE_ADMIN_EMAIL: {{ .Values.admin.email | quote }}
{{- end }} {{- end }}
{{- if .Values.storage.s3.accessKey }} {{- if .Values.storage.s3.accessKey }}
S3_ACCESS_KEY: {{ .Values.storage.s3.accessKey | quote }} S3_ACCESS_KEY: {{ .Values.storage.s3.accessKey | quote }}

View File

@@ -2,16 +2,16 @@
apiVersion: monitoring.coreos.com/v1 apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor kind: ServiceMonitor
metadata: metadata:
name: {{ include "switchboard.fullname" . }} name: {{ include "armature.fullname" . }}
labels: labels:
{{- include "switchboard.labels" . | nindent 4 }} {{- include "armature.labels" . | nindent 4 }}
{{- with .Values.monitoring.serviceMonitor.labels }} {{- with .Values.monitoring.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}
spec: spec:
selector: selector:
matchLabels: matchLabels:
{{- include "switchboard.selectorLabels" . | nindent 6 }} {{- include "armature.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: backend app.kubernetes.io/component: backend
endpoints: endpoints:
- port: http - port: http

View File

@@ -3,7 +3,7 @@ kind: Service
metadata: metadata:
name: {{ .Release.Name }}-backend name: {{ .Release.Name }}-backend
labels: labels:
{{- include "switchboard.backend.labels" . | nindent 4 }} {{- include "armature.backend.labels" . | nindent 4 }}
annotations: annotations:
prometheus.io/scrape: "true" prometheus.io/scrape: "true"
prometheus.io/port: {{ .Values.backend.port | quote }} prometheus.io/port: {{ .Values.backend.port | quote }}
@@ -25,7 +25,7 @@ kind: Service
metadata: metadata:
name: {{ .Release.Name }}-frontend name: {{ .Release.Name }}-frontend
labels: labels:
{{- include "switchboard.frontend.labels" . | nindent 4 }} {{- include "armature.frontend.labels" . | nindent 4 }}
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:

View File

@@ -1,10 +1,10 @@
# Switchboard Core — Helm values # Armature — Helm values
# helm install switchboard ./chart # helm install armature ./chart
# ── Images ───────────────────────────────── # ── Images ─────────────────────────────────
backend: backend:
image: image:
repository: git.gobha.me/switchboard/core repository: git.gobha.me/armature/core
tag: "" # defaults to Chart.appVersion tag: "" # defaults to Chart.appVersion
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
replicaCount: 2 # v0.32.0: multi-replica HA replicaCount: 2 # v0.32.0: multi-replica HA
@@ -19,7 +19,7 @@ backend:
frontend: frontend:
image: image:
repository: git.gobha.me/switchboard/core repository: git.gobha.me/armature/core
tag: "" # defaults to Chart.appVersion tag: "" # defaults to Chart.appVersion
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
replicaCount: 1 replicaCount: 1
@@ -41,12 +41,12 @@ database:
postgres: postgres:
host: postgresql host: postgresql
port: 5432 port: 5432
user: switchboard user: armature
password: "" # set via secret password: "" # set via secret
database: switchboard database: armature
sslmode: disable sslmode: disable
# For sqlite: path inside the container (requires persistence) # For sqlite: path inside the container (requires persistence)
sqlitePath: /data/switchboard.db sqlitePath: /data/armature.db
# ── Core ─────────────────────────────────── # ── Core ───────────────────────────────────
basePath: "" # URL prefix, e.g. "/dev" basePath: "" # URL prefix, e.g. "/dev"
@@ -85,7 +85,7 @@ ingress:
enabled: true enabled: true
className: traefik className: traefik
annotations: {} annotations: {}
host: switchboard.local host: armature.local
tls: tls:
enabled: false enabled: false
secretName: "" secretName: ""

View File

@@ -103,7 +103,7 @@ done
# ── Test 4: Stop one replica → stale sweep ──── # ── Test 4: Stop one replica → stale sweep ────
echo "=== Test 4: Stop node-3, wait for sweep ===" echo "=== Test 4: Stop node-3, wait for sweep ==="
docker compose -f docker-compose-e2e.yml stop switchboard-3 docker compose -f docker-compose-e2e.yml stop armature-3
echo " waiting ${STALE_WAIT}s for stale sweep..." echo " waiting ${STALE_WAIT}s for stale sweep..."
sleep "$STALE_WAIT" sleep "$STALE_WAIT"
@@ -118,7 +118,7 @@ fi
# ── Test 5: Restart replica → re-registers ──── # ── Test 5: Restart replica → re-registers ────
echo "=== Test 5: Restart node-3 ===" echo "=== Test 5: Restart node-3 ==="
docker compose -f docker-compose-e2e.yml start switchboard-3 docker compose -f docker-compose-e2e.yml start armature-3
# Wait for startup + at least one heartbeat # Wait for startup + at least one heartbeat
for i in $(seq 1 30); do for i in $(seq 1 30); do

View File

@@ -3,10 +3,10 @@ events {
} }
http { http {
upstream switchboard { upstream armature {
server switchboard-1:80; server armature-1:80;
server switchboard-2:80; server armature-2:80;
server switchboard-3:80; server armature-3:80;
} }
# WebSocket upgrade map # WebSocket upgrade map
@@ -19,7 +19,7 @@ http {
listen 80; listen 80;
location / { location / {
proxy_pass http://switchboard; proxy_pass http://armature;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -28,7 +28,7 @@ http {
# WebSocket endpoint # WebSocket endpoint
location /ws { location /ws {
proxy_pass http://switchboard; proxy_pass http://armature;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;

View File

@@ -83,11 +83,11 @@ authed() {
echo -e "\n${YELLOW}═══ Phase 1: Build Images ═══${NC}" echo -e "\n${YELLOW}═══ Phase 1: Build Images ═══${NC}"
echo "Building 'old' image from HEAD..." echo "Building 'old' image from HEAD..."
docker build -t switchboard-core:v-old . -q docker build -t armature:v-old . -q
ok "old image built" ok "old image built"
echo "Building 'new' image from working tree..." echo "Building 'new' image from working tree..."
docker build -t switchboard-core:v-new . -q docker build -t armature:v-new . -q
ok "new image built" ok "new image built"
# ═══════════════════════════════════════════════ # ═══════════════════════════════════════════════
@@ -97,7 +97,7 @@ ok "new image built"
echo -e "\n${YELLOW}═══ Phase 2: Start Cluster (Old Version) ═══${NC}" echo -e "\n${YELLOW}═══ Phase 2: Start Cluster (Old Version) ═══${NC}"
# Override the build with old image # Override the build with old image
SWITCHBOARD_IMAGE=switchboard-core:v-old \ ARMATURE_IMAGE=armature:v-old \
docker compose -f "$COMPOSE_FILE" up postgres -d docker compose -f "$COMPOSE_FILE" up postgres -d
# Wait for postgres # Wait for postgres
@@ -107,30 +107,30 @@ sleep 3
docker run -d --name sb-old-1 --network "$(basename "$(pwd)")_default" \ docker run -d --name sb-old-1 --network "$(basename "$(pwd)")_default" \
-e PORT=8080 -e BASE_PATH="" \ -e PORT=8080 -e BASE_PATH="" \
-e DB_DRIVER=postgres \ -e DB_DRIVER=postgres \
-e "DATABASE_URL=postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable" \ -e "DATABASE_URL=postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable" \
-e JWT_SECRET=e2e-jwt-secret \ -e JWT_SECRET=e2e-jwt-secret \
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \ -e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
-e SWITCHBOARD_ADMIN_USERNAME=admin \ -e ARMATURE_ADMIN_USERNAME=admin \
-e SWITCHBOARD_ADMIN_PASSWORD=admin \ -e ARMATURE_ADMIN_PASSWORD=admin \
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \ -e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \ -e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
-e LOG_FORMAT=text -e LOG_LEVEL=info \ -e LOG_FORMAT=text -e LOG_LEVEL=info \
-e "SEED_USERS=alice:password123:user,bob:password456:user" \ -e "SEED_USERS=alice:password123:user,bob:password456:user" \
-p 8081:80 switchboard-core:v-old 2>/dev/null || true -p 8081:80 armature:v-old 2>/dev/null || true
docker run -d --name sb-old-2 --network "$(basename "$(pwd)")_default" \ docker run -d --name sb-old-2 --network "$(basename "$(pwd)")_default" \
-e PORT=8080 -e BASE_PATH="" \ -e PORT=8080 -e BASE_PATH="" \
-e DB_DRIVER=postgres \ -e DB_DRIVER=postgres \
-e "DATABASE_URL=postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable" \ -e "DATABASE_URL=postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable" \
-e JWT_SECRET=e2e-jwt-secret \ -e JWT_SECRET=e2e-jwt-secret \
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \ -e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
-e SWITCHBOARD_ADMIN_USERNAME=admin \ -e ARMATURE_ADMIN_USERNAME=admin \
-e SWITCHBOARD_ADMIN_PASSWORD=admin \ -e ARMATURE_ADMIN_PASSWORD=admin \
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \ -e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \ -e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
-e LOG_FORMAT=text -e LOG_LEVEL=info \ -e LOG_FORMAT=text -e LOG_LEVEL=info \
-e "SEED_USERS=alice:password123:user,bob:password456:user" \ -e "SEED_USERS=alice:password123:user,bob:password456:user" \
-p 8082:80 switchboard-core:v-old 2>/dev/null || true -p 8082:80 armature:v-old 2>/dev/null || true
# Start nginx LB # Start nginx LB
docker compose -f "$COMPOSE_FILE" up lb -d docker compose -f "$COMPOSE_FILE" up lb -d
@@ -189,16 +189,16 @@ echo "Starting new replica-1..."
docker run -d --name sb-new-1 --network "$(basename "$(pwd)")_default" \ docker run -d --name sb-new-1 --network "$(basename "$(pwd)")_default" \
-e PORT=8080 -e BASE_PATH="" \ -e PORT=8080 -e BASE_PATH="" \
-e DB_DRIVER=postgres \ -e DB_DRIVER=postgres \
-e "DATABASE_URL=postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable" \ -e "DATABASE_URL=postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable" \
-e JWT_SECRET=e2e-jwt-secret \ -e JWT_SECRET=e2e-jwt-secret \
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \ -e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
-e SWITCHBOARD_ADMIN_USERNAME=admin \ -e ARMATURE_ADMIN_USERNAME=admin \
-e SWITCHBOARD_ADMIN_PASSWORD=admin \ -e ARMATURE_ADMIN_PASSWORD=admin \
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \ -e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \ -e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
-e LOG_FORMAT=text -e LOG_LEVEL=info \ -e LOG_FORMAT=text -e LOG_LEVEL=info \
-e "SEED_USERS=alice:password123:user,bob:password456:user" \ -e "SEED_USERS=alice:password123:user,bob:password456:user" \
-p 8081:80 switchboard-core:v-new -p 8081:80 armature:v-new
wait_for_health "$R1" "new replica-1" wait_for_health "$R1" "new replica-1"
@@ -246,16 +246,16 @@ echo "Starting new replica-2..."
docker run -d --name sb-new-2 --network "$(basename "$(pwd)")_default" \ docker run -d --name sb-new-2 --network "$(basename "$(pwd)")_default" \
-e PORT=8080 -e BASE_PATH="" \ -e PORT=8080 -e BASE_PATH="" \
-e DB_DRIVER=postgres \ -e DB_DRIVER=postgres \
-e "DATABASE_URL=postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable" \ -e "DATABASE_URL=postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable" \
-e JWT_SECRET=e2e-jwt-secret \ -e JWT_SECRET=e2e-jwt-secret \
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \ -e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
-e SWITCHBOARD_ADMIN_USERNAME=admin \ -e ARMATURE_ADMIN_USERNAME=admin \
-e SWITCHBOARD_ADMIN_PASSWORD=admin \ -e ARMATURE_ADMIN_PASSWORD=admin \
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \ -e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \ -e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
-e LOG_FORMAT=text -e LOG_LEVEL=info \ -e LOG_FORMAT=text -e LOG_LEVEL=info \
-e "SEED_USERS=alice:password123:user,bob:password456:user" \ -e "SEED_USERS=alice:password123:user,bob:password456:user" \
-p 8082:80 switchboard-core:v-new -p 8082:80 armature:v-new
wait_for_health "$R2" "new replica-2" wait_for_health "$R2" "new replica-2"

View File

@@ -82,13 +82,13 @@ echo -e "\n${YELLOW}═══ Phase 1: Build Images ═══${NC}"
# Build "old" image from last commit (before current changes) # Build "old" image from last commit (before current changes)
echo "Building 'old' image from HEAD commit..." echo "Building 'old' image from HEAD commit..."
git stash -q 2>/dev/null || true git stash -q 2>/dev/null || true
docker build --no-cache -t switchboard-core:v-old . -q docker build --no-cache -t armature:v-old . -q
git stash pop -q 2>/dev/null || true git stash pop -q 2>/dev/null || true
ok "old image built" ok "old image built"
# Build "new" image from working tree (with current changes) # Build "new" image from working tree (with current changes)
echo "Building 'new' image from working tree..." echo "Building 'new' image from working tree..."
docker build --no-cache -t core-switchboard-new . -q docker build --no-cache -t armature:v-new . -q
ok "new image built" ok "new image built"
# ═══════════════════════════════════════════════ # ═══════════════════════════════════════════════
@@ -97,7 +97,7 @@ ok "new image built"
echo -e "\n${YELLOW}═══ Phase 2: Seed Data on Old Version ═══${NC}" echo -e "\n${YELLOW}═══ Phase 2: Seed Data on Old Version ═══${NC}"
docker compose -f "$COMPOSE_FILE" up postgres switchboard-old -d docker compose -f "$COMPOSE_FILE" up postgres armature-old -d
wait_for_health "$HOST" wait_for_health "$HOST"
# Auth # Auth
@@ -179,17 +179,17 @@ ok "recorded $PRE_PKG_COUNT installed packages"
echo -e "\n${YELLOW}═══ Phase 3: Upgrade ═══${NC}" echo -e "\n${YELLOW}═══ Phase 3: Upgrade ═══${NC}"
echo "Stopping old version..." echo "Stopping old version..."
docker compose -f "$COMPOSE_FILE" stop switchboard-old docker compose -f "$COMPOSE_FILE" stop armature-old
ok "old version stopped" ok "old version stopped"
echo "Starting new version..." echo "Starting new version..."
docker compose -f "$COMPOSE_FILE" up switchboard-new -d docker compose -f "$COMPOSE_FILE" up armature-new -d
wait_for_health "$HOST" wait_for_health "$HOST"
ok "new version started" ok "new version started"
# Check for migration errors in logs # Check for migration errors in logs
echo -e "\n${YELLOW}Checking startup logs...${NC}" echo -e "\n${YELLOW}Checking startup logs...${NC}"
LOGS=$(docker compose -f "$COMPOSE_FILE" logs switchboard-new 2>&1 || echo "") LOGS=$(docker compose -f "$COMPOSE_FILE" logs armature-new 2>&1 || echo "")
if echo "$LOGS" | grep -qi "migration.*fail\|schema.*error\|panic\|fatal"; then if echo "$LOGS" | grep -qi "migration.*fail\|schema.*error\|panic\|fatal"; then
fail "migration errors found in logs" fail "migration errors found in logs"
echo "$LOGS" | grep -i "migration\|schema\|panic\|fatal" | head -5 echo "$LOGS" | grep -i "migration\|schema\|panic\|fatal" | head -5

View File

@@ -1,5 +1,5 @@
{ {
"realm": "switchboard", "realm": "armature",
"enabled": true, "enabled": true,
"registrationAllowed": false, "registrationAllowed": false,
"loginWithEmailAllowed": true, "loginWithEmailAllowed": true,
@@ -9,11 +9,11 @@
"realm": [ "realm": [
{ {
"name": "sb-admin", "name": "sb-admin",
"description": "Switchboard admin role" "description": "Armature admin role"
}, },
{ {
"name": "sb-user", "name": "sb-user",
"description": "Switchboard regular user role" "description": "Armature regular user role"
} }
] ]
}, },
@@ -29,11 +29,11 @@
], ],
"clients": [ "clients": [
{ {
"clientId": "switchboard", "clientId": "armature",
"name": "Switchboard Core", "name": "Armature",
"enabled": true, "enabled": true,
"publicClient": false, "publicClient": false,
"secret": "switchboard-secret", "secret": "armature-secret",
"redirectUris": [ "redirectUris": [
"http://localhost:3000/*", "http://localhost:3000/*",
"http://localhost:8080/*" "http://localhost:8080/*"
@@ -69,7 +69,7 @@
"users": [ "users": [
{ {
"username": "alice", "username": "alice",
"email": "alice@switchboard.test", "email": "alice@armature.test",
"firstName": "Alice", "firstName": "Alice",
"lastName": "Engineer", "lastName": "Engineer",
"enabled": true, "enabled": true,
@@ -83,7 +83,7 @@
], ],
"realmRoles": [ "realmRoles": [
"sb-user", "sb-user",
"default-roles-switchboard" "default-roles-armature"
], ],
"groups": [ "groups": [
"/engineering" "/engineering"
@@ -91,7 +91,7 @@
}, },
{ {
"username": "bob", "username": "bob",
"email": "bob@switchboard.test", "email": "bob@armature.test",
"firstName": "Bob", "firstName": "Bob",
"lastName": "Lead", "lastName": "Lead",
"enabled": true, "enabled": true,
@@ -106,7 +106,7 @@
"realmRoles": [ "realmRoles": [
"sb-admin", "sb-admin",
"sb-user", "sb-user",
"default-roles-switchboard" "default-roles-armature"
], ],
"groups": [ "groups": [
"/engineering", "/engineering",

View File

@@ -1,6 +1,6 @@
# docker-compose-e2e.yml — Multi-replica E2E testing (Postgres) # docker-compose-e2e.yml — Multi-replica E2E testing (Postgres)
# #
# Three Switchboard replicas behind an nginx load balancer, # Three Armature replicas behind an nginx load balancer,
# sharing a single Postgres instance. Tests cross-replica # sharing a single Postgres instance. Tests cross-replica
# broadcast via pg_notify and cluster registry (v0.6.0). # broadcast via pg_notify and cluster registry (v0.6.0).
# #
@@ -14,18 +14,18 @@ services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
environment: environment:
POSTGRES_DB: switchboard_e2e POSTGRES_DB: armature_e2e
POSTGRES_USER: switchboard POSTGRES_USER: armature
POSTGRES_PASSWORD: e2e-password POSTGRES_PASSWORD: e2e-password
ports: ports:
- "5432:5432" - "5432:5432"
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U switchboard -d switchboard_e2e"] test: ["CMD-SHELL", "pg_isready -U armature -d armature_e2e"]
interval: 2s interval: 2s
timeout: 5s timeout: 5s
retries: 10 retries: 10
switchboard-1: armature-1:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -33,11 +33,11 @@ services:
PORT: "8080" PORT: "8080"
BASE_PATH: "" BASE_PATH: ""
DB_DRIVER: postgres DB_DRIVER: postgres
DATABASE_URL: postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable DATABASE_URL: postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable
JWT_SECRET: e2e-jwt-secret JWT_SECRET: e2e-jwt-secret
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!! ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
SWITCHBOARD_ADMIN_USERNAME: admin ARMATURE_ADMIN_USERNAME: admin
SWITCHBOARD_ADMIN_PASSWORD: admin ARMATURE_ADMIN_PASSWORD: admin
STORAGE_BACKEND: pvc STORAGE_BACKEND: pvc
STORAGE_PATH: /data/storage STORAGE_PATH: /data/storage
CORS_ALLOWED_ORIGINS: "*" CORS_ALLOWED_ORIGINS: "*"
@@ -48,14 +48,14 @@ services:
CLUSTER_NODE_ID: "node-1" CLUSTER_NODE_ID: "node-1"
CLUSTER_HEARTBEAT_INTERVAL: "5s" CLUSTER_HEARTBEAT_INTERVAL: "5s"
CLUSTER_STALE_THRESHOLD: "15s" CLUSTER_STALE_THRESHOLD: "15s"
CLUSTER_ENDPOINT: "http://switchboard-1:8080" CLUSTER_ENDPOINT: "http://armature-1:8080"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
ports: ports:
- "8081:80" - "8081:80"
switchboard-2: armature-2:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -63,11 +63,11 @@ services:
PORT: "8080" PORT: "8080"
BASE_PATH: "" BASE_PATH: ""
DB_DRIVER: postgres DB_DRIVER: postgres
DATABASE_URL: postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable DATABASE_URL: postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable
JWT_SECRET: e2e-jwt-secret JWT_SECRET: e2e-jwt-secret
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!! ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
SWITCHBOARD_ADMIN_USERNAME: admin ARMATURE_ADMIN_USERNAME: admin
SWITCHBOARD_ADMIN_PASSWORD: admin ARMATURE_ADMIN_PASSWORD: admin
STORAGE_BACKEND: pvc STORAGE_BACKEND: pvc
STORAGE_PATH: /data/storage STORAGE_PATH: /data/storage
CORS_ALLOWED_ORIGINS: "*" CORS_ALLOWED_ORIGINS: "*"
@@ -78,14 +78,14 @@ services:
CLUSTER_NODE_ID: "node-2" CLUSTER_NODE_ID: "node-2"
CLUSTER_HEARTBEAT_INTERVAL: "5s" CLUSTER_HEARTBEAT_INTERVAL: "5s"
CLUSTER_STALE_THRESHOLD: "15s" CLUSTER_STALE_THRESHOLD: "15s"
CLUSTER_ENDPOINT: "http://switchboard-2:8080" CLUSTER_ENDPOINT: "http://armature-2:8080"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
ports: ports:
- "8082:80" - "8082:80"
switchboard-3: armature-3:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -93,11 +93,11 @@ services:
PORT: "8080" PORT: "8080"
BASE_PATH: "" BASE_PATH: ""
DB_DRIVER: postgres DB_DRIVER: postgres
DATABASE_URL: postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable DATABASE_URL: postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable
JWT_SECRET: e2e-jwt-secret JWT_SECRET: e2e-jwt-secret
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!! ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
SWITCHBOARD_ADMIN_USERNAME: admin ARMATURE_ADMIN_USERNAME: admin
SWITCHBOARD_ADMIN_PASSWORD: admin ARMATURE_ADMIN_PASSWORD: admin
STORAGE_BACKEND: pvc STORAGE_BACKEND: pvc
STORAGE_PATH: /data/storage STORAGE_PATH: /data/storage
CORS_ALLOWED_ORIGINS: "*" CORS_ALLOWED_ORIGINS: "*"
@@ -108,7 +108,7 @@ services:
CLUSTER_NODE_ID: "node-3" CLUSTER_NODE_ID: "node-3"
CLUSTER_HEARTBEAT_INTERVAL: "5s" CLUSTER_HEARTBEAT_INTERVAL: "5s"
CLUSTER_STALE_THRESHOLD: "15s" CLUSTER_STALE_THRESHOLD: "15s"
CLUSTER_ENDPOINT: "http://switchboard-3:8080" CLUSTER_ENDPOINT: "http://armature-3:8080"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@@ -122,6 +122,6 @@ services:
ports: ports:
- "3000:80" - "3000:80"
depends_on: depends_on:
- switchboard-1 - armature-1
- switchboard-2 - armature-2
- switchboard-3 - armature-3

View File

@@ -6,11 +6,11 @@
# docker compose -f docker-compose.yml -f docker-compose-keycloak.yml up # docker compose -f docker-compose.yml -f docker-compose-keycloak.yml up
# #
# This extends the base docker-compose.yml to add: # This extends the base docker-compose.yml to add:
# - Keycloak IdP with a pre-configured "switchboard" realm # - Keycloak IdP with a pre-configured "armature" realm
# - Switchboard configured with AUTH_MODE=oidc pointing at Keycloak # - Armature configured with AUTH_MODE=oidc pointing at Keycloak
# #
# After startup: # After startup:
# Switchboard: http://localhost:3000 # Armature: http://localhost:3000
# Keycloak: http://localhost:8180 (admin / admin) # Keycloak: http://localhost:8180 (admin / admin)
# #
# Pre-configured test users in Keycloak: # Pre-configured test users in Keycloak:
@@ -21,14 +21,14 @@
# ============================================ # ============================================
services: services:
# Override switchboard to use OIDC # Override armature to use OIDC
switchboard: armature:
environment: environment:
AUTH_MODE: oidc AUTH_MODE: oidc
OIDC_ISSUER_URL: http://keycloak:8080/realms/switchboard OIDC_ISSUER_URL: http://keycloak:8080/realms/armature
OIDC_EXTERNAL_ISSUER_URL: http://localhost:8180/realms/switchboard OIDC_EXTERNAL_ISSUER_URL: http://localhost:8180/realms/armature
OIDC_CLIENT_ID: switchboard OIDC_CLIENT_ID: armature
OIDC_CLIENT_SECRET: switchboard-secret OIDC_CLIENT_SECRET: armature-secret
OIDC_REDIRECT_URL: http://localhost:3000/api/v1/auth/oidc/callback OIDC_REDIRECT_URL: http://localhost:3000/api/v1/auth/oidc/callback
OIDC_AUTO_ACTIVATE: "true" OIDC_AUTO_ACTIVATE: "true"
OIDC_ADMIN_ROLE: sb-admin OIDC_ADMIN_ROLE: sb-admin
@@ -38,7 +38,7 @@ services:
keycloak: keycloak:
image: quay.io/keycloak/keycloak:26.0 image: quay.io/keycloak/keycloak:26.0
container_name: switchboard-keycloak container_name: armature-keycloak
command: command:
- start-dev - start-dev
- --import-realm - --import-realm
@@ -48,7 +48,7 @@ services:
KC_HTTP_PORT: "8080" KC_HTTP_PORT: "8080"
KC_HEALTH_ENABLED: "true" KC_HEALTH_ENABLED: "true"
volumes: volumes:
- ./ci/keycloak-realm.json:/opt/keycloak/data/import/switchboard-realm.json:ro - ./ci/keycloak-realm.json:/opt/keycloak/data/import/armature-realm.json:ro
ports: ports:
- "8180:8080" - "8180:8080"
healthcheck: healthcheck:

View File

@@ -7,11 +7,11 @@
# ci/e2e-upgrade-test.sh (orchestrates build → seed → upgrade → verify) # ci/e2e-upgrade-test.sh (orchestrates build → seed → upgrade → verify)
# #
# Manual usage: # Manual usage:
# docker build -t switchboard-core:v-old . # docker build -t armature:v-old .
# docker compose -f docker-compose-upgrade.yml up postgres switchboard-old -d # docker compose -f docker-compose-upgrade.yml up postgres armature-old -d
# # ... seed data ... # # ... seed data ...
# docker compose -f docker-compose-upgrade.yml stop switchboard-old # docker compose -f docker-compose-upgrade.yml stop armature-old
# docker compose -f docker-compose-upgrade.yml up switchboard-new -d # docker compose -f docker-compose-upgrade.yml up armature-new -d
# # ... verify data ... # # ... verify data ...
# docker compose -f docker-compose-upgrade.yml down -v # docker compose -f docker-compose-upgrade.yml down -v
@@ -19,28 +19,28 @@ services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
environment: environment:
POSTGRES_DB: switchboard_upgrade POSTGRES_DB: armature_upgrade
POSTGRES_USER: switchboard POSTGRES_USER: armature
POSTGRES_PASSWORD: upgrade-password POSTGRES_PASSWORD: upgrade-password
ports: ports:
- "5433:5432" - "5433:5432"
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U switchboard -d switchboard_upgrade"] test: ["CMD-SHELL", "pg_isready -U armature -d armature_upgrade"]
interval: 2s interval: 2s
timeout: 5s timeout: 5s
retries: 10 retries: 10
switchboard-old: armature-old:
image: switchboard-core:v-old image: armature:v-old
environment: environment:
PORT: "8080" PORT: "8080"
BASE_PATH: "" BASE_PATH: ""
DB_DRIVER: postgres DB_DRIVER: postgres
DATABASE_URL: postgres://switchboard:upgrade-password@postgres:5432/switchboard_upgrade?sslmode=disable DATABASE_URL: postgres://armature:upgrade-password@postgres:5432/armature_upgrade?sslmode=disable
JWT_SECRET: upgrade-jwt-secret JWT_SECRET: upgrade-jwt-secret
ENCRYPTION_KEY: upgrade-encryption-key-32chars!! ENCRYPTION_KEY: upgrade-encryption-key-32chars!!
SWITCHBOARD_ADMIN_USERNAME: admin ARMATURE_ADMIN_USERNAME: admin
SWITCHBOARD_ADMIN_PASSWORD: admin ARMATURE_ADMIN_PASSWORD: admin
STORAGE_BACKEND: pvc STORAGE_BACKEND: pvc
STORAGE_PATH: /data/storage STORAGE_PATH: /data/storage
CORS_ALLOWED_ORIGINS: "*" CORS_ALLOWED_ORIGINS: "*"
@@ -56,17 +56,17 @@ services:
volumes: volumes:
- upgrade_storage:/data/storage - upgrade_storage:/data/storage
switchboard-new: armature-new:
image: core-switchboard-new image: core-armature-new
environment: environment:
PORT: "8080" PORT: "8080"
BASE_PATH: "" BASE_PATH: ""
DB_DRIVER: postgres DB_DRIVER: postgres
DATABASE_URL: postgres://switchboard:upgrade-password@postgres:5432/switchboard_upgrade?sslmode=disable DATABASE_URL: postgres://armature:upgrade-password@postgres:5432/armature_upgrade?sslmode=disable
JWT_SECRET: upgrade-jwt-secret JWT_SECRET: upgrade-jwt-secret
ENCRYPTION_KEY: upgrade-encryption-key-32chars!! ENCRYPTION_KEY: upgrade-encryption-key-32chars!!
SWITCHBOARD_ADMIN_USERNAME: admin ARMATURE_ADMIN_USERNAME: admin
SWITCHBOARD_ADMIN_PASSWORD: admin ARMATURE_ADMIN_PASSWORD: admin
STORAGE_BACKEND: pvc STORAGE_BACKEND: pvc
STORAGE_PATH: /data/storage STORAGE_PATH: /data/storage
CORS_ALLOWED_ORIGINS: "*" CORS_ALLOWED_ORIGINS: "*"

View File

@@ -13,20 +13,20 @@
# For Postgres / multi-replica / k8s deployment see k8s/ and Dockerfile. # For Postgres / multi-replica / k8s deployment see k8s/ and Dockerfile.
services: services:
switchboard: armature:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: switchboard-core container_name: armature
environment: environment:
PORT: "8080" PORT: "8080"
BASE_PATH: "" BASE_PATH: ""
DB_DRIVER: sqlite DB_DRIVER: sqlite
DATABASE_URL: /data/switchboard.db DATABASE_URL: /data/armature.db
JWT_SECRET: ${JWT_SECRET:-change-me-for-production} JWT_SECRET: ${JWT_SECRET:-change-me-for-production}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-change-me-for-production} ENCRYPTION_KEY: ${ENCRYPTION_KEY:-change-me-for-production}
SWITCHBOARD_ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} ARMATURE_ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
SWITCHBOARD_ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin} ARMATURE_ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin}
STORAGE_BACKEND: pvc STORAGE_BACKEND: pvc
STORAGE_PATH: /data/storage STORAGE_PATH: /data/storage
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000} CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000}

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ============================================ # ============================================
# Switchboard Core - Backend Launcher # Armature - Backend Launcher
# ============================================ # ============================================
# Runs as an nginx entrypoint.d hook. # Runs as an nginx entrypoint.d hook.
# Starts the Go backend in the background # Starts the Go backend in the background
@@ -20,15 +20,15 @@ else
-exec sed -i "s|%%BASE_PATH%%||g" {} + -exec sed -i "s|%%BASE_PATH%%||g" {} +
fi fi
echo "🔀 Starting Switchboard Core backend..." echo "🔀 Starting Armature backend..."
# Kill any stale backend from a previous entrypoint run # Kill any stale backend from a previous entrypoint run
pkill -f /usr/local/bin/switchboard 2>/dev/null || true pkill -f /usr/local/bin/armature 2>/dev/null || true
sleep 0.2 sleep 0.2
# Launch Go backend in background (from /app so migrations are found) # Launch Go backend in background (from /app so migrations are found)
cd /app cd /app
/usr/local/bin/switchboard & /usr/local/bin/armature &
BACKEND_PID=$! BACKEND_PID=$!
# Wait for backend to be ready (max 60s) # Wait for backend to be ready (max 60s)

View File

@@ -1,6 +1,6 @@
# Switchboard Core — Architecture # Armature — Architecture
Switchboard Core is a self-hosted extension platform. It provides identity, Armature is a self-hosted extension platform. It provides identity,
teams, permissions, storage, workflows, notifications, and a package system. teams, permissions, storage, workflows, notifications, and a package system.
Everything else — chat, AI providers, personas, knowledge bases, notes, Everything else — chat, AI providers, personas, knowledge bases, notes,
tools — ships as installable extensions. tools — ships as installable extensions.
@@ -284,7 +284,7 @@ Single Docker image: Go binary + migrations + frontend assets + vendor
libs. Kubernetes deployment with 3-node PG cluster. CI via Gitea Actions libs. Kubernetes deployment with 3-node PG cluster. CI via Gitea Actions
with DaemonSet DinD runners testing both PG and SQLite pipelines. with DaemonSet DinD runners testing both PG and SQLite pipelines.
Registry: `registry.gobha.me:5000/xcaliber/switchboard-core` Registry: `registry.gobha.me:5000/xcaliber/armature`
Namespace: `gobha-ai-chat` Namespace: `gobha-ai-chat`
### Cluster Topology ### Cluster Topology

View File

@@ -280,7 +280,7 @@ def on_fire(ctx):
resp = http.post(url, body=payload, headers={ resp = http.post(url, body=payload, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Switchboard-Event": data.get("event_name", "workflow.notify") "X-Armature-Event": data.get("event_name", "workflow.notify")
}) })
# Log the delivery # Log the delivery

View File

@@ -3,14 +3,14 @@
## Docker Single-Instance ## Docker Single-Instance
```bash ```bash
docker pull ghcr.io/switchboard-core/switchboard-core:latest docker pull ghcr.io/armature/armature:latest
docker run -p 8080:80 \ docker run -p 8080:80 \
-e SWITCHBOARD_ADMIN_USERNAME=admin \ -e ARMATURE_ADMIN_USERNAME=admin \
-e SWITCHBOARD_ADMIN_PASSWORD=changeme \ -e ARMATURE_ADMIN_PASSWORD=changeme \
-e JWT_SECRET="$(openssl rand -hex 32)" \ -e JWT_SECRET="$(openssl rand -hex 32)" \
-e ENCRYPTION_KEY="$(openssl rand -hex 32)" \ -e ENCRYPTION_KEY="$(openssl rand -hex 32)" \
-v switchboard-data:/data \ -v armature-data:/data \
ghcr.io/switchboard-core/switchboard-core:latest ghcr.io/armature/armature:latest
``` ```
This runs with SQLite and PVC storage. Suitable for evaluation and small teams. This runs with SQLite and PVC storage. Suitable for evaluation and small teams.
@@ -22,22 +22,22 @@ services:
postgres: postgres:
image: postgres:16 image: postgres:16
environment: environment:
POSTGRES_DB: switchboard POSTGRES_DB: armature
POSTGRES_USER: switchboard POSTGRES_USER: armature
POSTGRES_PASSWORD: secretpassword POSTGRES_PASSWORD: secretpassword
volumes: volumes:
- pg_data:/var/lib/postgresql/data - pg_data:/var/lib/postgresql/data
switchboard: armature:
image: ghcr.io/switchboard-core/switchboard-core:latest image: ghcr.io/armature/armature:latest
ports: ports:
- "8080:80" - "8080:80"
environment: environment:
DATABASE_URL: "postgres://switchboard:secretpassword@postgres:5432/switchboard?sslmode=disable" DATABASE_URL: "postgres://armature:secretpassword@postgres:5432/armature?sslmode=disable"
JWT_SECRET: "change-me-in-production" JWT_SECRET: "change-me-in-production"
ENCRYPTION_KEY: "change-me-in-production" ENCRYPTION_KEY: "change-me-in-production"
SWITCHBOARD_ADMIN_USERNAME: admin ARMATURE_ADMIN_USERNAME: admin
SWITCHBOARD_ADMIN_PASSWORD: changeme ARMATURE_ADMIN_PASSWORD: changeme
STORAGE_BACKEND: pvc STORAGE_BACKEND: pvc
STORAGE_PATH: /data/storage STORAGE_PATH: /data/storage
volumes: volumes:
@@ -58,7 +58,7 @@ See the `k8s/` directory for example manifests. Key considerations:
- Liveness probe: `/healthz/live`. Readiness probe: `/healthz/ready`. - Liveness probe: `/healthz/live`. Readiness probe: `/healthz/ready`.
- Mount a PVC at `/data/storage` or configure S3. - Mount a PVC at `/data/storage` or configure S3.
- Store `JWT_SECRET` and `ENCRYPTION_KEY` in Kubernetes Secrets. - Store `JWT_SECRET` and `ENCRYPTION_KEY` in Kubernetes Secrets.
- Registry: `registry.gobha.me:5000/xcaliber/switchboard-core`. - Registry: `registry.gobha.me:5000/xcaliber/armature`.
## Environment Variables ## Environment Variables
@@ -72,15 +72,15 @@ See the `k8s/` directory for example manifests. Key considerations:
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, `oidc` | | `AUTH_MODE` | `builtin` | `builtin`, `mtls`, `oidc` |
| `STORAGE_BACKEND` | auto | `pvc` or `s3` | | `STORAGE_BACKEND` | auto | `pvc` or `s3` |
| `STORAGE_PATH` | `/data/storage` | PVC mount point | | `STORAGE_PATH` | `/data/storage` | PVC mount point |
| `BASE_PATH` | | URL prefix (e.g., `/switchboard`) | | `BASE_PATH` | | URL prefix (e.g., `/armature`) |
| `LOG_FORMAT` | `text` | `text` or `json` | | `LOG_FORMAT` | `text` | `text` or `json` |
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` | | `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
| `CORS_ALLOWED_ORIGINS` | | Comma-separated allowed origins | | `CORS_ALLOWED_ORIGINS` | | Comma-separated allowed origins |
| `BUNDLED_PACKAGES` | (empty) | `""` defaults, `"*"` all, or comma-separated | | `BUNDLED_PACKAGES` | (empty) | `""` defaults, `"*"` all, or comma-separated |
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable bundled package install | | `SKIP_BUNDLED_PACKAGES` | `false` | Disable bundled package install |
| `BUNDLED_PACKAGES_DIR` | `/app/bundled-packages` | Custom bundle directory | | `BUNDLED_PACKAGES_DIR` | `/app/bundled-packages` | Custom bundle directory |
| `SWITCHBOARD_ADMIN_USERNAME` | | Bootstrap admin username | | `ARMATURE_ADMIN_USERNAME` | | Bootstrap admin username |
| `SWITCHBOARD_ADMIN_PASSWORD` | | Bootstrap admin password | | `ARMATURE_ADMIN_PASSWORD` | | Bootstrap admin password |
| `SEED_USERS` | | Dev seed users (ignored in production) | | `SEED_USERS` | | Dev seed users (ignored in production) |
### S3 Storage Variables ### S3 Storage Variables
@@ -107,14 +107,14 @@ See the `k8s/` directory for example manifests. Key considerations:
**PostgreSQL** (recommended for production): **PostgreSQL** (recommended for production):
```bash ```bash
DATABASE_URL="postgres://user:pass@host:5432/switchboard?sslmode=require" DATABASE_URL="postgres://user:pass@host:5432/armature?sslmode=require"
``` ```
**SQLite** (dev, test, edge deployments): **SQLite** (dev, test, edge deployments):
```bash ```bash
DB_DRIVER=sqlite DB_DRIVER=sqlite
DATABASE_URL=/data/switchboard.db DATABASE_URL=/data/armature.db
``` ```
Both databases are first-class -- every query compiles and passes tests on both. The driver is auto-detected from `DATABASE_URL` if `DB_DRIVER` is not set. Both databases are first-class -- every query compiles and passes tests on both. The driver is auto-detected from `DATABASE_URL` if `DB_DRIVER` is not set.

View File

@@ -9,7 +9,7 @@
## Problem ## Problem
Switchboard-core is tightly coupled to PostgreSQL. Scaling horizontally requires instance coordination: peer discovery, health monitoring, ephemeral event routing, and (optionally) leader election. Traditional HA solutions (Raft, etcd, Consul) introduce a second consensus layer on top of PG — doubling operational complexity for a system that already has serializable transactions and LISTEN/NOTIFY. Armature-core is tightly coupled to PostgreSQL. Scaling horizontally requires instance coordination: peer discovery, health monitoring, ephemeral event routing, and (optionally) leader election. Traditional HA solutions (Raft, etcd, Consul) introduce a second consensus layer on top of PG — doubling operational complexity for a system that already has serializable transactions and LISTEN/NOTIFY.
## Principle ## Principle
@@ -241,7 +241,7 @@ Pre-MVP: fold `CREATE UNLOGGED TABLE` into existing schema initialization. No ne
### Single-Node Behavior ### Single-Node Behavior
When only one node is registered, the system behaves identically to pre-cluster switchboard-core. The registry has one row. LISTEN/NOTIFY delivers events back to the same instance. No special-casing required. When only one node is registered, the system behaves identically to pre-cluster armature. The registry has one row. LISTEN/NOTIFY delivers events back to the same instance. No special-casing required.
### Health Endpoint Integration ### Health Endpoint Integration

View File

@@ -172,17 +172,17 @@ their own `node-N.key`.
### Cert Provisioning Tooling ### Cert Provisioning Tooling
A shell script (`scripts/switchboard-ca.sh`) wrapping `openssl` is the A shell script (`scripts/armature-ca.sh`) wrapping `openssl` is the
KISS path. No new binary, no new dependency. Three commands: KISS path. No new binary, no new dependency. Three commands:
``` ```
switchboard-ca init armature-ca init
→ generates cluster-ca.crt + cluster-ca.key in ./ca/ → generates cluster-ca.crt + cluster-ca.key in ./ca/
switchboard-ca issue-node --name node-1 --san "node-1.internal,10.0.0.1" armature-ca issue-node --name node-1 --san "node-1.internal,10.0.0.1"
→ generates node-1.crt + node-1.key in ./nodes/ → generates node-1.crt + node-1.key in ./nodes/
switchboard-ca issue-user --cn jeff [--email jeff@example.com] armature-ca issue-user --cn jeff [--email jeff@example.com]
→ generates jeff.crt + jeff.key in ./users/ → generates jeff.crt + jeff.key in ./users/
``` ```
@@ -280,7 +280,7 @@ The new code is:
| `server/auth/mtls_helpers.go` | Shared: `ParseDN`, `FingerprintCert`, `resolveOrProvision` | | `server/auth/mtls_helpers.go` | Shared: `ParseDN`, `FingerprintCert`, `resolveOrProvision` |
| `server/auth/mtls_native_test.go` | Unit + integration tests | | `server/auth/mtls_native_test.go` | Unit + integration tests |
| `server/config/tls.go` | `TLSConfig` struct, loader, validation | | `server/config/tls.go` | `TLSConfig` struct, loader, validation |
| `scripts/switchboard-ca.sh` | Cert provisioning wrapper | | `scripts/armature-ca.sh` | Cert provisioning wrapper |
`server/auth/mtls.go` (existing) is renamed to `mtls_proxy.go` for `server/auth/mtls.go` (existing) is renamed to `mtls_proxy.go` for
clarity. No behavioral changes to the proxy provider. clarity. No behavioral changes to the proxy provider.

View File

@@ -3,11 +3,11 @@
## Quick Start ## Quick Start
```bash ```bash
docker pull ghcr.io/switchboard-core/switchboard-core:latest docker pull ghcr.io/armature/armature:latest
docker run -p 8080:80 \ docker run -p 8080:80 \
-e SWITCHBOARD_ADMIN_USERNAME=admin \ -e ARMATURE_ADMIN_USERNAME=admin \
-e SWITCHBOARD_ADMIN_PASSWORD=changeme \ -e ARMATURE_ADMIN_PASSWORD=changeme \
ghcr.io/switchboard-core/switchboard-core:latest ghcr.io/armature/armature:latest
``` ```
On first run, bundled packages are automatically installed — workflows, surfaces, and extensions are ready to use immediately. On first run, bundled packages are automatically installed — workflows, surfaces, and extensions are ready to use immediately.
@@ -66,12 +66,12 @@ Set `BUNDLED_PACKAGES` to control which packages are installed:
# Install ALL packages (everything in the image) # Install ALL packages (everything in the image)
docker run -p 8080:80 \ docker run -p 8080:80 \
-e BUNDLED_PACKAGES="*" \ -e BUNDLED_PACKAGES="*" \
ghcr.io/switchboard-core/switchboard-core:latest ghcr.io/armature/armature:latest
# Install specific packages only # Install specific packages only
docker run -p 8080:80 \ docker run -p 8080:80 \
-e BUNDLED_PACKAGES="notes,tasks,schedules" \ -e BUNDLED_PACKAGES="notes,tasks,schedules" \
ghcr.io/switchboard-core/switchboard-core:latest ghcr.io/armature/armature:latest
``` ```
Empty (default) installs the curated default set. Use `*` to install all packages. This is useful for Helm charts where different environments need different packages. Empty (default) installs the curated default set. Use `*` to install all packages. This is useful for Helm charts where different environments need different packages.
@@ -83,7 +83,7 @@ Set `SKIP_BUNDLED_PACKAGES=true` to prevent bundled packages from being installe
```bash ```bash
docker run -p 8080:80 \ docker run -p 8080:80 \
-e SKIP_BUNDLED_PACKAGES=true \ -e SKIP_BUNDLED_PACKAGES=true \
ghcr.io/switchboard-core/switchboard-core:latest ghcr.io/armature/armature:latest
``` ```
### Custom Bundle Directory ### Custom Bundle Directory
@@ -94,7 +94,7 @@ Override the default bundled packages location with `BUNDLED_PACKAGES_DIR`:
docker run -p 8080:80 \ docker run -p 8080:80 \
-e BUNDLED_PACKAGES_DIR=/custom/packages \ -e BUNDLED_PACKAGES_DIR=/custom/packages \
-v /host/packages:/custom/packages \ -v /host/packages:/custom/packages \
ghcr.io/switchboard-core/switchboard-core:latest ghcr.io/armature/armature:latest
``` ```
## Builder Image ## Builder Image
@@ -102,7 +102,7 @@ docker run -p 8080:80 \
The builder image pre-caches Go modules and Node dependencies for faster custom builds. The builder image pre-caches Go modules and Node dependencies for faster custom builds.
```bash ```bash
docker pull ghcr.io/switchboard-core/builder:latest docker pull ghcr.io/armature/builder:latest
``` ```
### What It Caches ### What It Caches
@@ -117,16 +117,16 @@ docker pull ghcr.io/switchboard-core/builder:latest
Reference the builder image as a base stage in your Dockerfile: Reference the builder image as a base stage in your Dockerfile:
```dockerfile ```dockerfile
FROM ghcr.io/switchboard-core/builder:latest AS builder FROM ghcr.io/armature/builder:latest AS builder
WORKDIR /app WORKDIR /app
COPY server/ . COPY server/ .
RUN go build -ldflags="-s -w" -o /bin/switchboard . RUN go build -ldflags="-s -w" -o /bin/armature .
``` ```
### Building Locally ### Building Locally
```bash ```bash
docker build -f Dockerfile.builder -t switchboard-builder . docker build -f Dockerfile.builder -t armature-builder .
``` ```
## Custom Build Guide ## Custom Build Guide
@@ -135,7 +135,7 @@ docker build -f Dockerfile.builder -t switchboard-builder .
1. Create your package in `packages/your-package/` with a `manifest.json` 1. Create your package in `packages/your-package/` with a `manifest.json`
2. Build all packages: `cd packages && bash build.sh all` 2. Build all packages: `cd packages && bash build.sh all`
3. Build the Docker image: `docker build -t my-switchboard .` 3. Build the Docker image: `docker build -t my-armature .`
The Dockerfile automatically builds all packages in the `packages/` directory and bundles them into the production image. The Dockerfile automatically builds all packages in the `packages/` directory and bundles them into the production image.
@@ -148,14 +148,14 @@ To exclude specific packages from the bundle, either:
### Forking for Custom Builds ### Forking for Custom Builds
```bash ```bash
git clone https://github.com/switchboard-core/switchboard-core.git git clone https://github.com/armature/armature.git
cd switchboard-core cd armature
# Add/modify packages # Add/modify packages
cp -r my-extension packages/my-extension/ cp -r my-extension packages/my-extension/
# Build with builder image for faster compilation # Build with builder image for faster compilation
docker build -t my-switchboard . docker build -t my-armature .
``` ```
## Production Deployment ## Production Deployment
@@ -172,7 +172,7 @@ docker build -t my-switchboard .
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, or `oidc` | | `AUTH_MODE` | `builtin` | `builtin`, `mtls`, or `oidc` |
| `STORAGE_BACKEND` | (auto) | `pvc` or `s3` | | `STORAGE_BACKEND` | (auto) | `pvc` or `s3` |
| `STORAGE_PATH` | `/data/storage` | PVC mount point | | `STORAGE_PATH` | `/data/storage` | PVC mount point |
| `BASE_PATH` | | URL prefix (e.g. `/switchboard`) | | `BASE_PATH` | | URL prefix (e.g. `/armature`) |
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable auto-install of bundled packages | | `SKIP_BUNDLED_PACKAGES` | `false` | Disable auto-install of bundled packages |
| `BUNDLED_PACKAGES` | (empty = defaults) | `""` curated defaults, `"*"` all, or comma-separated IDs | | `BUNDLED_PACKAGES` | (empty = defaults) | `""` curated defaults, `"*"` all, or comma-separated IDs |
| `BUNDLED_PACKAGES_DIR` | `/app/bundled-packages` | Override bundled packages location | | `BUNDLED_PACKAGES_DIR` | `/app/bundled-packages` | Override bundled packages location |
@@ -186,16 +186,16 @@ PostgreSQL is recommended for production. SQLite is suitable for single-instance
```bash ```bash
# PostgreSQL (recommended) # PostgreSQL (recommended)
docker run -p 8080:80 \ docker run -p 8080:80 \
-e DATABASE_URL="postgres://user:pass@host:5432/switchboard?sslmode=require" \ -e DATABASE_URL="postgres://user:pass@host:5432/armature?sslmode=require" \
-e JWT_SECRET="$(openssl rand -hex 32)" \ -e JWT_SECRET="$(openssl rand -hex 32)" \
-e ENCRYPTION_KEY="$(openssl rand -hex 32)" \ -e ENCRYPTION_KEY="$(openssl rand -hex 32)" \
ghcr.io/switchboard-core/switchboard-core:latest ghcr.io/armature/armature:latest
# SQLite (evaluation only) # SQLite (evaluation only)
docker run -p 8080:80 \ docker run -p 8080:80 \
-e DB_DRIVER=sqlite \ -e DB_DRIVER=sqlite \
-v switchboard-data:/data \ -v armature-data:/data \
ghcr.io/switchboard-core/switchboard-core:latest ghcr.io/armature/armature:latest
``` ```
### Storage ### Storage
@@ -204,12 +204,12 @@ Object storage is required for file uploads and package asset extraction.
```bash ```bash
# PVC (auto-detected if path is writable) # PVC (auto-detected if path is writable)
docker run -v switchboard-storage:/data/storage ... docker run -v armature-storage:/data/storage ...
# S3-compatible (MinIO, AWS S3, Ceph) # S3-compatible (MinIO, AWS S3, Ceph)
docker run \ docker run \
-e STORAGE_BACKEND=s3 \ -e STORAGE_BACKEND=s3 \
-e S3_BUCKET=switchboard \ -e S3_BUCKET=armature \
-e S3_ENDPOINT=https://minio.corp:9000 \ -e S3_ENDPOINT=https://minio.corp:9000 \
-e S3_ACCESS_KEY=... \ -e S3_ACCESS_KEY=... \
-e S3_SECRET_KEY=... \ -e S3_SECRET_KEY=... \

View File

@@ -20,10 +20,10 @@ docker compose down -v
## First Boot ## First Boot
On first start, Switchboard Core will: On first start, Armature will:
1. Run database migrations (SQLite by default in compose). 1. Run database migrations (SQLite by default in compose).
2. Create the admin user from `SWITCHBOARD_ADMIN_USERNAME` / `SWITCHBOARD_ADMIN_PASSWORD` env vars. 2. Create the admin user from `ARMATURE_ADMIN_USERNAME` / `ARMATURE_ADMIN_PASSWORD` env vars.
3. Auto-install the curated default package set (notes, chat-core, dashboard, workflow demos, etc.). 3. Auto-install the curated default package set (notes, chat-core, dashboard, workflow demos, etc.).
No manual setup steps are required. No manual setup steps are required.
@@ -67,8 +67,8 @@ You can also upload `.pkg` archives through the Admin > Packages page.
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, or `oidc` | | `AUTH_MODE` | `builtin` | `builtin`, `mtls`, or `oidc` |
| `STORAGE_BACKEND` | auto | `pvc` or `s3` | | `STORAGE_BACKEND` | auto | `pvc` or `s3` |
| `STORAGE_PATH` | `/data/storage` | PVC mount point | | `STORAGE_PATH` | `/data/storage` | PVC mount point |
| `SWITCHBOARD_ADMIN_USERNAME` | | Bootstrap admin username | | `ARMATURE_ADMIN_USERNAME` | | Bootstrap admin username |
| `SWITCHBOARD_ADMIN_PASSWORD` | | Bootstrap admin password | | `ARMATURE_ADMIN_PASSWORD` | | Bootstrap admin password |
| `BUNDLED_PACKAGES` | (empty) | `""` = curated defaults, `"*"` = all, or comma-separated IDs | | `BUNDLED_PACKAGES` | (empty) | `""` = curated defaults, `"*"` = all, or comma-separated IDs |
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable auto-install entirely | | `SKIP_BUNDLED_PACKAGES` | `false` | Disable auto-install entirely |
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` | | `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
@@ -77,7 +77,7 @@ You can also upload `.pkg` archives through the Admin > Packages page.
## From Source ## From Source
```bash ```bash
git clone <repo-url> && cd switchboard-core git clone <repo-url> && cd armature
cp server/.env.example server/.env # edit DB credentials cp server/.env.example server/.env # edit DB credentials
cd server && go run . cd server && go run .
# Backend on http://localhost:8080 # Backend on http://localhost:8080

View File

@@ -1,6 +1,6 @@
# Package Format # Package Format
Switchboard packages are distributed as `.pkg` files -- ZIP archives with a standard internal structure. Armature packages are distributed as `.pkg` files -- ZIP archives with a standard internal structure.
## ZIP Structure ## ZIP Structure
@@ -125,6 +125,6 @@ To bundle custom packages into a Docker image:
1. Place your package in `packages/your-package/` with a `manifest.json`. 1. Place your package in `packages/your-package/` with a `manifest.json`.
2. Run `cd packages && bash build.sh all`. 2. Run `cd packages && bash build.sh all`.
3. Build the image: `docker build -t my-switchboard .` 3. Build the image: `docker build -t my-armature .`
The Dockerfile builds all packages and copies them into the bundled packages directory. The Dockerfile builds all packages and copies them into the bundled packages directory.

View File

@@ -15,7 +15,7 @@ The registry is a static JSON file matching the `RegistryResponse` struct:
"title": "Notes", "title": "Notes",
"version": "0.8.0", "version": "0.8.0",
"description": "Markdown notes with backlinks and graph view", "description": "Markdown notes with backlinks and graph view",
"author": "switchboard", "author": "armature",
"type": "extension", "type": "extension",
"tier": "core", "tier": "core",
"download_url": "https://cdn.example.com/pkg/notes.pkg", "download_url": "https://cdn.example.com/pkg/notes.pkg",

View File

@@ -1,9 +1,9 @@
# Build Your First Browser Extension # Build Your First Browser Extension
This tutorial walks through building a browser extension that renders custom This tutorial walks through building a browser extension that renders custom
code blocks, modeled on the CSV Table Viewer that ships with Switchboard. code blocks, modeled on the CSV Table Viewer that ships with Armature.
**Prerequisites:** A running Switchboard instance, a text editor, and `zip`. **Prerequisites:** A running Armature instance, a text editor, and `zip`.
## Step 1: Create the Directory ## Step 1: Create the Directory

View File

@@ -1,14 +1,14 @@
# k8s/switchboard.yaml # k8s/armature.yaml
# ============================================ # ============================================
# Switchboard Core - Deployment # Armature Core - Deployment
# ============================================ # ============================================
# Secrets: # Secrets:
# switchboard-db-credentials - POSTGRES_USER, POSTGRES_PASSWORD # armature-db-credentials - POSTGRES_USER, POSTGRES_PASSWORD
# switchboard-admin - username, password, email (optional) # armature-admin - username, password, email (optional)
# switchboard-encryption - ENCRYPTION_KEY (required for v0.9.4+) # armature-encryption - ENCRYPTION_KEY (required for v0.9.4+)
# #
# PVC: # PVC:
# switchboard-storage - Extraction scratch (always needed) # armature-storage - Extraction scratch (always needed)
# Requires ReadWriteMany (RWX) — see k8s/storage-pvc.yaml # Requires ReadWriteMany (RWX) — see k8s/storage-pvc.yaml
# With S3 backend, PVC is small (extraction status only, not blobs) # With S3 backend, PVC is small (extraction status only, not blobs)
# #
@@ -23,32 +23,32 @@
# S3_ACCESS_KEY - S3 access key # S3_ACCESS_KEY - S3 access key
# S3_SECRET_KEY - S3 secret key # S3_SECRET_KEY - S3 secret key
# #
# Admin bootstrap: on every pod start, if SWITCHBOARD_ADMIN_* env vars # Admin bootstrap: on every pod start, if ARMATURE_ADMIN_* env vars
# are set, the backend upserts an admin user. Change the secret and # are set, the backend upserts an admin user. Change the secret and
# restart the pod to reset the admin password. # restart the pod to reset the admin password.
# ============================================ # ============================================
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: switchboard${DEPLOY_SUFFIX} name: armature${DEPLOY_SUFFIX}
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
labels: labels:
app: switchboard app: armature
env: ${ENVIRONMENT} env: ${ENVIRONMENT}
spec: spec:
replicas: ${REPLICAS} replicas: ${REPLICAS}
selector: selector:
matchLabels: matchLabels:
app: switchboard app: armature
env: ${ENVIRONMENT} env: ${ENVIRONMENT}
template: template:
metadata: metadata:
labels: labels:
app: switchboard app: armature
env: ${ENVIRONMENT} env: ${ENVIRONMENT}
spec: spec:
containers: containers:
- name: switchboard - name: armature
image: ${IMAGE}:${IMAGE_TAG} image: ${IMAGE}:${IMAGE_TAG}
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
@@ -72,43 +72,43 @@ spec:
- name: POSTGRES_USER - name: POSTGRES_USER
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-db-credentials name: armature-db-credentials
key: POSTGRES_USER key: POSTGRES_USER
- name: POSTGRES_PASSWORD - name: POSTGRES_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-db-credentials name: armature-db-credentials
key: POSTGRES_PASSWORD key: POSTGRES_PASSWORD
- name: DATABASE_URL - name: DATABASE_URL
value: "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable" value: "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable"
- name: SWITCHBOARD_ADMIN_USERNAME - name: ARMATURE_ADMIN_USERNAME
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-admin name: armature-admin
key: username key: username
optional: true optional: true
- name: SWITCHBOARD_ADMIN_PASSWORD - name: ARMATURE_ADMIN_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-admin name: armature-admin
key: password key: password
optional: true optional: true
- name: SWITCHBOARD_ADMIN_EMAIL - name: ARMATURE_ADMIN_EMAIL
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-admin name: armature-admin
key: email key: email
optional: true optional: true
- name: SEED_USERS - name: SEED_USERS
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-seed-users name: armature-seed-users
key: users key: users
optional: true optional: true
- name: ENCRYPTION_KEY - name: ENCRYPTION_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-encryption name: armature-encryption
key: ENCRYPTION_KEY key: ENCRYPTION_KEY
optional: true optional: true
# File storage (v0.12.0+) # File storage (v0.12.0+)
@@ -120,37 +120,37 @@ spec:
- name: S3_ENDPOINT - name: S3_ENDPOINT
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-s3 name: armature-s3
key: S3_ENDPOINT key: S3_ENDPOINT
optional: true optional: true
- name: S3_BUCKET - name: S3_BUCKET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-s3 name: armature-s3
key: S3_BUCKET key: S3_BUCKET
optional: true optional: true
- name: S3_ACCESS_KEY - name: S3_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-s3 name: armature-s3
key: S3_ACCESS_KEY key: S3_ACCESS_KEY
optional: true optional: true
- name: S3_SECRET_KEY - name: S3_SECRET_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-s3 name: armature-s3
key: S3_SECRET_KEY key: S3_SECRET_KEY
optional: true optional: true
- name: S3_REGION - name: S3_REGION
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-s3 name: armature-s3
key: S3_REGION key: S3_REGION
optional: true optional: true
- name: S3_PREFIX - name: S3_PREFIX
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: switchboard-s3 name: armature-s3
key: S3_PREFIX key: S3_PREFIX
optional: true optional: true
- name: S3_FORCE_PATH_STYLE - name: S3_FORCE_PATH_STYLE
@@ -191,19 +191,19 @@ spec:
volumes: volumes:
- name: storage - name: storage
persistentVolumeClaim: persistentVolumeClaim:
claimName: switchboard-storage${DEPLOY_SUFFIX} claimName: armature-storage${DEPLOY_SUFFIX}
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: switchboard${DEPLOY_SUFFIX} name: armature${DEPLOY_SUFFIX}
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
labels: labels:
app: switchboard app: armature
env: ${ENVIRONMENT} env: ${ENVIRONMENT}
spec: spec:
selector: selector:
app: switchboard app: armature
env: ${ENVIRONMENT} env: ${ENVIRONMENT}
ports: ports:
- protocol: TCP - protocol: TCP

View File

@@ -1,11 +1,11 @@
# k8s/ingress.yaml # k8s/ingress.yaml
# ============================================ # ============================================
# Switchboard Core - Ingress # Armature Core - Ingress
# ============================================ # ============================================
# Path-based routing on a single domain: # Path-based routing on a single domain:
# switchboard.DOMAIN/ → production # armature.DOMAIN/ → production
# switchboard.DOMAIN/test/ → test (main branch) # armature.DOMAIN/test/ → test (main branch)
# switchboard.DOMAIN/dev/ → dev (PR branches) # armature.DOMAIN/dev/ → dev (PR branches)
# #
# Each environment deploys its own Ingress resource. # Each environment deploys its own Ingress resource.
# Traefik merges rules for the same host automatically. # Traefik merges rules for the same host automatically.
@@ -18,29 +18,29 @@
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: switchboard${DEPLOY_SUFFIX} name: armature${DEPLOY_SUFFIX}
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
annotations: annotations:
cert-manager.io/cluster-issuer: "${CERT_ISSUER}" cert-manager.io/cluster-issuer: "${CERT_ISSUER}"
traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.entrypoints: websecure
labels: labels:
app: switchboard app: armature
env: ${ENVIRONMENT} env: ${ENVIRONMENT}
spec: spec:
ingressClassName: "traefik" ingressClassName: "traefik"
tls: tls:
- hosts: - hosts:
- ${DEPLOY_HOST} - ${DEPLOY_HOST}
secretName: switchboard-tls secretName: armature-tls
rules: rules:
- host: "${DEPLOY_HOST}" - host: "${DEPLOY_HOST}"
http: http:
paths: paths:
# All traffic → switchboard (unified image) # All traffic → armature (unified image)
- path: ${BASE_PATH}/ - path: ${BASE_PATH}/
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: switchboard${DEPLOY_SUFFIX} name: armature${DEPLOY_SUFFIX}
port: port:
number: 80 number: 80

View File

@@ -1,6 +1,6 @@
# k8s/middleware-retry.yaml # k8s/middleware-retry.yaml
# ============================================ # ============================================
# Switchboard Core - Traefik Retry Middleware # Armature Core - Traefik Retry Middleware
# ============================================ # ============================================
# Retries failed requests (502, connection reset) once before giving up. # Retries failed requests (502, connection reset) once before giving up.
# Applies to API + WebSocket paths via the Ingress annotation. # Applies to API + WebSocket paths via the Ingress annotation.
@@ -15,10 +15,10 @@
apiVersion: traefik.io/v1alpha1 apiVersion: traefik.io/v1alpha1
kind: Middleware kind: Middleware
metadata: metadata:
name: switchboard-retry${DEPLOY_SUFFIX} name: armature-retry${DEPLOY_SUFFIX}
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
labels: labels:
app: switchboard app: armature
env: ${ENVIRONMENT} env: ${ENVIRONMENT}
spec: spec:
retry: retry:

View File

@@ -16,7 +16,7 @@ metadata:
name: traefik-middleware-manager name: traefik-middleware-manager
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
labels: labels:
app: switchboard app: armature
rules: rules:
- apiGroups: ["traefik.io"] - apiGroups: ["traefik.io"]
resources: ["middlewares"] resources: ["middlewares"]
@@ -28,7 +28,7 @@ metadata:
name: gitea-runner-traefik-middleware name: gitea-runner-traefik-middleware
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
labels: labels:
app: switchboard app: armature
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: gitea-runner-sa name: gitea-runner-sa

View File

@@ -1,6 +1,6 @@
# k8s/storage-pvc.yaml # k8s/storage-pvc.yaml
# ============================================ # ============================================
# Switchboard Core - Storage PVC # Armature Core - Storage PVC
# ============================================ # ============================================
# Persistent storage for file attachments, extraction queue, # Persistent storage for file attachments, extraction queue,
# and future knowledge base / compaction data. # and future knowledge base / compaction data.
@@ -17,10 +17,10 @@
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: switchboard-storage${DEPLOY_SUFFIX} name: armature-storage${DEPLOY_SUFFIX}
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
labels: labels:
app: switchboard app: armature
component: storage component: storage
env: ${ENVIRONMENT} env: ${ENVIRONMENT}
spec: spec:

View File

@@ -1,6 +1,6 @@
# Packages # Packages
Installable `.pkg` archives for Chat Switchboard. Each subdirectory is a Installable `.pkg` archives for Chat Armature. Each subdirectory is a
package — either a surface (routable UI), an extension (hooks/tools/pipes), package — either a surface (routable UI), an extension (hooks/tools/pipes),
or both (`full` type). or both (`full` type).

View File

@@ -5,7 +5,7 @@
"version": "0.1.0", "version": "0.1.0",
"tier": "browser", "tier": "browser",
"icon": "🐛", "icon": "🐛",
"author": "Switchboard Core", "author": "Armature",
"description": "Public bug report submission with severity-based routing, team assignment, and SLA timers.", "description": "Public bug report submission with severity-based routing, team assignment, and SLA timers.",
"permissions": [], "permissions": [],

View File

@@ -5,7 +5,7 @@
"tier": "starlark", "tier": "starlark",
"version": "0.2.0", "version": "0.2.0",
"description": "Core chat library — conversations, messages, participants, read cursors. Usable by other packages via lib.require().", "description": "Core chat library — conversations, messages, participants, read cursors. Usable by other packages via lib.require().",
"author": "switchboard", "author": "armature",
"permissions": ["db.write", "realtime.publish"], "permissions": ["db.write", "realtime.publish"],

View File

@@ -9,7 +9,7 @@
"version": "0.2.0", "version": "0.2.0",
"icon": "\ud83d\udcac", "icon": "\ud83d\udcac",
"description": "Chat surface — conversations, messaging, typing indicators, read receipts.", "description": "Chat surface — conversations, messaging, typing indicators, read receipts.",
"author": "switchboard", "author": "armature",
"permissions": ["db.write", "realtime.publish"], "permissions": ["db.write", "realtime.publish"],

View File

@@ -5,7 +5,7 @@
"version": "0.1.0", "version": "0.1.0",
"tier": "browser", "tier": "browser",
"icon": "📝", "icon": "📝",
"author": "Switchboard Core", "author": "Armature",
"description": "Multi-party content review with quorum signoff and revision cycle.", "description": "Multi-party content review with quorum signoff and revision cycle.",
"permissions": [], "permissions": [],

View File

@@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "extension", "type": "extension",
"tier": "browser", "tier": "browser",
"author": "switchboard", "author": "armature",
"description": "Renders ```csv code blocks as sortable, interactive HTML tables", "description": "Renders ```csv code blocks as sortable, interactive HTML tables",
"requires": [], "requires": [],
"permissions": [], "permissions": [],

View File

@@ -1,5 +1,5 @@
// ========================================== // ==========================================
// Switchboard Core — Dashboard Package (v0.31.1) // Armature — Dashboard Package (v0.31.1)
// ========================================== // ==========================================
// SDK Exercise Surface. Exercises every sw.* primitive with ZERO // SDK Exercise Surface. Exercises every sw.* primitive with ZERO
// component CSS overrides. All styling comes from the SDK itself. // component CSS overrides. All styling comes from the SDK itself.

View File

@@ -4,7 +4,7 @@
"type": "full", "type": "full",
"version": "0.31.1", "version": "0.31.1",
"tier": "browser", "tier": "browser",
"author": "Switchboard Core", "author": "Armature",
"icon": "📊", "icon": "📊",
"description": "Project dashboard exercising all SDK primitives (requires legacy sw.* SDK — dormant until rewritten)", "description": "Project dashboard exercising all SDK primitives (requires legacy sw.* SDK — dormant until rewritten)",
"requires": ["legacy-sdk"], "requires": ["legacy-sdk"],

View File

@@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "extension", "type": "extension",
"tier": "browser", "tier": "browser",
"author": "switchboard", "author": "armature",
"description": "Renders ```diff code blocks with syntax-highlighted additions, deletions, and context lines", "description": "Renders ```diff code blocks with syntax-highlighted additions, deletions, and context lines",
"requires": [], "requires": [],
"permissions": [], "permissions": [],

View File

@@ -1,5 +1,5 @@
/* ========================================== /* ==========================================
Switchboard Core — Editor Surface (v0.25.0) Armature — Editor Surface (v0.25.0)
========================================== ==========================================
Replaces editor-mode.css for the pane-based editor. Replaces editor-mode.css for the pane-based editor.
Covers: topbar, bootstrap, and component-specific Covers: topbar, bootstrap, and component-specific

View File

@@ -1,5 +1,5 @@
// ========================================== // ==========================================
// Switchboard Core — Editor Package (v0.31.0) // Armature — Editor Package (v0.31.0)
// ========================================== // ==========================================
// Installable .pkg that provides the code editor surface. // Installable .pkg that provides the code editor surface.
// Mounts into #extension-mount (surface-extension template). // Mounts into #extension-mount (surface-extension template).

View File

@@ -4,7 +4,7 @@
"type": "full", "type": "full",
"version": "0.31.0", "version": "0.31.0",
"tier": "browser", "tier": "browser",
"author": "Switchboard Core", "author": "Armature",
"icon": "✏️", "icon": "✏️",
"description": "Code editor with workspace management, file tree, and AI assist (requires legacy sw.* SDK — dormant until rewritten)", "description": "Code editor with workspace management, file tree, and AI assist (requires legacy sw.* SDK — dormant until rewritten)",
"requires": ["legacy-sdk"], "requires": ["legacy-sdk"],

View File

@@ -5,7 +5,7 @@
"version": "0.1.0", "version": "0.1.0",
"tier": "starlark", "tier": "starlark",
"icon": "🏢", "icon": "🏢",
"author": "Switchboard Core", "author": "Armature",
"description": "Employee onboarding with automated provisioning, manager signoff, and welcome notification.", "description": "Employee onboarding with automated provisioning, manager signoff, and welcome notification.",
"permissions": ["db.write", "notifications.send"], "permissions": ["db.write", "notifications.send"],

View File

@@ -9,7 +9,7 @@
"version": "0.2.0", "version": "0.2.0",
"icon": "🔀", "icon": "🔀",
"description": "Gitea issue and PR board powered by the gitea-client library. Uses extension connections for authentication.", "description": "Gitea issue and PR board powered by the gitea-client library. Uses extension connections for authentication.",
"author": "switchboard", "author": "armature",
"permissions": ["connections.read"], "permissions": ["connections.read"],

View File

@@ -5,7 +5,7 @@
"tier": "starlark", "tier": "starlark",
"version": "1.0.1", "version": "1.0.1",
"description": "Shared Gitea client — connections, API helpers, optional data caching.", "description": "Shared Gitea client — connections, API helpers, optional data caching.",
"author": "switchboard", "author": "armature",
"permissions": ["api.http", "connections.read", "db.read", "db.write"], "permissions": ["api.http", "connections.read", "db.read", "db.write"],

View File

@@ -27,7 +27,7 @@
type: 'full', type: 'full',
version: '0.31.1', version: '0.31.1',
tier: 'browser', tier: 'browser',
author: 'Switchboard Core', author: 'Armature',
description: 'Project dashboard exercising all SDK primitives', description: 'Project dashboard exercising all SDK primitives',
route: '/s/dashboard', route: '/s/dashboard',
permissions: [], permissions: [],

View File

@@ -27,7 +27,7 @@
type: 'full', type: 'full',
version: '0.31.0', version: '0.31.0',
tier: 'browser', tier: 'browser',
author: 'Switchboard Core', author: 'Armature',
description: 'Code editor with workspace management', description: 'Code editor with workspace management',
route: '/s/editor', route: '/s/editor',
layout: 'editor', layout: 'editor',

View File

@@ -14,9 +14,9 @@
var resp = await fetch(T.base + '/metrics'); var resp = await fetch(T.base + '/metrics');
T.assert(resp.ok, 'expected 200 from /metrics, got ' + resp.status); T.assert(resp.ok, 'expected 200 from /metrics, got ' + resp.status);
var text = await resp.text(); var text = await resp.text();
T.assert(text.indexOf('switchboard_http_requests_total') !== -1, 'expected switchboard_http_requests_total in /metrics'); T.assert(text.indexOf('armature_http_requests_total') !== -1, 'expected armature_http_requests_total in /metrics');
T.assert(text.indexOf('switchboard_http_request_duration_seconds') !== -1, 'expected switchboard_http_request_duration_seconds in /metrics'); T.assert(text.indexOf('armature_http_request_duration_seconds') !== -1, 'expected armature_http_request_duration_seconds in /metrics');
T.assert(text.indexOf('switchboard_websocket_connections') !== -1, 'expected switchboard_websocket_connections in /metrics'); T.assert(text.indexOf('armature_websocket_connections') !== -1, 'expected armature_websocket_connections in /metrics');
}); });
// -- GET /api/docs (Swagger UI) -- // -- GET /api/docs (Swagger UI) --

View File

@@ -270,7 +270,7 @@
function getAuthTokenFromStorage() { function getAuthTokenFromStorage() {
var env = (T.base || '').replace(/^\//, '') || 'default'; var env = (T.base || '').replace(/^\//, '') || 'default';
var keys = ['sb_auth_' + env, 'sb_auth', 'sb_token']; var keys = ['arm_auth_' + env, 'arm_auth', 'arm_token'];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
try { try {
var raw = localStorage.getItem(keys[i]); var raw = localStorage.getItem(keys[i]);

View File

@@ -15,7 +15,7 @@
* 8. tier-security.js — Adversarial red-team tests (auth, cross-tenant, input validation) * 8. tier-security.js — Adversarial red-team tests (auth, cross-tenant, input validation)
* 9. tier-providers.js — Three-tier provider CRUD + live completions * 9. tier-providers.js — Three-tier provider CRUD + live completions
* 10. tier-packaging.js — Extension permission lifecycle + secrets (v0.29.0) * 10. tier-packaging.js — Extension permission lifecycle + secrets (v0.29.0)
* 11. tier-sdk.js — Switchboard SDK contract tests * 11. tier-sdk.js — Armature SDK contract tests
* 12. ui.js — Render functions, export, provider setup panel * 12. ui.js — Render functions, export, provider setup panel
* 11. (this file) — Boot * 11. (this file) — Boot
*/ */

View File

@@ -3,7 +3,7 @@
* Validates Preact SDK (sw/sdk/index.js) contract: boot, identity, * Validates Preact SDK (sw/sdk/index.js) contract: boot, identity,
* REST client, events, theme, pipe registration, execution ordering, * REST client, events, theme, pipe registration, execution ordering,
* scoping, halt semantics, error isolation, introspection. * scoping, halt semantics, error isolation, introspection.
* v0.37.14: Updated from Switchboard.init() to boot() API. * v0.37.14: Updated from Armature.init() to boot() API.
*/ */
(function () { (function () {
'use strict'; 'use strict';

View File

@@ -236,7 +236,7 @@
}, 'Providers'); }, 'Providers');
T.el.controls.appendChild(btnProv); T.el.controls.appendChild(btnProv);
// SDK tier button — tests switchboard-sdk.js contract // SDK tier button — tests armature-sdk.js contract
var btnSdk = $('button', { var btnSdk = $('button', {
className: (typeof window.sw !== 'undefined') ? 'btn-secondary' : 'btn-ghost', className: (typeof window.sw !== 'undefined') ? 'btn-secondary' : 'btn-ghost',
style: { opacity: (typeof window.sw !== 'undefined') ? '1' : '0.5' }, style: { opacity: (typeof window.sw !== 'undefined') ? '1' : '0.5' },
@@ -368,7 +368,7 @@
var totalMs = T.results.reduce(function (s, r) { return s + r.duration; }, 0); var totalMs = T.results.reduce(function (s, r) { return s + r.duration; }, 0);
var lines = []; var lines = [];
lines.push('=== Switchboard Core ICD Test Report ==='); lines.push('=== Armature ICD Test Report ===');
lines.push('Generated: ' + now); lines.push('Generated: ' + now);
lines.push('Platform: v' + (T.manifest.version || '0.28.0')); lines.push('Platform: v' + (T.manifest.version || '0.28.0'));
lines.push('User: ' + T.user.username + ' (' + T.user.role + ')'); lines.push('User: ' + T.user.username + ' (' + T.user.role + ')');

View File

@@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "extension", "type": "extension",
"tier": "browser", "tier": "browser",
"author": "switchboard", "author": "armature",
"description": "Executes JavaScript in a sandboxed iframe. Allows the LLM to run code, verify calculations, and transform data — no server compute required.", "description": "Executes JavaScript in a sandboxed iframe. Allows the LLM to run code, verify calculations, and transform data — no server compute required.",
"requires": ["chat"], "requires": ["chat"],
"permissions": [], "permissions": [],

View File

@@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "extension", "type": "extension",
"tier": "browser", "tier": "browser",
"author": "switchboard", "author": "armature",
"description": "Renders LaTeX math expressions: ```latex blocks and inline $...$ / $$...$$ syntax", "description": "Renders LaTeX math expressions: ```latex blocks and inline $...$ / $$...$$ syntax",
"requires": [], "requires": [],
"permissions": [], "permissions": [],

View File

@@ -4,7 +4,7 @@
"version": "2.0.0", "version": "2.0.0",
"type": "extension", "type": "extension",
"tier": "browser", "tier": "browser",
"author": "switchboard", "author": "armature",
"description": "Renders ```mermaid code blocks as interactive SVG diagrams with zoom/pan, SVG/PNG export, and source copy", "description": "Renders ```mermaid code blocks as interactive SVG diagrams with zoom/pan, SVG/PNG export, and source copy",
"requires": [], "requires": [],
"permissions": [], "permissions": [],

View File

@@ -9,7 +9,7 @@
"version": "0.8.0", "version": "0.8.0",
"icon": "📝", "icon": "📝",
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.", "description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.",
"author": "switchboard", "author": "armature",
"permissions": ["db.write"], "permissions": ["db.write"],

View File

@@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "extension", "type": "extension",
"tier": "browser", "tier": "browser",
"author": "switchboard", "author": "armature",
"description": "Tests regular expressions against input strings. Allows the LLM to verify regex patterns and see matches, groups, and indices.", "description": "Tests regular expressions against input strings. Allows the LLM to verify regex patterns and see matches, groups, and indices.",
"requires": ["chat"], "requires": ["chat"],
"permissions": [], "permissions": [],

View File

@@ -4,7 +4,7 @@
"type": "full", "type": "full",
"version": "0.1.0", "version": "0.1.0",
"tier": "browser", "tier": "browser",
"author": "Switchboard Core", "author": "Armature",
"icon": "⏰", "icon": "⏰",
"description": "Manage cron jobs and scheduled automations", "description": "Manage cron jobs and scheduled automations",
"route": "/s/schedules", "route": "/s/schedules",

View File

@@ -141,7 +141,7 @@
function _readToken() { function _readToken() {
var env = (T.base || '').replace(/^\//, '') || 'default'; var env = (T.base || '').replace(/^\//, '') || 'default';
var keys = ['sb_auth_' + env, 'sb_auth', 'sb_token']; var keys = ['arm_auth_' + env, 'arm_auth', 'arm_token'];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
try { try {
var raw = localStorage.getItem(keys[i]); var raw = localStorage.getItem(keys[i]);

View File

@@ -9,7 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"icon": "✅", "icon": "✅",
"description": "Task management surface with lifecycle events, webhook integration, and scheduled reminders.", "description": "Task management surface with lifecycle events, webhook integration, and scheduled reminders.",
"author": "switchboard", "author": "armature",
"permissions": ["db.write", "notifications.send"], "permissions": ["db.write", "notifications.send"],

View File

@@ -9,7 +9,7 @@
"layout": "single", "layout": "single",
"version": "0.1.0", "version": "0.1.0",
"description": "Example surface demonstrating Starlark API routes, extension-owned DB tables, admin settings, and SDK integration.", "description": "Example surface demonstrating Starlark API routes, extension-owned DB tables, admin settings, and SDK integration.",
"author": "switchboard", "author": "armature",
"permissions": ["api.http", "db.read", "db.write"], "permissions": ["api.http", "db.read", "db.write"],

View File

@@ -5,7 +5,7 @@
"version": "0.1.0", "version": "0.1.0",
"tier": "starlark", "tier": "starlark",
"icon": "🔔", "icon": "🔔",
"author": "Switchboard Core", "author": "Armature",
"description": "Outbound webhook delivery with connection credential support and delivery logging.", "description": "Outbound webhook delivery with connection credential support and delivery logging.",
"permissions": ["api.http", "connections.read", "db.write"], "permissions": ["api.http", "connections.read", "db.write"],

View File

@@ -21,7 +21,7 @@ def on_fire(ctx):
resp = http.post(url, body=payload, headers={ resp = http.post(url, body=payload, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Switchboard-Event": data.get("event_name", "workflow.notify"), "X-Armature-Event": data.get("event_name", "workflow.notify"),
}) })
# Log the delivery # Log the delivery

View File

@@ -5,7 +5,7 @@
"tier": "starlark", "tier": "starlark",
"version": "0.1.0", "version": "0.1.0",
"description": "Creates scoped chat conversations when workflow stages require team collaboration. Wire into stage_config as an on_advance hook.", "description": "Creates scoped chat conversations when workflow stages require team collaboration. Wire into stage_config as an on_advance hook.",
"author": "switchboard", "author": "armature",
"permissions": ["db.write", "realtime.publish"], "permissions": ["db.write", "realtime.publish"],

View File

@@ -1,5 +1,5 @@
// ========================================== // ==========================================
// Switchboard Core — Workflow Demo Surface // Armature — Workflow Demo Surface
// ========================================== // ==========================================
// Interactive walkthrough of the four example workflow packages. // Interactive walkthrough of the four example workflow packages.
// Shows workflow cards with feature badges, stage flow diagrams, // Shows workflow cards with feature badges, stage flow diagrams,

View File

@@ -5,7 +5,7 @@
"version": "0.1.0", "version": "0.1.0",
"tier": "browser", "tier": "browser",
"icon": "🎓", "icon": "🎓",
"author": "Switchboard Core", "author": "Armature",
"description": "Interactive demo surface for example workflow packages. Cards, stage diagrams, and API walkthroughs.", "description": "Interactive demo surface for example workflow packages. Cards, stage diagrams, and API walkthroughs.",
"route": "/s/workflow-demo", "route": "/s/workflow-demo",
"auth": "authenticated", "auth": "authenticated",

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# switchboard-ca.sh — Certificate provisioning for Switchboard Core mTLS. # armature-ca.sh — Certificate provisioning for Armature mTLS.
# #
# Wraps openssl to generate a cluster CA, node certificates (ServerAuth + # Wraps openssl to generate a cluster CA, node certificates (ServerAuth +
# ClientAuth), and user certificates (ClientAuth only). All output is PEM. # ClientAuth), and user certificates (ClientAuth only). All output is PEM.
# #
# Usage: # Usage:
# switchboard-ca init # armature-ca init
# switchboard-ca issue-node --name <node-name> --san <dns1,ip1,...> # armature-ca issue-node --name <node-name> --san <dns1,ip1,...>
# switchboard-ca issue-user --cn <username> [--email <email>] # armature-ca issue-user --cn <username> [--email <email>]
# #
# Files are written to the current directory under ca/, nodes/, users/. # Files are written to the current directory under ca/, nodes/, users/.
@@ -52,7 +52,7 @@ cmd_init() {
-key "$CA_DIR/cluster-ca.key" \ -key "$CA_DIR/cluster-ca.key" \
-out "$CA_DIR/cluster-ca.crt" \ -out "$CA_DIR/cluster-ca.crt" \
-days "$CA_DAYS" \ -days "$CA_DAYS" \
-subj "/CN=Switchboard Cluster CA" \ -subj "/CN=Armature Cluster CA" \
-addext "basicConstraints=critical,CA:TRUE" \ -addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \
2>/dev/null 2>/dev/null

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ============================================ # ============================================
# Switchboard Core - Database Bootstrap # Armature - Database Bootstrap
# ============================================ # ============================================
# Idempotent: safe to run on every CI build. # Idempotent: safe to run on every CI build.
# Creates the app role and database if they # Creates the app role and database if they

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ============================================ # ============================================
# Switchboard Core - Database Migration Runner # Armature - Database Migration Runner
# ============================================ # ============================================
# NOTE: The Go backend auto-migrates on startup. # NOTE: The Go backend auto-migrates on startup.
# This script is for manual/emergency use only. # This script is for manual/emergency use only.

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ============================================ # ============================================
# Switchboard Core - Schema Validation (v0.16) # Armature - Schema Validation (v0.16)
# ============================================ # ============================================
# Verifies the database schema is correct after # Verifies the database schema is correct after
# migration. Checks expected tables, key columns, # migration. Checks expected tables, key columns,

View File

@@ -1,4 +1,4 @@
# Switchboard Core - Server Environment Variables # Armature - Server Environment Variables
# Copy this file to .env and fill in the values # Copy this file to .env and fill in the values
# ── Server ─────────────────────────────────── # ── Server ───────────────────────────────────
@@ -9,9 +9,9 @@ BASE_PATH= # e.g. /chat for path-based routing
# ── Database (PostgreSQL) ──────────────────── # ── Database (PostgreSQL) ────────────────────
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_USER=switchboard_core DB_USER=armature
DB_PASSWORD=your-secure-password-here DB_PASSWORD=your-secure-password-here
DB_NAME=switchboard_core DB_NAME=armature
DB_SSL_MODE=disable DB_SSL_MODE=disable
DB_MAX_CONNS=25 DB_MAX_CONNS=25
@@ -20,19 +20,19 @@ JWT_SECRET=your-super-secret-jwt-key-change-in-production
# JWT durations (hardcoded in handlers/auth.go — not configurable via env): # JWT durations (hardcoded in handlers/auth.go — not configurable via env):
# Access token: 15 minutes # Access token: 15 minutes
# Refresh token: 7 days # Refresh token: 7 days
JWT_ISSUER=switchboard-core JWT_ISSUER=armature
# ── Bootstrap Admin ────────────────────────── # ── Bootstrap Admin ──────────────────────────
# Creates or updates admin account on every startup. # Creates or updates admin account on every startup.
# Set via K8s secret or env. Leave blank to skip. # Set via K8s secret or env. Leave blank to skip.
SWITCHBOARD_ADMIN_USERNAME= ARMATURE_ADMIN_USERNAME=
SWITCHBOARD_ADMIN_PASSWORD= ARMATURE_ADMIN_PASSWORD=
# ── Seed Users (dev/test only) ────────────── # ── Seed Users (dev/test only) ──────────────
# Pre-create users on startup. Ignored in production. # Pre-create users on startup. Ignored in production.
# Format: user:pass:role,user2:pass2:role2 (role = admin|user, default: user) # Format: user:pass:role,user2:pass2:role2 (role = admin|user, default: user)
# Idempotent: existing usernames are skipped. # Idempotent: existing usernames are skipped.
# K8s: create secret "switchboard-seed-users" with key "users" # K8s: create secret "armature-seed-users" with key "users"
SEED_USERS= SEED_USERS=
# ── CORS ───────────────────────────────────── # ── CORS ─────────────────────────────────────
@@ -57,7 +57,7 @@ STORAGE_PATH=/data/storage
# S3 settings (only when STORAGE_BACKEND=s3) # S3 settings (only when STORAGE_BACKEND=s3)
# Works with MinIO, Ceph RGW, AWS S3. # Works with MinIO, Ceph RGW, AWS S3.
# S3_ENDPOINT=http://minio:9000 # S3_ENDPOINT=http://minio:9000
# S3_BUCKET=switchboard-storage # S3_BUCKET=armature-storage
# S3_ACCESS_KEY= # S3_ACCESS_KEY=
# S3_SECRET_KEY= # S3_SECRET_KEY=
# S3_REGION=us-east-1 # S3_REGION=us-east-1

View File

@@ -5,8 +5,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"switchboard-core/models" "armature/models"
"switchboard-core/store" "armature/store"
) )
type Mode string type Mode string

View File

@@ -9,8 +9,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"switchboard-core/models" "armature/models"
"switchboard-core/store" "armature/store"
) )
const bcryptCost = 12 const bcryptCost = 12

View File

@@ -109,7 +109,7 @@ func (m *mockIdP) issueToken(sub, email, name string, groups []string, roles []s
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"iss": m.server.URL, "iss": m.server.URL,
"sub": sub, "sub": sub,
"aud": "switchboard", "aud": "armature",
"exp": time.Now().Add(1 * time.Hour).Unix(), "exp": time.Now().Add(1 * time.Hour).Unix(),
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"email": email, "email": email,
@@ -140,7 +140,7 @@ func (m *mockIdP) issueExpiredToken(sub string) string {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"iss": m.server.URL, "iss": m.server.URL,
"sub": sub, "sub": sub,
"aud": "switchboard", "aud": "armature",
"exp": time.Now().Add(-1 * time.Hour).Unix(), "exp": time.Now().Add(-1 * time.Hour).Unix(),
"iat": time.Now().Add(-2 * time.Hour).Unix(), "iat": time.Now().Add(-2 * time.Hour).Unix(),
} }
@@ -160,7 +160,7 @@ func (m *mockIdP) issueTokenBadIssuer(sub string) string {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"iss": "https://evil.example.com", "iss": "https://evil.example.com",
"sub": sub, "sub": sub,
"aud": "switchboard", "aud": "armature",
"exp": time.Now().Add(1 * time.Hour).Unix(), "exp": time.Now().Add(1 * time.Hour).Unix(),
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
} }

View File

@@ -9,8 +9,8 @@ import (
"log" "log"
"strings" "strings"
"switchboard-core/models" "armature/models"
"switchboard-core/store" "armature/store"
) )
// ParseDN parses an RFC 2253 / RFC 4514 distinguished name into key-value pairs. // ParseDN parses an RFC 2253 / RFC 4514 distinguished name into key-value pairs.

View File

@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"switchboard-core/store" "armature/store"
) )
// ErrNoCert is returned when no client certificate is presented on a // ErrNoCert is returned when no client certificate is presented on a

View File

@@ -19,7 +19,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"switchboard-core/store" "armature/store"
) )
// ── Test CA helpers ────────────────────────────────────────────────── // ── Test CA helpers ──────────────────────────────────────────────────

View File

@@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"switchboard-core/store" "armature/store"
) )
// MTLSProxyConfig holds configuration for the proxy-terminated mTLS provider. // MTLSProxyConfig holds configuration for the proxy-terminated mTLS provider.

View File

@@ -3,7 +3,7 @@ package auth
import ( import (
"testing" "testing"
"switchboard-core/store" "armature/store"
) )
func TestParseDN(t *testing.T) { func TestParseDN(t *testing.T) {

Some files were not shown because too many files have changed in this diff Show More