From dd0c82127274a976e1357f2cbbf16b9db84828a5 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Thu, 28 May 2026 07:38:21 +1000 Subject: [PATCH] =?UTF-8?q?docs(plans):=20cloud=20deployment=20plan=20(fam?= =?UTF-8?q?ily=20testing=20phase)=20=E2=80=94=20draft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan-stage document for Oracle Cloud VM deployment with family-scale hardening. Covers architecture overview, TLS termination, iptables / OCI Security List policy, OAuth credential transfer, auth model (allow_anonymous off, per-key tier mapping), audit visibility, and the Phase 7 sandbox-runtime prerequisite. Status: Draft, pending Phase 7 (sandbox-runtime integration) completion. This commit makes the plan referenceable from ADR 0009 Amendment 1 § Caveats #3, the 2026-05-27 incident memory (cc-rules), and the forthcoming Phase 7 charter. NOT a code change — no ALIGNMENT.md authority citation required. Co-Authored-By: Claude Opus 4.7 --- docs/plans/cloud-deployment-family.md | 655 ++++++++++++++++++++++++++ 1 file changed, 655 insertions(+) create mode 100644 docs/plans/cloud-deployment-family.md diff --git a/docs/plans/cloud-deployment-family.md b/docs/plans/cloud-deployment-family.md new file mode 100644 index 0000000..2542bdc --- /dev/null +++ b/docs/plans/cloud-deployment-family.md @@ -0,0 +1,655 @@ +# OLP Cloud Deployment Plan — Family Testing Phase + +**Status:** Draft — pending current Phase 6 completion +**Target:** Oracle Cloud VM (existing infrastructure) +**Audience:** Project maintainer deployment reference +**Scope:** Single-VM deployment for family (3–5 users), spawn-binary architecture, public internet exposure with hardened auth + +--- + +## 0. Prerequisites + +- OLP current phase (Phase 6) is closed and tagged +- Oracle Cloud VM accessible via SSH (existing `opc` user) +- Domain name (optional but strongly recommended for TLS) +- Provider CLI OAuth completed on at least one machine (credentials transferable) + +--- + +## 1. Architecture Overview + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ Family Devices (anywhere on internet) │ +│ │ +│ Wife iPad / Kid Laptop / Maintainer MacBook / ... │ +│ IDE: Cline / Continue.dev / Cursor / Aider / OpenClaw │ +│ Config: OPENAI_BASE_URL=https://olp.example.com/v1 │ +│ OPENAI_API_KEY=olp_ │ +└──────────────────────────┬─────────────────────────────────────────┘ + │ HTTPS (TLS 1.3) + ▼ +┌────────────────────────────────────────────────────────────────────┐ +│ Oracle Cloud VM │ +│ │ +│ ┌─ iptables / OCI Security List ──────────────────────────────┐ │ +│ │ ALLOW: TCP 443 (HTTPS) from 0.0.0.0/0 │ │ +│ │ ALLOW: TCP 22 (SSH) from maintainer IP only │ │ +│ │ DENY: everything else │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Nginx (reverse proxy + TLS termination) ───────────────────┐ │ +│ │ :443 → TLS (Let's Encrypt auto-renew via certbot) │ │ +│ │ proxy_pass → http://127.0.0.1:4567 │ │ +│ │ Rate limit: 30 req/min per IP (burst 10) │ │ +│ │ Request body limit: 1MB │ │ +│ │ Connection timeout: 300s (streaming needs long timeout) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ OLP server.mjs ───────────────────────────────────────────┐ │ +│ │ OLP_BIND=127.0.0.1 (loopback only — Nginx fronts it) │ │ +│ │ OLP_PORT=4567 │ │ +│ │ auth.allow_anonymous: false │ │ +│ │ auth.advertise_anonymous_key: false │ │ +│ │ Per-key audit logging to ~/.olp/logs/audit.ndjson │ │ +│ │ Owner key: maintainer only │ │ +│ │ Guest keys: one per family member │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Provider CLIs (installed on this VM) ──────────────────────┐ │ +│ │ claude → ~/.claude/.credentials.json (OAuth) │ │ +│ │ codex → ~/.codex/auth.json (OAuth) │ │ +│ │ vibe → ~/.vibe/.env (API key) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ systemd service ──────────────────────────────────────────┐ │ +│ │ olp.service: auto-start, auto-restart on crash │ │ +│ │ Runs as dedicated `olp` user (not root, not opc) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────┘ + │ + │ Provider CLIs spawn outbound HTTPS calls + ▼ + Anthropic API / OpenAI API / Mistral API +``` + +--- + +## 2. Security Design (7 Layers) + +### Layer 1 — Network Perimeter (OCI Security List + iptables) + +**Principle:** Minimum attack surface. Only two ports reachable from the internet. + +``` +OCI Security List (stateful ingress rules): + ┌──────────┬────────────┬───────────────────────────────┐ + │ Port │ Protocol │ Source │ + ├──────────┼────────────┼───────────────────────────────┤ + │ 443 │ TCP │ 0.0.0.0/0 (public HTTPS) │ + │ 22 │ TCP │ /32 only │ + └──────────┴────────────┴───────────────────────────────┘ + + NOT exposed: + - Port 4567 (OLP direct) — Nginx fronts it + - Port 80 (HTTP) — only for certbot ACME challenge, redirect to 443 +``` + +**iptables backup** (defense in depth — OCI Security List is primary, iptables is secondary): + +```bash +# Drop everything by default +sudo iptables -P INPUT DROP +sudo iptables -P FORWARD DROP + +# Allow established connections +sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Allow loopback +sudo iptables -A INPUT -i lo -j ACCEPT + +# Allow SSH from maintainer IP only +sudo iptables -A INPUT -p tcp --dport 22 -s -j ACCEPT + +# Allow HTTPS from anywhere +sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT + +# Allow HTTP (certbot ACME only — Nginx redirects everything else) +sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT + +# Persist +sudo iptables-save | sudo tee /etc/iptables/rules.v4 +``` + +### Layer 2 — TLS Termination (Nginx + Let's Encrypt) + +**Principle:** All client traffic encrypted. OLP itself runs plain HTTP on loopback — simpler, no cert management in Node. + +```nginx +# /etc/nginx/sites-available/olp.conf + +# Redirect HTTP → HTTPS +server { + listen 80; + server_name olp.example.com; + + # Let's Encrypt ACME challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# HTTPS — TLS 1.3 only +server { + listen 443 ssl http2; + server_name olp.example.com; + + # TLS config + ssl_certificate /etc/letsencrypt/live/olp.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/olp.example.com/privkey.pem; + ssl_protocols TLSv1.3; # TLS 1.3 only + ssl_prefer_server_ciphers off; # TLS 1.3 manages its own + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + + # Security headers + add_header Strict-Transport-Security "max-age=63072000" always; + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + + # Rate limiting (per IP) + limit_req zone=olp_limit burst=10 nodelay; + + # Request body size (LLM prompts can be large but cap at 1MB) + client_max_body_size 1m; + + # Proxy to OLP + location / { + proxy_pass http://127.0.0.1:4567; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # SSE streaming support (critical for /v1/chat/completions) + proxy_set_header Connection ''; + proxy_buffering off; # Don't buffer SSE + proxy_cache off; + chunked_transfer_encoding on; + + # Long timeouts for LLM inference + proxy_connect_timeout 10s; + proxy_read_timeout 300s; # 5 min — long reasoning + proxy_send_timeout 300s; + } +} + +# Rate limit zone definition (in http {} block of nginx.conf) +# limit_req_zone $binary_remote_addr zone=olp_limit:10m rate=30r/m; +``` + +**Certbot auto-renewal:** + +```bash +sudo certbot certonly --webroot -w /var/www/certbot -d olp.example.com +# Auto-renew via systemd timer (certbot installs this automatically) +``` + +### Layer 3 — Application Auth (OLP Multi-Key) + +**Principle:** Every request must carry a valid API key. No anonymous access. Per-key audit trail. + +```json +// ~/.olp/config.json on the cloud VM +{ + "auth": { + "allow_anonymous": false, + "advertise_anonymous_key": false, + "owner_only_endpoints": [ + "/health", + "/v0/management/dashboard-data", + "/v0/management/quota", + "/v0/management/status", + "/cache/stats", + "/dashboard" + ], + "fallback_detail_header_policy": "owner_only" + } +} +``` + +**Key provisioning plan:** + +``` +┌───────────────┬──────────┬─────────────────────────────────────┐ +│ Key name │ Tier │ providers_enabled │ +├───────────────┼──────────┼─────────────────────────────────────┤ +│ cloud-owner │ owner │ all (dashboard + management access) │ +│ wife-ipad │ guest │ anthropic, openai │ +│ kid-laptop │ guest │ anthropic only (cost control) │ +│ maintainer-mb │ guest │ all (daily driver, not owner tier) │ +└───────────────┴──────────┴─────────────────────────────────────┘ +``` + +**Why maintainer uses a guest key for daily driving:** owner key gives access to management endpoints. Routine IDE usage should not carry owner privilege. Owner key is used only for dashboard access and administration. + +**Key lifecycle:** +- Keys generated on the cloud VM via `olp-keys keygen` +- Plaintext token communicated to family member via secure channel (Signal / iMessage, not email) +- Each key logged independently in audit.ndjson (per-key `key_id` field) +- Revocation: `olp-keys revoke --id=` — immediate, no grace period + +### Layer 4 — Process Isolation (Dedicated User + systemd) + +**Principle:** OLP runs as a non-root, non-login user. Crash recovery is automatic. + +```bash +# Create dedicated user +sudo useradd --system --shell /usr/sbin/nologin --home-dir /opt/olp olp + +# OLP code +sudo mkdir -p /opt/olp +sudo git clone https://github.com/dtzp555-max/olp.git /opt/olp/app +sudo chown -R olp:olp /opt/olp + +# OLP data (keys, config, logs, cache) +sudo mkdir -p /home/olp/.olp/{keys,logs,cache} +sudo chown -R olp:olp /home/olp +``` + +**systemd unit:** + +```ini +# /etc/systemd/system/olp.service +[Unit] +Description=OLP — Open LLM Proxy +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=olp +Group=olp + +WorkingDirectory=/opt/olp/app +ExecStart=/usr/bin/node server.mjs + +# Environment +Environment=OLP_BIND=127.0.0.1 +Environment=OLP_PORT=4567 +Environment=NODE_ENV=production +Environment=HOME=/home/olp + +# Auto-restart on crash +Restart=on-failure +RestartSec=5 +StartLimitIntervalSec=60 +StartLimitBurst=5 + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=false +ReadWritePaths=/home/olp/.olp +PrivateTmp=true + +# Resource limits +LimitNOFILE=65536 +MemoryMax=1G + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=olp + +[Install] +WantedBy=multi-user.target +``` + +### Layer 5 — Credential Protection (Provider OAuth Tokens) + +**Principle:** OAuth tokens are the crown jewels. Stolen tokens = someone else using your Claude/OpenAI subscription. + +``` +Credential storage on cloud VM: + +~olp/ +├── .claude/ +│ └── .credentials.json # chmod 600, owner=olp +├── .codex/ +│ └── auth.json # chmod 600, owner=olp +└── .vibe/ + └── .env # chmod 600, owner=olp + +Security measures: + 1. chmod 600 on all credential files (olp user only) + 2. Credential files NOT in the git repo (already .gitignored) + 3. No credential in env vars (OLP reads from filesystem) + 4. Credential transfer: scp from local machine, then delete local copy of the scp command from shell history + 5. Periodic rotation: re-auth quarterly (or on any suspicion of compromise) +``` + +**Credential transfer procedure:** + +```bash +# FROM maintainer's Mac mini (one-time): + +# 1. Claude credentials +scp ~/.claude/.credentials.json opc@:/tmp/claude-cred.json +ssh opc@ "sudo mv /tmp/claude-cred.json /home/olp/.claude/.credentials.json && sudo chown olp:olp /home/olp/.claude/.credentials.json && sudo chmod 600 /home/olp/.claude/.credentials.json" + +# 2. Codex credentials +scp ~/.codex/auth.json opc@:/tmp/codex-cred.json +ssh opc@ "sudo mv /tmp/codex-cred.json /home/olp/.codex/auth.json && sudo chown olp:olp /home/olp/.codex/auth.json && sudo chmod 600 /home/olp/.codex/auth.json" + +# 3. Mistral API key +ssh opc@ "sudo -u olp bash -c 'echo MISTRAL_API_KEY=sk-xxx > ~/.vibe/.env && chmod 600 ~/.vibe/.env'" + +# 4. Verify +ssh opc@ "sudo -u olp node /opt/olp/app/bin/olp.mjs doctor --json" | jq '.checks[] | select(.name | contains("auth"))' +``` + +### Layer 6 — Audit and Monitoring + +**Principle:** Every request logged. Anomalies detectable. No silent failures. + +**Audit (already built into OLP):** +- `~/.olp/logs/audit.ndjson` — append-only, per-request, includes `key_id`, provider, model, cache hit/miss, fallback hops +- Daily rotation: `audit-YYYY-MM-DD.ndjson` (built-in, triggers on first append after UTC midnight) +- External rotation tool: `olp-audit-rotate` (idempotent, cron-safe) + +**Additional monitoring for cloud deployment:** + +```bash +# Cron: daily audit rotation (belt-and-suspenders alongside in-server rotation) +0 0 * * * /usr/bin/node /opt/olp/app/bin/olp-audit-rotate.mjs + +# Cron: daily health check + alert +*/5 * * * * curl -sf -H "Authorization: Bearer $OLP_OWNER_KEY" https://olp.example.com/health > /dev/null || echo "OLP health check failed at $(date)" >> /home/olp/alerts.log + +# Cron: audit log size check (alert if >100MB — suggests anomalous traffic) +0 6 * * * find /home/olp/.olp/logs -name 'audit*.ndjson' -size +100M -exec echo "Large audit log: {}" \; >> /home/olp/alerts.log + +# Cron: disk usage check +0 6 * * * df -h / | awk 'NR==2 && $5+0 > 80 {print "Disk usage above 80%: "$5}' >> /home/olp/alerts.log +``` + +**What to watch for (manually, weekly):** +1. `olp-keys list` — any unexpected keys? +2. Dashboard (`/dashboard`) — unusual request volume? Unknown providers being hit? +3. `journalctl -u olp --since "7 days ago" | grep -c ERROR` — error spike? +4. Audit log: `grep "fallback" ~/.olp/logs/audit.ndjson | wc -l` — fallback frequency (high = provider instability) + +### Layer 7 — Update and Recovery + +**Principle:** Rollback within 60 seconds. No data loss on failed update. + +**Update procedure:** + +```bash +# SSH to cloud VM as opc + +# 1. Snapshot before update (Oracle Cloud console or CLI) +# OCI CLI: oci compute boot-volume-backup create ... + +# 2. Pull latest code +cd /opt/olp/app +sudo -u olp git fetch origin main +sudo -u olp git log --oneline HEAD..origin/main # review what's coming + +# 3. Run tests BEFORE deploying +sudo -u olp git checkout main +sudo -u olp git pull +sudo -u olp node test-features.mjs +# STOP if tests fail + +# 4. Restart service +sudo systemctl restart olp +sleep 3 +sudo systemctl status olp # verify running + +# 5. Smoke test +curl -sf -H "Authorization: Bearer $OLP_OWNER_KEY" https://olp.example.com/health | jq .ok +# Expect: true +``` + +**Rollback:** + +```bash +# If update breaks things: +cd /opt/olp/app +sudo -u olp git checkout # e.g. v0.6.0 +sudo systemctl restart olp +``` + +**Backup (automated):** + +```bash +# Cron: daily backup of OLP state (keys + config + recent audit) +0 3 * * * tar czf /home/opc/backups/olp-state-$(date +\%Y\%m\%d).tar.gz -C /home/olp .olp/keys .olp/config.json .olp/logs/audit.ndjson 2>/dev/null; find /home/opc/backups -name 'olp-state-*' -mtime +30 -delete +``` + +--- + +## 3. Implementation Checklist + +Execute in order. Each step has a verification gate — do not proceed if the gate fails. + +### Phase A — VM Preparation + +``` +[ ] A1. SSH to Oracle Cloud VM, verify Node.js >= 18 + Gate: `node --version` prints v18+ + +[ ] A2. Create `olp` system user + Gate: `id olp` shows the user exists + +[ ] A3. Clone OLP repo to /opt/olp/app + Gate: `sudo -u olp node /opt/olp/app/test-features.mjs` — all tests pass + +[ ] A4. Install provider CLIs (as olp user) + - npm install -g @anthropic-ai/claude-code + - npm install -g @openai/codex + - (mistral vibe if needed) + Gate: `which claude && which codex` both resolve + +[ ] A5. Transfer OAuth credentials (Layer 5 procedure) + Gate: `sudo -u olp claude auth status` shows authenticated +``` + +### Phase B — Security Hardening + +``` +[ ] B1. Configure OCI Security List (Layer 1) + Gate: nmap from external IP shows only 22 and 443 open + +[ ] B2. Configure iptables backup (Layer 1) + Gate: `sudo iptables -L -n` matches the plan + +[ ] B3. Install + configure Nginx (Layer 2) + Gate: `curl -I http://olp.example.com` returns 301 → HTTPS + +[ ] B4. Obtain Let's Encrypt certificate + Gate: `curl -I https://olp.example.com` returns valid cert + +[ ] B5. Verify Nginx SSE passthrough + Gate: test streaming request completes without timeout +``` + +### Phase C — OLP Configuration + +``` +[ ] C1. Write ~/.olp/config.json (Layer 3 — auth config) + Gate: config validates (no startup warnings in journal) + +[ ] C2. Generate owner key + Gate: `olp-keys list --owner-only` shows 1 owner key + +[ ] C3. Generate family guest keys (one per person) + Gate: `olp-keys list` shows correct count + +[ ] C4. Install systemd unit (Layer 4) + Gate: `systemctl status olp` shows active (running) + +[ ] C5. Verify /health with owner key + Gate: `curl -H "Authorization: Bearer $OWNER_KEY" https://olp.example.com/health | jq .ok` → true + +[ ] C6. Verify /health rejects unauthenticated + Gate: `curl https://olp.example.com/health` → 401 + +[ ] C7. Verify guest key cannot access /dashboard + Gate: `curl -H "Authorization: Bearer $GUEST_KEY" https://olp.example.com/dashboard` → 403 + +[ ] C8. End-to-end LLM request with guest key + Gate: streaming chat completion returns a valid response +``` + +### Phase D — Monitoring Setup + +``` +[ ] D1. Install cron jobs (Layer 6) + Gate: `crontab -l` shows all 4 jobs + +[ ] D2. Verify daily backup cron + Gate: manual trigger produces valid tar.gz + +[ ] D3. Test health-check alert + Gate: stop OLP, wait 5min, check alerts.log has entry +``` + +### Phase E — Family Onboarding + +``` +[ ] E1. Send each family member their API key via Signal/iMessage + (NOT via email, NOT via any cloud-stored medium) + +[ ] E2. Each family member configures their IDE: + export OPENAI_BASE_URL=https://olp.example.com/v1 + export OPENAI_API_KEY=olp_ + +[ ] E3. Each family member runs a test prompt + Gate: audit.ndjson shows their key_id in the log + +[ ] E4. Verify per-key provider scoping + Gate: kid's key cannot hit providers outside their scope +``` + +--- + +## 4. Security Threat Model + +| Threat | Mitigation | Residual Risk | +|---|---|---| +| **Brute-force API key** | 32-byte entropy = 2^256 keyspace; Nginx rate limit 30r/m | Negligible | +| **TLS downgrade** | TLS 1.3 only; HSTS header | None with modern clients | +| **Credential theft (OAuth tokens on VM)** | chmod 600 + dedicated user + no root access to OLP dirs | VM root compromise (mitigated by OCI IAM) | +| **Stolen guest key** | Single-key revocation via `olp-keys revoke`; per-key audit trail for forensics | Window between theft and detection | +| **DDoS** | OCI DDoS protection (free tier) + Nginx rate limit + Nginx connection limit | Sustained volumetric attack may overwhelm free-tier VM | +| **Provider credential abuse** | OLP is the only consumer; anomalous spend visible on provider dashboard | Provider-side detection lag | +| **Supply chain (OLP code tampered)** | Git clone from known repo; `npm test` before deploy; no npm dependencies | Compromised maintainer GitHub account | +| **Log exfiltration** | audit.ndjson contains no message content (PII guard per ADR 0008); only metadata | Key IDs in logs (low sensitivity) | + +--- + +## 5. Operational Runbooks + +### Runbook: OAuth Token Expired + +``` +Symptom: /health shows provider auth.ok=false; fallback firing on every request +Diagnosis: sudo -u olp claude auth status → "not authenticated" or expired + +Fix: + 1. sudo -u olp claude setup-token + 2. Complete OAuth flow (browser URL → paste code) + 3. Verify: sudo -u olp claude auth status → authenticated + 4. No OLP restart needed — next spawn picks up new credentials +``` + +### Runbook: Revoke a Compromised Key + +``` +Symptom: suspicious traffic in audit.ndjson from a specific key_id + grep "" ~/.olp/logs/audit.ndjson | tail -20 + +Fix: + 1. olp-keys revoke --id= + 2. Notify family member: "Your key was revoked. Here's a new one." + 3. olp-keys keygen --name= --providers= + 4. Send new key via secure channel +``` + +### Runbook: VM Disk Full + +``` +Symptom: OLP stops writing audit logs; new requests may fail +Diagnosis: df -h / + +Fix: + 1. Purge old audit logs: find ~/.olp/logs -name 'audit-202*.ndjson' -mtime +90 -delete + 2. Purge old backups: find /home/opc/backups -name 'olp-state-*' -mtime +60 -delete + 3. Purge cache if needed: rm -rf ~/.olp/cache/* + 4. Verify: df -h / shows >20% free +``` + +### Runbook: OLP Process Crash Loop + +``` +Symptom: systemctl status olp shows "activating (auto-restart)" +Diagnosis: journalctl -u olp --since "10 min ago" | tail -50 + +Common causes: + - Port conflict → check `lsof -nP -iTCP:4567` + - Corrupt config.json → validate JSON syntax + - Node.js version drift → `node --version` + +Fix: + 1. Fix root cause + 2. sudo systemctl restart olp + 3. Gate: `curl -H "Authorization: Bearer $OWNER_KEY" https://olp.example.com/health | jq .ok` +``` + +--- + +## 6. Cost Estimate (Oracle Cloud Free Tier) + +| Resource | Spec | Cost | +|---|---|---| +| VM | ARM Ampere A1 (4 OCPU, 24GB RAM) | **Free** (Always Free tier) | +| Boot volume | 200GB | **Free** (up to 200GB) | +| Outbound bandwidth | 10TB/month | **Free** (first 10TB) | +| Public IP | 1 reserved | **Free** | +| Domain | olp.example.com | ~$10/year (external registrar) | +| TLS cert | Let's Encrypt | **Free** | +| **Total** | | **~$10/year** (domain only) | + +Oracle Cloud's Always Free ARM VM is overprovisioned for this use case. OLP + Nginx + 3 provider CLIs will use <1GB RAM and negligible CPU (the LLM inference happens at the provider, not here). + +--- + +## 7. Migration Path to Commercial + +This family deployment is a stepping stone. When commercial service is ready: + +| Aspect | Family (this plan) | Commercial (future) | +|---|---|---| +| Upstream | spawn CLI (subscription) | direct API (commercial key) | +| Auth | OLP multi-key (filesystem) | Registration + billing system | +| TLS | Let's Encrypt (single domain) | Managed cert (Cloudflare / AWS ACM) | +| Compute | Single VM (Oracle Free) | Container cluster (auto-scale) | +| Monitoring | Cron + manual | Prometheus + Grafana + PagerDuty | +| Rate limit | Nginx per-IP | Per-key token bucket in OLP | +| Data | ~/.olp/ filesystem | PostgreSQL + S3 | + +The deployment experience from this plan directly informs the commercial architecture. Every operational runbook becomes a feature requirement for the commercial platform. + +--- + +**Authors:** project maintainer (with AI drafting assistance) +**Created:** 2026-05-27