diff --git a/.github/workflows/e2e-staging-saas.yml b/.github/workflows/e2e-staging-saas.yml index c43e1200..c1e2b878 100644 --- a/.github/workflows/e2e-staging-saas.yml +++ b/.github/workflows/e2e-staging-saas.yml @@ -78,6 +78,10 @@ jobs: # retrieval + teardown. Configure in # Settings → Secrets and variables → Actions → Repository secrets. MOLECULE_ADMIN_TOKEN: ${{ secrets.MOLECULE_STAGING_ADMIN_TOKEN }} + # OpenAI key for workspace LLM calls (section 8 A2A). Without it, + # Hermes runtime crashes at boot with "No provider API key found". + # Configure at Settings → Secrets → Actions → MOLECULE_STAGING_OPENAI_KEY. + E2E_OPENAI_API_KEY: ${{ secrets.MOLECULE_STAGING_OPENAI_KEY }} E2E_RUNTIME: ${{ github.event.inputs.runtime || 'hermes' }} E2E_RUN_ID: "${{ github.run_id }}-${{ github.run_attempt }}" E2E_KEEP_ORG: ${{ github.event.inputs.keep_org && '1' || '0' }} @@ -93,6 +97,14 @@ jobs: fi echo "Admin token present ✓" + - name: Verify OpenAI key present + run: | + if [ -z "$E2E_OPENAI_API_KEY" ]; then + echo "::error::MOLECULE_STAGING_OPENAI_KEY secret not set — workspaces will fail at boot with 'No provider API key found'" + exit 2 + fi + echo "OpenAI key present ✓ (len=${#E2E_OPENAI_API_KEY})" + - name: CP staging health preflight run: | code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 10 "$MOLECULE_CP_URL/health") diff --git a/tests/e2e/test_staging_full_saas.sh b/tests/e2e/test_staging_full_saas.sh index 8e66f525..1218ae02 100755 --- a/tests/e2e/test_staging_full_saas.sh +++ b/tests/e2e/test_staging_full_saas.sh @@ -229,10 +229,27 @@ tenant_call() { } # ─── 5. Provision parent workspace ───────────────────────────────────── +# Runtimes like hermes crash at boot with "No provider API key found" +# if nothing in the standard env-var list is set. Inject the API key +# from E2E_OPENAI_API_KEY so the runtime can actually start — it's +# per-workspace secret, so it's persisted as a workspace_secret and +# materialized into the container env. Missing key falls through to +# an empty secrets map; workspace will still fail but the error is +# expected and actionable. +SECRETS_JSON='{}' +if [ -n "${E2E_OPENAI_API_KEY:-}" ]; then + # MODEL_PROVIDER is a full model slug in 'provider:model' format per + # workspace/config.py:258. Using just "openai" gets parsed as the + # model name → 404 model_not_found. Also set OPENAI_BASE_URL to + # OpenAI's own endpoint — default is openrouter.ai which would need + # a different key format. + SECRETS_JSON="{\"OPENAI_API_KEY\":\"$E2E_OPENAI_API_KEY\",\"OPENAI_BASE_URL\":\"https://api.openai.com/v1\",\"MODEL_PROVIDER\":\"openai:gpt-4o\"}" +fi + log "5/11 Provisioning parent workspace (runtime=$RUNTIME)..." PARENT_RESP=$(tenant_call POST /workspaces \ -H "Content-Type: application/json" \ - -d "{\"name\":\"E2E Parent\",\"runtime\":\"$RUNTIME\",\"tier\":2,\"model\":\"gpt-4o\"}") + -d "{\"name\":\"E2E Parent\",\"runtime\":\"$RUNTIME\",\"tier\":2,\"model\":\"gpt-4o\",\"secrets\":$SECRETS_JSON}") PARENT_ID=$(echo "$PARENT_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])") log " PARENT_ID=$PARENT_ID" @@ -242,7 +259,7 @@ if [ "$MODE" = "full" ]; then log "6/11 Provisioning child workspace..." CHILD_RESP=$(tenant_call POST /workspaces \ -H "Content-Type: application/json" \ - -d "{\"name\":\"E2E Child\",\"runtime\":\"$RUNTIME\",\"tier\":2,\"model\":\"gpt-4o\",\"parent_id\":\"$PARENT_ID\"}") + -d "{\"name\":\"E2E Child\",\"runtime\":\"$RUNTIME\",\"tier\":2,\"model\":\"gpt-4o\",\"parent_id\":\"$PARENT_ID\",\"secrets\":$SECRETS_JSON}") CHILD_ID=$(echo "$CHILD_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])") log " CHILD_ID=$CHILD_ID" else @@ -359,8 +376,13 @@ print(json.dumps({ })) ") set +e + # Raw curl (not tenant_call) because this call carries an extra + # X-Source-Workspace-Id header. Must still send X-Molecule-Org-Id + # or TenantGuard 404s — previously missing, caused section 10 to + # fail rc=22 despite everything upstream being correct (2026-04-21). DELEG_RESP=$(curl "${CURL_COMMON[@]}" -X POST "$TENANT_URL/workspaces/$CHILD_ID/a2a" \ -H "Authorization: Bearer $EFFECTIVE_TENANT_TOKEN" \ + -H "X-Molecule-Org-Id: $ORG_ID" \ -H "X-Source-Workspace-Id: $PARENT_ID" \ -H "Content-Type: application/json" \ -d "$DELEG_PAYLOAD")