Skip to content

Live — external events (escalation → agent calls driver) #3

Live — external events (escalation → agent calls driver)

Live — external events (escalation → agent calls driver) #3

name: Live — external events (escalation → agent calls driver)
# Boots the agent-under-test (AUT) gateway, then POSTs a signed external
# escalation webhook (a CI-escalation demo shape) at its local webhook listener
# asking it to phone the driver contact. The test verifies the agent actually
# places that call. The driver sits on auto_reject (set by the test) — we
# monitor the escalation, not the call itself.
# Real model + real call, 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, standardization]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
inputs:
timeout_s:
description: "Seconds to wait for the agent to place the call"
default: "200"
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:
external-events:
runs-on: ubuntu-latest
timeout-minutes: 45
# Skip fork PRs (no secrets) and draft PRs (expensive). Dispatch always runs.
if: >-
(github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft == false))
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"
mkdir -p "$RUNNER_TEMP/codex-home" "$RUNNER_TEMP/project"
- name: Install bridge + test deps
run: pip install -e . pytest
# @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
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"
# Per-run GitHub webhook secret: shared by the gateway (to verify) and
# the test (to sign). Generated fresh so nothing is committed.
GH_SECRET="$(openssl rand -hex 24)"
{
echo "INKBOX_IDENTITY=$HANDLE"
# NB: no INKBOX_ALLOW_ALL_USERS here on purpose — external events
# are routed on their own external: sessions and must bypass user
# auth on their own. Setting allow-all would mask a regression in
# that bypass.
# The whole point of this suite — let external webhooks reach the agent.
echo "INKBOX_EXTERNAL_EVENTS_ENABLED=true"
# Secret the github WebhookProvider verifies X-Hub-Signature-256 against.
echo "INKBOX_WEBHOOK_SECRET_GITHUB=$GH_SECRET"
# No realtime needed — the driver auto-rejects, so no media leg runs.
echo "INKBOX_REALTIME_ENABLED=false"
# Unattended runner: nobody is on the other end to answer an approval
# text, so never escalate — and keep the sandbox read-only so a stray
# command the model dreams up stays harmless.
echo "CODEX_SANDBOX=read-only"
echo "CODEX_APPROVAL_POLICY=never"
# MCP tool confirmations are opt-in here: without this flag the
# gateway escalates each Inkbox tool prompt as a poll nobody answers.
echo "INKBOX_CODEX_AUTO_APPROVE_INKBOX_TOOLS=true"
} >> "$GITHUB_ENV"
# Real OpenAI via the default provider — authenticate the codex CLI
# with the API key (writes auth.json under CODEX_HOME).
printenv OPENAI_API_KEY | codex login --with-api-key
echo "CODEX_MODEL=gpt-5.5" >> "$GITHUB_ENV"
- 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 to be ready (tunnel + webhooks)…"
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: Run external-event tests
env:
REMOTE_INKBOX_API_KEY: ${{ secrets.REMOTE_INKBOX_API_KEY }}
CODEX_INKBOX_API_KEY: ${{ secrets.CODEX_INKBOX_API_KEY }}
CODEX_INKBOX_SIGNING_KEY: ${{ secrets.CODEX_INKBOX_SIGNING_KEY }}
LIVE_EXTERNAL_TIMEOUT: ${{ github.event.inputs.timeout_s || '200' }}
run: |
# Inkbox-signed escalation + GitHub-signed (valid & forged) escalation.
LIVE_REAL_MODEL=1 AUT_WEBHOOK_URL=http://127.0.0.1:8767/webhook \
python3 -m pytest \
tests/live/test_external_event_intelligence.py \
tests/live/test_external_event_github.py -v
# Failure-only: these logs carry live agent content and this repo
# (and its Action logs/artifacts) is public.
- name: Dump logs (on failure only)
if: failure()
run: |
echo "=== gateway.log ==="; cat "$GATEWAY_LOG" || true
- name: Tear down (always)
if: always()
run: |
kill "$(cat "$RUNNER_TEMP/gateway.pid" 2>/dev/null)" 2>/dev/null || true
sleep 3
- name: Upload artifacts (on failure only)
if: failure()
uses: actions/upload-artifact@v4
with:
name: live-external-events-logs
retention-days: 5
path: ${{ runner.temp }}/gateway.log
if-no-files-found: ignore