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.
25 KiB
2026-08-05 — Backend evaluation: architecture, security, and reliability improvements
Status: In Progress — Phase 0 (B1, B2, B4, B5, B6, B7) and Phase 2 (D1–D5)
complete; D1–D5 were hardened across two /review passes (deploy lock, TOCTOU
guard, token hygiene, XFF rightmost-hop, ctx-driven sweep). B3 is a post-deploy
operational step. Remaining: Phase 1 security (C1–C3), Phase 3 code quality
(E1–E5), and Phase 4–6 backlog.
Scope: full evaluation of the oikos backend (Go binaries oikos, nomos, webhook,
Postgres/TimescaleDB, Docker deployment, MCP server) excluding frontend clients
(web/ SPA and desktop/ Wails app). Began as a research-only pass; Phase 0 (B)
and Phase 2 (D) have since been implemented as code changes — see each item's
"Status".
Method: four parallel research passes (Go backend structure, database schema,
deployment/infrastructure, API/MCP design) plus Infisical secrets audit and
dependency analysis of go.mod.
A. Summary of findings
The backend is well-architected with strong fundamentals: contract-first API (oapi-codegen), type-safe SQL (sqlc + pgx), TimescaleDB observability, policy- governed autonomy, and a sophisticated ontology-driven data model. The main gaps are secret management (Infisical is wired but barely used — 3 of 4 binaries read secrets from env/plaintext), operational maturity (CI, image tagging, backup reliability), security hardening (SSH host keys, unauthenticated endpoints), and code hygiene (monolithic files, mixed SQL access patterns).
| Category | Grade | Notes |
|---|---|---|
| Tech stack | A | Go + pgx + sqlc + TimescaleDB + chi + slog — all correct choices |
| Data model | A- | Dual-entity pattern, temporal relationships, partial indexes, 6 state machines |
| API design | B+ | Contract-first with ~50 MCP tools, RFC 9457 errors; no rate limiting |
| Secret management | D | Infisical SDK wired but only in API/MCP tools path; nomos, webhook, scheduler, notifier all read plaintext env vars. 7 production secrets in .env, HMAC in world-readable plist. SOPS fallback is dead code. |
| Security | C | OIDC+static token auth is good, but SSH host keys disabled, unauthenticated nomos endpoint, HMAC secret in world-readable plist |
| Deployment | C+ | Multi-stage builds, pre-deploy backups, but no CI, no versioned images, silent backup failures |
| Code quality | B | Good error handling, panic safety, doc; but 3 files over 1100 lines, mixed raw/sqlc SQL |
| Performance | B | Appropriate for scale; SSH check storm risk, no query caching |
| Observability | B- | TimescaleDB hypertables + SSE + slog; no Prometheus/Grafana, no OTel tracing |
| Testing | C | make test exists but many core packages (scheduler, domain, actuator, policy) have 0% coverage |
B. Infisical consolidation (critical)
Infisical is deployed (Redis + Infisical service in compose, Go SDK in go.mod,
internal/secrets/infisical.go fully implemented) but severely underutilized.
Only the API server's MCP tools path creates an Infisical backend. Every other
binary reads secrets from env vars or plaintext files.
Current wiring map
| Binary / role | Uses Infisical? | Secrets read from env/plaintext |
|---|---|---|
oikos api role |
Yes (MCP tools only) | OIKOS_DATABASE_URL, OIKOS_MCP_BEARER_TOKEN, OIKOS_API_TOKEN, OIKOS_OIDC_CLIENT_SECRET |
oikos scheduler role |
No | OIKOS_SSH_KEY_PATH, OIKOS_DATABASE_URL |
oikos notifier role |
No | OIKOS_MATRIX_TOKEN, OIKOS_APPROVAL_HMAC_SECRET, OIKOS_DATABASE_URL |
| nomos | No | OPENROUTER_API_KEY, OIKOS_MCP_BEARER_TOKEN, DATABASE_URL |
| webhook | No | WEBHOOK_HMAC_SECRET (also hardcoded in plist) |
B1. Wire Infisical into all binaries at startup
- Goal: Every binary fetches its secrets from Infisical at startup instead of
relying on env vars. Bootstrap-only env vars (
INFISICAL_CLIENT_ID,INFISICAL_CLIENT_SECRET,INFISICAL_SITE_URL,INFISICAL_PROJECT_ID,OIKOS_DATABASE_URL) remain as env vars (chicken-egg). - Approach: Add a
secrets.InitFromEnv(ctx)call to each binary'smain()that creates asecrets.Manager(primary Infisical + SOPS fallback). Store the manager in a package-level var or pass it through the initialization chain. - Files to change:
cmd/oikos/main.go— create Manager inrunWithPool, pass to scheduler and notifier runners alongside cfg and poolcmd/nomos/main.go— create Manager at startup, fetchOPENROUTER_API_KEYandOIKOS_MCP_BEARER_TOKENfrom Infisical before creating the agentcmd/webhook/main.go— create Manager at startup, fetchWEBHOOK_HMAC_SECRETfrom Infisical
- Secrets to migrate into Infisical (move from env vars /
.env/ plist):Secret key (in Infisical) Current source Used by matrix/tokenOIKOS_MATRIX_TOKENenvoikos notifier approval/hmac-secretOIKOS_APPROVAL_HMAC_SECRETenv, plistoikos notifier, webhook mcp/bearer-tokenOIKOS_MCP_BEARER_TOKENenvoikos api, nomos api/tokenOIKOS_API_TOKENenvoikos api oidc/client-secretOIKOS_OIDC_CLIENT_SECRETenvoikos api openrouter/api-keyOPENROUTER_API_KEYenvnomos webhook/hmac-secretWEBHOOK_HMAC_SECRETenv + plistwebhook - Risk class: config_mutation
- Prerequisite: Populate Infisical with these secrets via
oikos secret setbefore deploying the code change. Existing.envvalues serve as the source of truth for the initial migration.
B2. Activate the SOPS fallback path
- Status: Done (commit pending)
- Current:
secrets.NewManager(infisical, sops)is only used in tests.httpapi/server.gocreatesInfisicalBackenddirectly — if Infisical is down, there is no fallback. - Fix: Use
secrets.NewManager()in production everywhere so the SOPS DR fallback actually works when Infisical is unreachable. The Manager's cache (5min TTL) already masks transient Infisical blips. - What changed:
httpapi/server.gonow createssecrets.NewManager(infisical, sopsFallback)instead of baresecrets.NewInfisicalBackend. SOPS backend is created fromcfg.SecretsDirwhen set. - Risk class: config_mutation
B3. Remove .env plaintext secrets after migration
- Current:
.envcontains 7 production secrets in plaintext on mac-mini disk. - Fix: After B1 is deployed and all binaries read from Infisical, strip
secrets from
.envleaving only non-secret config (OIKOS_API_LISTEN,OIKOS_SCHEDULER_INTERVAL, etc.). Bootstrap env vars (OIKOS_DATABASE_URL,OIKOS_INFISICAL_*) stay — they're the trust anchor. - Risk class: config_mutation
B4. Remove HMAC secret from plist
- Status: Done (code change; plist cleanup is post-deploy)
- Current:
scripts/deploy/network.hubris.oikos-deploy-webhook.plistline 14 hasWEBHOOK_HMAC_SECREThardcoded in plaintext. World-readable. - Fix: After B1 (webhook reads from Infisical), remove the
EnvironmentVariablesWEBHOOK_HMAC_SECRETentry from the plist entirely. The webhook binary will fetch it from Infisical at startup. - Risk class: config_mutation
- Supersedes: Original plan item B3 (HMAC secret in plist) — same issue, now resolved via Infisical instead of file permissions workarounds.
B5. Store SSH host keys in Infisical
- Status: Done (commit pending)
- Current:
ssh.InsecureIgnoreHostKey()in 3 code paths (internal/actuator/ssh.go:160,internal/actuator/actuator.go:427,internal/mcp/server.go). - Fix: Store Proxmox host public keys in Infisical under
ssh/host-keys/{hostname}. Actuator reads them at connection init and builds aknownhostscallback. For dynamic targets, implement TOFU (trust-on-first- use) writing back to Infisical. - What changed:
internal/actuator/hostkeys.go—HostKeyCallback()returns anssh.HostKeyCallbackthat verifies against cached keys (MITM detection) and accepts unknown hosts via TOFU, persisting new keys to Infisical.internal/actuator/hostkeys_infisical.go—InfisicalHostKeySourceimplementsHostKeySourceoversecrets.Backend;ResolveSSHHosts()queries DB for active proxmox-host/standalone-server slugs.internal/actuator/ssh.go:160— replacedInsecureIgnoreHostKey()withHostKeyCallback().internal/actuator/actuator.go:427— replacedInsecureIgnoreHostKey()withHostKeyCallback().internal/mcp/server.go:494— replacedInsecureIgnoreHostKey()withactuator.HostKeyCallback().internal/httpapi/server.go— pre-loads SSH host keys from Infisical at startup (queries active hosts, loads their keys from Infisical).
- Post-deploy step: On first deploy, TOFU will accept all current host
keys and store them in Infisical under
ssh/host-keys/{slug}. Verify the stored keys are correct by checkingoikos secret list. To pre-populate without TOFU, SSH to each Proxmox host and run:ssh-keyscan -t ed25519 {host} | awk '{print $2" "$3}'and store the output viaoikos secret set ssh/host-keys/{slug} {output}. - Risk class: config_mutation (initial pin) / destructive (if keys change)
B6. Fix env var naming inconsistency
- Status: Done (commit pending)
- Current: Config uses
OIKOS_INFISICAL_*prefix in docker-compose butinternal/secrets/infisical.golines 58–62 falls back to bareINFISICAL_*(without OIKOS prefix). Two naming conventions for the same bootstrap vars. - Fix: Standardize on
OIKOS_INFISICAL_*everywhere. Remove the bareINFISICAL_*fallback in infisical.go. - What changed: Removed the
os.Getenv("INFISICAL_CLIENT_ID")andos.Getenv("INFISICAL_CLIENT_SECRET")fallbacks ininfisical.go connect(). Removed unusedosimport. Error message updated to referenceOIKOS_INFISICAL_*names. - Risk class: config_mutation
B7. Align secretsBackend interface with secrets.Backend
- Status: Done (commit pending)
- Current:
internal/httpapi/server.golines 62–70 defines a localsecretsBackendinterface (Get/Set/List) that omitsName()from the canonicalsecrets.Backend. - Fix: Use
secrets.Backenddirectly in httpapi, or embed it in the local interface. - What changed:
- Removed
secretsBackendinterface fromhttpapi/server.go;Server.secretsManagernow usessecrets.Backenddirectly. - Removed
secretBackendinterface frommcp/server.go;NewHandlerandnewServernow acceptsecrets.Backend. - Updated
mcp/tools.goallTools()signature to acceptsecrets.Backend. - Updated
mcp/secrets_tools_test.gomock to implementsecrets.Backend(addedName()method, usessecrets.ErrNotFoundinstead of custom error).
- Removed
- Risk class: read_only
C. Security fixes (critical, non-Infisical)
C1. nomos.hubris.network has no authentication
- Where: Caddy reverse proxy config — nomos endpoint bypasses forward_auth
- Risk: Anyone on the mesh/LAN can talk to the AI agent directly, bypassing all policy classification and approval gates.
- Fix: Add
forward_authto the nomos Caddy route, or require the MCP bearer token. At minimum, add a shared secret via Caddybasicauth. - Risk class: config_mutation
C2. pg_dump failure is silently ignored
- Where:
scripts/deploy.sh—pg_dump ... || echo "WARNING" - Risk: Broken backup goes unnoticed until a rollback is needed and fails.
- Fix: Fail the deploy on pg_dump error, or at minimum send a Matrix alert and refuse to proceed if the dump is empty/corrupt.
- Risk class: config_mutation
C3. CORS defaults to *
- Where:
internal/httpapi/server.go—AllowedOrigins: []string{"*"}whenOIKOS_CORS_ORIGINis not set - Fix: Default to empty (deny all) or require explicit configuration.
- Risk class: config_mutation
D. Operational improvements (high)
D1. Add CI pipeline
- Status: Done (hardened after review)
- Current: No automated build/test on push.
make lint test generate-checkexists but is manual. - Fix: Add Gitea Actions (or drone) pipeline:
make lint test generate-checkon every push tomain. Block deploy if pipeline fails. - What changed: The Gitea Actions pipeline already exists
(
.gitea/workflows/ci.yml). Added the missing deploy gate as step [1/8] inscripts/deploy.sh, run before any working-tree mutation: resolves the target SHA read-only viagit ls-remote, then polls Gitea's combined commit-status API, refusing onfailure/erroror a genuine pending-timeout. Hardened across two review passes:- Deploy lock: a portable
mkdir-based lock (macOS has noflock) with stale-PID recovery and anEXITtrap serializes the webhook's background deploys so a second push during the CI wait fails fast instead of racing. - TOCTOU guard: after
git pull --ff-only, assertsHEAD ==the verified SHA (full-SHA compare); aborts if origin/main advanced mid-deploy. - Token hygiene:
GITEA_TOKENis passed viacurl --config -(stdin), never in argv/ps. - Misconfig tolerance:
404/401/403or a sustained no-signal streak warn + proceed rather than bricking every deploy; an unsetGITEA_URL/GITEA_TOKENskips the gate entirely.
- Deploy lock: a portable
- Risk class: config_mutation
D2. Version Docker images
- Status: Done
- Current: All images built as
:latest. Rollback requires full rebuild. - Fix: Tag images with
v$VERSIONfrom the VERSION file in deploy.sh. Keep last 3 versions. Enabledocker compose upto pin a version tag. - What changed: Every built compose service now carries an
image: oikos-<svc>:${OIKOS_VERSION:-latest}tag.deploy.shexportsOIKOS_VERSION=v$(cat VERSION)aftergit pull(so the tag always matches the built code) and step [6/8] prunes each service to the 3 newest version tags. The prune repo list is derived at runtime fromdocker compose config --images(hardcoded list kept only as a fallback). - Risk class: config_mutation
D3. Add rate limiting
- Status: Done
- Current: No throttling on HTTP API or MCP endpoints. An agent in a loop could hammer the API or exhaust DB connections.
- Fix: Add
golang.org/x/time/ratemiddleware to chi router. Per-IP or per-token rate limit with burst allowance. Separate limits for API vs MCP. - What changed: New
internal/httpapi/ratelimit.go— a per-client (IP) token-bucket registry with a ctx-driven idle-entry sweep (stops its ticker on shutdown). Wired intoNewHandlerbefore CORS/auth;/healthzis exempt. Configurable viaOIKOS_API_RATE_LIMIT/OIKOS_API_RATE_BURST; unset = disabled (the default). Returns RFC 9457 429 + Retry-After.x/timepromoted to a direct dependency.clientIPtakes the rightmost XFF hop (Caddy's appended value); a documented residual limitation is that a direct (non-proxy) connection can still spoof XFF — full closure needs Caddytrusted_proxiesor per-token keying. - Risk class: config_mutation
D4. Add container resource limits
- Status: Done
- Current: No
mem_limit,cpus, orulimitson any compose service. - Fix: Add memory and CPU limits to all services in docker-compose.yml. Suggested: API 512MB, scheduler 256MB, notifier 128MB, nomos 512MB.
- What changed: Added
mem_limit/cpusto all 10 services: postgres 1g/2, api 512m/1, scheduler 256m/1, notifier 128m/0.5, nomos 512m/1, web 64m/0.25, redis 128m/0.5, infisical 512m/1, migrate/seed 512m/1. - Risk class: config_mutation
D5. Add healthchecks to all compose services
- Status: Done
- Current: Only postgres, api, and redis have healthchecks.
- Fix: Add
healthcheckto scheduler, notifier, and nomos. Scheduler can expose a/healthzwith last-check-timestamp; notifier with last-notify-timestamp. - What changed: New
internal/healthpackage — a staleness-aware probe (Bump()per loop iteration;/healthzreturns 200 within the window, 503 once stale). Wired intoscheduler.Run(:8093, 3× interval) andnotifier.Run(:8094, 2 min); nomos already served:8092/healthz. Added compose healthchecks for scheduler, notifier, and nomos. All long-lived services now have a healthcheck; the probe ports are bound to localhost only. - Risk class: config_mutation
E. Code quality (medium)
E1. Split monolithic files
| File | Lines | Split target |
|---|---|---|
internal/mcp/tools.go |
1774 | entity_tools.go, ops_tools.go, knowledge_tools.go, analysis_tools.go |
internal/httpapi/impl.go |
1533 | Handler group files by domain (entities, executions, approvals, metrics, etc.) |
cmd/nomos/main.go |
1127 | server.go, workers.go, mcp.go (partially done — agent.go and store.go exist) |
E2. Migrate raw pool.Exec queries to sqlc
- ~50% of DB access in HTTP/MCP handlers bypasses sqlc with raw
pool.Exec/pool.QueryRow. - Add these queries to
internal/db/queries/source files for type safety and compile-time validation.
E3. Unify SSH implementations
- Scheduler uses
os/exec ssh(system binary), MCP/actuator usescrypto/ssh. - Unify on
crypto/sshthroughout for consistency, testability, and connection multiplexing (single TCP connection, multiple sessions). - Consider a shared SSH pool in
internal/actuator/used by both scheduler and MCP.
E4. Fix lifecycle attribute check
internal/db/lifecycle.go:checkPreconditionusesstrings.Contains(attrs, want)on raw JSONB text, bypassing the GIN index.- Parse attributes properly and use
@>or?JSONB operators.
E5. Add table-driven tests for core logic
Priority packages (currently 0% coverage):
internal/policy— risk classification rules (table-driven with seed policy.yaml cases)internal/domain— lifecycle state machine transitionsinternal/scheduler— check dispatch and signal resolutioninternal/ontology— monitoring resolution and type tree traversalinternal/checkdefaults— check derivation from monitoring specs
F. Performance (medium)
F1. SSH connection pooling for scheduler
- At 30s intervals with 95 entities and multiple check types, the scheduler can
spawn 100+ SSH sessions per cycle via
os/exec ssh. - Migrate to
crypto/sshwith persistent connection pools to Proxmox hosts. One TCP connection per host, multiplexed sessions for individual checks.
F2. Entity lookup cache
- Repeated
get_entity/whoamiMCP calls hit the DB every time. - Add an in-memory TTL cache (hashicorp/golang-lru, already in go.mod) with 60s TTL for entity lookups. Invalidate on write.
F3. Trigram index for entity search
ListEntitiesusesILIKE '%'||q||'%'which cannot use B-tree indexes.- Add GIN trigram indexes on
entities.slugandentities.name. - Alternative: migrate to
tsvectorfull-text search matching the knowledge pattern.
F4. Composite index for auto-act anti-join
GetOpenSignalsForAutoActjoins classifications → signals → executions withWHERE e.entity_id IS NULL. No composite index on(classification_id, entity_id).- Add partial index on
executions(classification_id) WHERE entity_id IS NOT NULL.
G. Observability (low)
G1. Add OpenTelemetry tracing
- OTel SDK is already in go.mod as indirect dependency.
- Instrument HTTP handlers, MCP tools, and DB queries with spans.
- Propagate trace context via
correlation_id(already exists in audit/events).
G2. Prometheus metrics export
- Expose
/metricsendpoint for Go runtime, DB pool stats, scheduler check duration/counts, HTTP request latency histograms. - Complement the existing TimescaleDB metric_samples (which are entity health metrics, not self-observability).
G3. Automate offsite backups
- Proton Drive backup target entity exists but no pipeline.
- Add
rclone crontopg_dump | zstd | rclone syncto Proton Drive. - Weekly backup verification (restore to test DB, run
make test-db).
H. Infrastructure (low)
H1. Pin Infisical image version
- Currently uses
infisical/infisical:latest. - Pin to a specific version tag.
H2. Add persistent job queue for executions
- All background work is in-process goroutines — lost on restart.
- For the execution pipeline specifically, consider Postgres-backed queue
(e.g.,
riveror custompending_executionspoll with advisory lock). - Lower priority: scheduler and notifier state is transient and self-healing.
H3. Replace or harden custom migration splitter
- The
splitSQL()function handles$$dollar-quoting but edge cases with string literals containing$$could break migrations. - Add test cases for nested quoting, or adopt golang-migrate.
H4. Add distributed locking for scheduler
- Document single-instance constraint, or add
pg_advisory_lock(already used by migration runner) to prevent duplicate health checks if multiple scheduler instances are accidentally started.
Execution order
- Phase 0 — Infisical consolidation (B1–B7): Wire Infisical into all
binaries, migrate secrets from env/plaintext, activate SOPS fallback, remove
.envsecrets and plist HMAC. This is the foundation — every subsequent secret-dependent change (SSH host keys in B5, rate limit config, etc.) goes through Infisical. Do this first. - Phase 1 — Security (C1–C3, B5): Nomos auth, pg_dump failure, CORS default, SSH host keys (now stored in Infisical per B5).
- Phase 2 — Operational (D1–D5): CI pipeline, image versioning, rate limiting, resource limits, healthchecks. Done.
- Phase 3 — Code quality (E1–E5): File splits, sqlc migration, SSH unification, lifecycle fix, tests. E1–E3 are large refactors — do one file/area per commit.
- Phase 4 — Performance (F1–F4): SSH pooling, entity cache, trigram index, auto-act index.
- Phase 5 — Observability (G1–G3): OTel tracing, Prometheus, offsite backups.
- Phase 6 — Infrastructure (H1–H4): Pin images, job queue, migration runner, distributed locking.
Phase 0 is the gate. Once all secrets flow through Infisical, phases 1–2 can proceed. Phases 3–4 should wait for CI (D1) so refactors are validated. Phases 5–6 are backlog.
Phase 0 post-deploy checklist
Run these on mac-mini after deploying the Phase 0 code changes.
Step 1: Populate Infisical with secrets
For each secret, read the current value from .env and store it in Infisical:
# Values from .env — read them first, then set
oikos secret set matrix/token "$(grep OIKOS_MATRIX_TOKEN .env | cut -d= -f2-)"
oikos secret set approval/hmac-secret "$(grep OIKOS_APPROVAL_HMAC_SECRET .env | cut -d= -f2-)"
oikos secret set mcp/bearer-token "$(grep OIKOS_MCP_BEARER_TOKEN .env | cut -d= -f2-)"
oikos secret set api/token "$(grep OIKOS_API_TOKEN .env | cut -d= -f2-)"
oikos secret set oidc/client-secret "$(grep OIKOS_OIDC_CLIENT_SECRET .env | cut -d= -f2-)"
oikos secret set openrouter/api-key "$(grep OPENROUTER_API_KEY .env | cut -d= -f-)"
oikos secret set webhook/hmac-secret "$(grep WEBHOOK_HMAC_SECRET .env | cut -d= -f2-)"
Verify: oikos secret list should show all 8 keys.
Step 2: Pre-populate SSH host keys (optional, skip if TOFU is acceptable)
# For each Proxmox host, scan and store the public key
for host in pve1 pve2; do
key=$(ssh-keyscan -t ed25519 $host 2>/dev/null | awk '{print $2" "$3}')
oikos secret set "ssh/host-keys/$host" "ssh-ed25519 $key"
done
Alternatively, skip this step — the first deployment will TOFU-accept all current host keys and persist them to Infisical automatically.
Step 3: Remove HMAC secret from webhook plist
On mac-mini:
sudo launchctl unload ~/Library/LaunchAgents/network.hubris.oikos-deploy-webhook.plist
# Edit the plist: remove the <key>WEBHOOK_HMAC_SECRET</key> block
sudo launchctl load ~/Library/LaunchAgents/network.hubris.oikos-deploy-webhook.plist
Step 4: Strip secrets from .env
Edit .env to remove the 7 migrated secrets, keeping only bootstrap and
non-secret config:
# Remove these lines:
# INFISICAL_ENCRYPTION_KEY=...
# OIKOS_MATRIX_TOKEN=...
# OIKOS_INFISICAL_CLIENT_ID=...
# OIKOS_INFISICAL_CLIENT_SECRET=...
# OIKOS_INFISICAL_PROJECT_ID=...
# OPENROUTER_API_KEY=...
# OIKOS_MCP_BEARER_TOKEN=...
# Keep these (bootstrap / non-secret):
# OIKOS_DATABASE_URL=...
# OIKOS_API_LISTEN=...
# OIKOS_INFISICAL_SITE_URL=...
# OIKOS_INFISICAL_ENV=...
Step 5: Verify
oikos secret list— 8 keys + SSH host keysdocker compose logs api | grep "secrets resolved"— should show count=5- Trigger a test deploy — webhook should still validate HMAC signatures
nomosshould start and resolve secrets from Infisical (check logs fornomos: secrets resolved from Infisical)- Verify no secrets appear in process env:
docker compose exec api env | should not showOIKOS_MCP_BEARER_TOKEN,OIKOS_MATRIX_TOKEN`, etc. (they come from Infisical at startup, not env)