IDENTITY

title: "Secrets Encryption, LiteLLM Config & Smoke Test"
type: ticket
subtype: infrastructure
purpose: "Encrypt the secrets file with SOPS+age, update litellm_config.yaml for Vertex AI, run full runtime smoke test."

GOVERNANCE

status: PLANNED
priority_index: 1
budget_cap: "$0.25"
assigned_agent: "Coder Swarm (Claude Sonnet 4.6 via Roo Code)"
approved_by: "Principal Architect (Claude Opus 4.6)"
sprint: "S001_base_and_mvp1"
depends_on: ["TICKET_031"]
last_updated: "2026-03-14 21:30"
copyright: "© 2026 STRUXIO.ai"

TICKET_034: Secrets Encryption, LiteLLM Config & Smoke Test

1. OBJECTIVE

Three tasks in sequence:
A. Set up SOPS + age encryption and encrypt infrastructure_secrets.yamlinfrastructure_secrets.enc.yaml
B. Update litellm_config.yaml to correctly reference Google Vertex AI credentials with the new Gemini model names
C. Run a full runtime smoke test to verify the DEVXIO portal renders and all services respond

2. CONSTRAINTS

3. ACCEPTANCE CRITERIA

4. EXECUTION PLAN

Sub-Task A: Install SOPS + age

# On Hetzner (Ubuntu 24.04)
# Install age
sudo apt-get update && sudo apt-get install -y age

# Install SOPS
curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
sudo mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops

# Verify
age --version
sops --version

Sub-Task B: Generate age Keypair

# Generate keypair
age-keygen -o ~/age-key.txt

# Display public key (we need this for SOPS config)
grep "public key:" ~/age-key.txt

IMPORTANT: ~/age-key.txt contains the private key. It stays on the server only. Never commit it. Note the public key output — it looks like age1xxxxx....

Sub-Task C: Create SOPS Config

FILE CREATE: STRUXIO_OS/01_state/.sops.yaml

creation_rules:
  - path_regex: infrastructure_secrets\.yaml$
    age: >-
      PUBLIC_KEY_FROM_STEP_B

Replace PUBLIC_KEY_FROM_STEP_B with the actual age1... public key from Sub-Task B.

Sub-Task D: Encrypt the Secrets File

cd ~/STRUXIO_Workspace/STRUXIO_OS/01_state

# Set the age key location for SOPS
export SOPS_AGE_KEY_FILE=~/age-key.txt

# Encrypt
sops --encrypt infrastructure_secrets.yaml > infrastructure_secrets.enc.yaml

# Verify round-trip
sops --decrypt infrastructure_secrets.enc.yaml > /tmp/secrets_test.yaml
diff infrastructure_secrets.yaml /tmp/secrets_test.yaml
# Expected: no output (files are identical)
rm /tmp/secrets_test.yaml

echo "Encryption round-trip verified."

Sub-Task E: Update litellm_config.yaml

FILE EDIT: STRUXIO_OS/02_infra/litellm_config.yaml

Ensure the model_list contains these entries (add or replace as needed). Keep any existing Anthropic/OpenAI entries intact:

model_list:
  # ── Anthropic Models ──────────────────────────────────────────────────────
  - model_name: struxio-active
    litellm_params:
      model: anthropic/claude-sonnet-4-6
      api_key: os.environ/ANTHROPIC_API_KEY

  - model_name: claude-sonnet-4-6
    litellm_params:
      model: anthropic/claude-sonnet-4-6
      api_key: os.environ/ANTHROPIC_API_KEY

  # ── Google Vertex AI Models (Gemini) ──────────────────────────────────────
  # These use service account auth via a JSON credential file.
  # The file is mounted into the container at /app/secrets/ via docker-compose.
  - model_name: gemini-3.1-pro
    litellm_params:
      model: vertex_ai/gemini-3.1-pro-preview
      vertex_project: "struxio-devxio-core"
      vertex_location: "us-central1"
      vertex_credentials: "/app/secrets/struxio-vertex-service-account.json"

  - model_name: gemini-3-flash
    litellm_params:
      model: vertex_ai/gemini-3-flash
      vertex_project: "struxio-devxio-core"
      vertex_location: "us-central1"
      vertex_credentials: "/app/secrets/struxio-vertex-service-account.json"

  - model_name: gemini-3.1-flash-lite
    litellm_params:
      model: vertex_ai/gemini-3.1-flash-lite-preview
      vertex_project: "struxio-devxio-core"
      vertex_location: "us-central1"
      vertex_credentials: "/app/secrets/struxio-vertex-service-account.json"

  # ── OpenAI (Whisper Only) ─────────────────────────────────────────────────
  - model_name: whisper-1
    litellm_params:
      model: openai/whisper-1
      api_key: os.environ/OPENAI_API_KEY

