0.28.0 — operational hardening (plan D1–D5): CI deploy gate, versioned images, rate limiting, resource limits, health probes
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

D1: deploy.sh CI gate — read-only SHA via git ls-remote, Gitea commit-status
    poll, portable mkdir deploy lock (macOS, no flock), TOCTOU guard, token
    passed via curl --config - (not argv), graceful misconfig tolerance.
D2: version-tagged images — OIKOS_VERSION=v$VERSION, keep-last-3 prune derived
    from 'docker compose config --images'; VERSION read after pull.
D3: per-IP rate limiting — new internal/httpapi/ratelimit.go (x/time/rate),
    rightmost-XFF, /healthz exempt, ctx-driven sweep; disabled by default.
D4: mem_limit/cpus on all 10 compose services.
D5: staleness-aware health probes — new internal/health package wired into
    scheduler (:8093) and notifier (:8094); nomos already had :8092.

Two /review passes hardened the deploy lock, TOCTOU guard, token hygiene,
and XFF handling.
This commit is contained in:
2026-08-08 21:31:16 +02:00
parent ef762794e7
commit fa79c1ea25
14 changed files with 852 additions and 41 deletions

View File

@@ -20,6 +20,8 @@ services:
- "5432:5432"
volumes:
- pg-data:/var/lib/postgresql/data
mem_limit: 1g
cpus: 2.0
healthcheck:
test: ["CMD", "pg_isready", "-U", "oikos"]
interval: 5s
@@ -28,6 +30,7 @@ services:
# One-shot: run migrations then exit
migrate:
image: oikos-migrate:${OIKOS_VERSION:-latest}
build:
context: .
dockerfile: compose/oikos/Dockerfile
@@ -38,9 +41,12 @@ services:
OIKOS_DATABASE_URL: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/oikos?sslmode=disable
command: ["migrate"]
restart: "no"
mem_limit: 512m
cpus: 1.0
# One-shot: ingest seeds then exit
seed:
image: oikos-seed:${OIKOS_VERSION:-latest}
build:
context: .
dockerfile: compose/oikos/Dockerfile
@@ -52,9 +58,12 @@ services:
OIKOS_SEEDS_DIR: /seeds
command: ["seed"]
restart: "no"
mem_limit: 512m
cpus: 1.0
# API server (Phase 2)
api:
image: oikos-api:${OIKOS_VERSION:-latest}
build:
context: .
dockerfile: compose/oikos/Dockerfile
@@ -76,6 +85,10 @@ services:
OIKOS_OIDC_CLIENT_ID: ${OIKOS_OIDC_CLIENT_ID:-otkHBSueHJsYtOHstL6rn5izeGgyOsavp1qA1hod}
OIKOS_NOMOS_AGENT_SLUG: ${OIKOS_NOMOS_AGENT_SLUG:-agent:nomos}
NOMOS_PROXY_URL: http://nomos:8092
# Rate limiting (plan D3). Default off; set OIKOS_API_RATE_LIMIT to a
# requests/sec value to throttle runaway agent loops per source IP.
OIKOS_API_RATE_LIMIT: ${OIKOS_API_RATE_LIMIT:-}
OIKOS_API_RATE_BURST: ${OIKOS_API_RATE_BURST:-}
# Infisical secret store (Phase 5)
OIKOS_INFISICAL_SITE_URL: ${OIKOS_INFISICAL_SITE_URL:-}
OIKOS_INFISICAL_CLIENT_ID: ${OIKOS_INFISICAL_CLIENT_ID:-}
@@ -89,6 +102,8 @@ services:
command: ["api"]
stop_signal: SIGTERM
stop_grace_period: 30s
mem_limit: 512m
cpus: 1.0
# Exists so nomos can wait for the API to actually answer rather than just
# for its container to exist — see nomos's depends_on below. wget is
# BusyBox's, already in the alpine runtime image, so this adds no
@@ -104,6 +119,7 @@ services:
# Scheduler (Phase 3) — observe loop
scheduler:
image: oikos-scheduler:${OIKOS_VERSION:-latest}
build:
context: .
dockerfile: compose/oikos/Dockerfile
@@ -118,6 +134,9 @@ services:
OIKOS_SCHEDULER_INTERVAL: "30s"
OIKOS_SSH_KEY_PATH: /etc/oikos/ssh_key
OIKOS_SSH_USER: root
# Liveness probe (plan D5): exposes a staleness-aware /healthz inside
# the container; the scheduler bumps it each check pass.
OIKOS_HEALTH_LISTEN: ":8093"
volumes:
- ${OIKOS_SSH_KEY_PATH:-~/.ssh/id_ed25519}:/etc/oikos/ssh_key:ro
cap_add:
@@ -125,9 +144,18 @@ services:
command: ["scheduler"]
stop_signal: SIGTERM
stop_grace_period: 30s
mem_limit: 256m
cpus: 1.0
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8093/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 90s
# Notifier (Phase 3) — Matrix alerts
notifier:
image: oikos-notifier:${OIKOS_VERSION:-latest}
build:
context: .
dockerfile: compose/oikos/Dockerfile
@@ -144,6 +172,8 @@ services:
OIKOS_MATRIX_USER: ${OIKOS_MATRIX_USER:-@hermes:hubris.network}
OIKOS_MATRIX_TOKEN: ${OIKOS_MATRIX_TOKEN}
OIKOS_MATRIX_ROOM: ${OIKOS_MATRIX_ROOM:-!alerts:hubris.network}
# Liveness probe (plan D5): bumps each approval/reaction tick.
OIKOS_HEALTH_LISTEN: ":8094"
OIKOS_INFISICAL_SITE_URL: ${OIKOS_INFISICAL_SITE_URL:-}
OIKOS_INFISICAL_CLIENT_ID: ${OIKOS_INFISICAL_CLIENT_ID:-}
OIKOS_INFISICAL_CLIENT_SECRET: ${OIKOS_INFISICAL_CLIENT_SECRET:-}
@@ -152,9 +182,18 @@ services:
command: ["notifier"]
stop_signal: SIGTERM
stop_grace_period: 30s
mem_limit: 128m
cpus: 0.5
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8094/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 120s
# Nomos agent gateway (Phase 4) — mesh-published :8092
nomos:
image: oikos-nomos:${OIKOS_VERSION:-latest}
build:
context: .
dockerfile: compose/nomos/Dockerfile
@@ -188,12 +227,21 @@ services:
- "8092:8092"
stop_signal: SIGTERM
stop_grace_period: 10s
mem_limit: 512m
cpus: 1.0
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8092/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
# Control-room SPA — static build served behind Caddy. The outer
# production Caddy (caddy-conf repo, LXC 121) splits /api/*, /mcp,
# /agent/* off to api:8090 and sends everything else here; this
# container only serves static files with SPA-fallback routing.
web:
image: oikos-web:${OIKOS_VERSION:-latest}
build:
context: .
dockerfile: compose/web/Dockerfile
@@ -202,6 +250,8 @@ services:
ports:
- "8091:80"
stop_signal: SIGTERM
mem_limit: 64m
cpus: 0.25
# Redis (required by Infisical — Phase 5)
redis:
@@ -210,6 +260,8 @@ services:
profiles: ["infisical", "full"]
volumes:
- redis-data:/data
mem_limit: 128m
cpus: 0.5
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
@@ -240,6 +292,8 @@ services:
REDIS_URL: redis://redis:6379
ports:
- "8080:8080"
mem_limit: 512m
cpus: 1.0
volumes:
pg-data: