Live — voice calls (Voice AI + Realtime + Inkbox TTS/STT) #2
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Live — voice calls (Inkbox TTS/STT + realtime) | |
| # Boots the agent-under-test (AUT) gateway plus a driver process that bridges the | |
| # other side of a real phone call over its own Inkbox tunnel. Two matrix legs: | |
| # inbound_inkbox — driver calls the agent; agent answers with Inkbox STT/TTS. | |
| # outbound_realtime — driver texts "call me"; the agent calls back powered by the | |
| # OpenAI Realtime API. | |
| # Each leg verifies the stored call transcript shows the agent spoke to the caller. | |
| # Real model + real calls, so this runs only on ready (non-draft) PRs + dispatch, and | |
| # shares the AUT tunnel lock with the other live suites. | |
| on: | |
| pull_request: | |
| branches: [main] | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| workflow_dispatch: | |
| inputs: | |
| timeout_s: | |
| description: "Seconds to wait for the call/transcript" | |
| default: "220" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| # Same group as the other live suites: only one holder of the AUT tunnel at a time. | |
| group: inkbox-live-aut-tunnel | |
| cancel-in-progress: false | |
| jobs: | |
| voice: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| # Skip fork PRs (no secrets) and draft PRs (expensive). Pushes + dispatch always run. | |
| if: >- | |
| (github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft == false)) | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 1 # legs share the AUT identity → one at a time | |
| matrix: | |
| scenario: [inbound_inkbox, outbound_realtime] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: Set up env paths | |
| run: | | |
| echo "CODEX_HOME=$RUNNER_TEMP/codex-home" >> "$GITHUB_ENV" | |
| echo "CODEX_PROJECT_DIR=$RUNNER_TEMP/project" >> "$GITHUB_ENV" | |
| echo "GATEWAY_LOG=$RUNNER_TEMP/gateway.log" >> "$GITHUB_ENV" | |
| echo "DRIVER_LOG=$RUNNER_TEMP/driver.log" >> "$GITHUB_ENV" | |
| echo "DRIVER_STATE=$RUNNER_TEMP/driver_state.json" >> "$GITHUB_ENV" | |
| mkdir -p "$RUNNER_TEMP/codex-home" "$RUNNER_TEMP/project" | |
| # uvicorn[standard] matters: the bare install can't accept WebSocket upgrades, | |
| # and the driver's call-media endpoint is a WebSocket. | |
| - name: Install bridge + test deps | |
| run: pip install -e . pytest fastapi 'uvicorn[standard]' | |
| # @alpha is the prerelease channel cut from codex main near-daily — the | |
| # freshest main build available without compiling the host from source. | |
| - name: Install Codex (freshest main prerelease) | |
| run: | | |
| npm install -g @openai/codex@alpha | |
| codex --version | |
| - name: Configure AUT identity + model + speech path (${{ matrix.scenario }}) | |
| env: | |
| CODEX_INKBOX_API_KEY: ${{ secrets.CODEX_INKBOX_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| run: | | |
| HANDLE="$(python3 - <<'PYEOF' | |
| import os | |
| from inkbox import Inkbox | |
| c = Inkbox(api_key=os.environ["CODEX_INKBOX_API_KEY"], base_url=os.environ.get("INKBOX_BASE_URL", "https://inkbox.ai")) | |
| print(c.mailboxes.list()[0].email_address.split("@", 1)[0]) | |
| PYEOF | |
| )" | |
| echo "AUT handle: $HANDLE" | |
| # The agent's reasoning (deciding to place a call, composing replies) | |
| # uses the chat model; authenticate the codex CLI with the API key. | |
| printenv OPENAI_API_KEY | codex login --with-api-key | |
| { | |
| echo "INKBOX_IDENTITY=$HANDLE" | |
| echo "INKBOX_ALLOW_ALL_USERS=true" | |
| echo "CODEX_MODEL=gpt-5.5" | |
| # Unattended runner: nobody answers approval texts → never escalate, | |
| # and keep the sandbox read-only so stray commands stay harmless. | |
| echo "CODEX_SANDBOX=read-only" | |
| echo "CODEX_APPROVAL_POLICY=never" | |
| } >> "$GITHUB_ENV" | |
| if [ "${{ matrix.scenario }}" = "outbound_realtime" ]; then | |
| # Realtime key falls back to OPENAI_API_KEY in the gateway env. | |
| echo "INKBOX_REALTIME_ENABLED=true" >> "$GITHUB_ENV" | |
| else | |
| echo "INKBOX_REALTIME_ENABLED=false" >> "$GITHUB_ENV" | |
| fi | |
| - name: Start gateway and wait for readiness | |
| env: | |
| INKBOX_API_KEY: ${{ secrets.CODEX_INKBOX_API_KEY }} | |
| INKBOX_SIGNING_KEY: ${{ secrets.CODEX_INKBOX_SIGNING_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| run: | | |
| inkbox-codex run > "$GATEWAY_LOG" 2>&1 & | |
| echo $! > "$RUNNER_TEMP/gateway.pid" | |
| echo "Waiting for the gateway (tunnel + webhooks + call channel)…" | |
| for i in $(seq 1 36); do # up to ~180s | |
| if grep -q "tunnel ready" "$GATEWAY_LOG" && grep -q "\[bridge\] phone" "$GATEWAY_LOG"; then | |
| echo "Gateway ready."; exit 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "::error::gateway did not become ready"; cat "$GATEWAY_LOG"; exit 1 | |
| - name: Start voice driver and wait for its tunnel | |
| env: | |
| REMOTE_INKBOX_API_KEY: ${{ secrets.REMOTE_INKBOX_API_KEY }} | |
| run: | | |
| VOICE_DRIVER_STATE="$DRIVER_STATE" VOICE_DRIVER_PORT=8090 \ | |
| python3 "$GITHUB_WORKSPACE/tests/live/voice_driver.py" > "$DRIVER_LOG" 2>&1 & | |
| echo $! > "$RUNNER_TEMP/driver.pid" | |
| for i in $(seq 1 30); do # up to ~90s | |
| if [ -s "$DRIVER_STATE" ]; then echo "driver ready:"; cat "$DRIVER_STATE"; exit 0; fi | |
| sleep 3 | |
| done | |
| echo "::error::driver did not become ready"; cat "$DRIVER_LOG"; exit 1 | |
| - name: Run voice test (${{ matrix.scenario }}) | |
| env: | |
| REMOTE_INKBOX_API_KEY: ${{ secrets.REMOTE_INKBOX_API_KEY }} | |
| CODEX_INKBOX_API_KEY: ${{ secrets.CODEX_INKBOX_API_KEY }} | |
| VOICE_SCENARIO: ${{ matrix.scenario }} | |
| LIVE_VOICE_TIMEOUT: ${{ github.event.inputs.timeout_s || '220' }} | |
| run: | | |
| LIVE_REAL_MODEL=1 VOICE_DRIVER_STATE="$DRIVER_STATE" \ | |
| python3 -m pytest tests/live/test_voice.py -v | |
| # Failure-only: these logs carry live call content and this repo is public. | |
| - name: Dump logs (on failure only) | |
| if: failure() | |
| run: | | |
| echo "=== gateway.log ==="; cat "$GATEWAY_LOG" || true | |
| echo "=== driver.log ==="; cat "$DRIVER_LOG" || true | |
| - name: Tear down (always) | |
| if: always() | |
| run: | | |
| kill "$(cat "$RUNNER_TEMP/driver.pid" 2>/dev/null)" 2>/dev/null || true | |
| kill "$(cat "$RUNNER_TEMP/gateway.pid" 2>/dev/null)" 2>/dev/null || true | |
| sleep 3 # let the driver revert its number on exit | |
| notify: | |
| needs: [voice] | |
| if: always() && needs.voice.result == 'failure' && github.event_name == 'workflow_run' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Notify Google Chat on scheduled failure | |
| run: | | |
| curl -sS --max-time 10 --retry 3 -X POST "${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}" \ | |
| -H 'Content-Type: application/json' \ | |
| -d '{"text": "⚠️ *FAILED* — Live voice-call suite\n\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' || true |