Sub-Task F: Update docker-compose.yml for Vertex Credentials Mount

FILE EDIT: STRUXIO_OS/02_infra/docker-compose.yml

In the LiteLLM service block, add a volume mount for the service account JSON:

  devxio-litellm:
    # ... existing config ...
    volumes:
      - ../01_state/struxio-vertex-service-account.json:/app/secrets/struxio-vertex-service-account.json:ro
      # ... any existing volumes ...

The :ro flag makes it read-only inside the container.

Sub-Task G: Rebuild and Smoke Test

cd ~/STRUXIO_Workspace/STRUXIO_OS

# Rebuild all containers
docker compose -f 02_infra/docker-compose.yml down
docker compose -f 02_infra/docker-compose.yml up -d --build

# Wait for startup
sleep 30

# ── Smoke Test Battery ──────────────────────────────────────────────────────

echo "=== Test 1: Frontend HTTP ==="
curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" http://89.167.96.154:3002

echo "=== Test 2: Frontend renders HTML ==="
DIVCOUNT=$(curl -s http://89.167.96.154:3002 | grep -c "<div")
echo "Div count: $DIVCOUNT"

echo "=== Test 3: Docker containers ==="
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

echo "=== Test 4: LiteLLM health ==="
curl -s http://localhost:4002/health || echo "LiteLLM health check failed"

echo "=== Test 5: FastAPI bridge ==="
curl -s http://89.167.96.154:8002/health || curl -s http://89.167.96.154:8002/docs || echo "Bridge health check failed — try /docs endpoint"

echo "=== Test 6: WebSocket handshake ==="
python3 -c "
import asyncio
try:
    import websockets
except ImportError:
    import subprocess, sys
    subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'websockets', '-q'])
    import websockets

async def test():
    try:
        async with websockets.connect('ws://89.167.96.154:8002/ws/execute') as ws:
            print('WS CONNECTED OK')
    except Exception as e:
        print(f'WS FAILED: {e}')
asyncio.run(test())
"

echo "=== Test 7: LiteLLM model list ==="
curl -s http://localhost:4002/model/info | python3 -m json.tool 2>/dev/null | head -30 || echo "Model info endpoint not available"

Paste ALL raw output into the LOG file. No interpretation — just evidence.

Sub-Task H: Commit and Push

cd ~/STRUXIO_Workspace/STRUXIO_OS

# Stage the encrypted file and configs (NOT the unencrypted secrets)
git add 01_state/infrastructure_secrets.enc.yaml
git add 01_state/.sops.yaml
git add 02_infra/litellm_config.yaml
git add 02_infra/docker-compose.yml

# Verify infrastructure_secrets.yaml is NOT staged
git diff --cached --name-only | grep -v "enc" | grep "secrets" && echo "⚠️ UNENCRYPTED SECRETS STAGED — ABORT" && exit 1

git commit -m "feat(infra): SOPS encryption, Vertex AI models in LiteLLM, credential mount"
git push origin main

5. AFFECTED FILES

STRUXIO_OS/01_state/.sops.yaml (NEW)
STRUXIO_OS/01_state/infrastructure_secrets.enc.yaml (UPDATED — encrypted)
STRUXIO_OS/02_infra/litellm_config.yaml (UPDATED — Gemini models added)
STRUXIO_OS/02_infra/docker-compose.yml (UPDATED — volume mount for Vertex JSON)

6. ROLLBACK

If LiteLLM fails to start after the config change:

  1. Revert litellm_config.yaml: git checkout HEAD -- 02_infra/litellm_config.yaml
  2. Restart: docker compose -f 02_infra/docker-compose.yml restart devxio-litellm
  3. Report the error output from docker logs devxio-litellm --tail 50

STRUXIO.ai // Confidential & Proprietary // Generated by Claude Opus 4.6 (Principal Architect) // © 2026