Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ INKBOX_SIGNING_KEY=whsec_xxxxxxxxxxxx
# INKBOX_WEBHOOK_SECRET_GITHUB=... # verification secret for a registered source
# INKBOX_GATEWAY_PORT=8767

# --- Voice (on by default; Realtime auto-enables when a key is set) ---
# --- Phone call voice stack ---
# INKBOX_VOICE_STACK=inkbox_voice_ai # or openai_realtime / inkbox_tts_stt
# INKBOX_VOICE_AI_AUTHORITY_MODE=contact_scoped # or yolo; local mirror checked by doctor
# INKBOX_VOICEMAIL_DETECTION=enabled # set disabled for calls that must ignore detection
# OPENAI_API_KEY=sk-openai # enables OpenAI Realtime calls
# INKBOX_REALTIME_API_KEY=sk-realtime # dedicated key; OPENAI_API_KEY backstops
# INKBOX_VOICE_ENABLED=false # stop answering calls entirely
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: a4dd76b534e33c0a8352148d8f0e9ab25199f05b
ref: 73f18a2b8c0e9dc6887c5663e6e904d54869927e
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand All @@ -40,7 +40,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: a4dd76b534e33c0a8352148d8f0e9ab25199f05b
ref: 73f18a2b8c0e9dc6887c5663e6e904d54869927e
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/live-a2a.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ jobs:
python-version: "3.12"

- name: Install protocol driver
run: pip install 'inkbox==0.5.8'
run: pip install 'inkbox==0.5.9'

- name: Install plugin and host
run: |
npm ci
npm install --no-save --package-lock=false \
@inkbox/sdk@0.5.8 \
@inkbox/sdk@0.5.9 \
@opencode-ai/sdk@1.17.18 @opencode-ai/plugin@1.17.18
npm install -g opencode-ai@latest

Expand All @@ -75,6 +75,7 @@ jobs:
AUT_INKBOX_SIGNING_KEY: ${{ secrets.AUT_INKBOX_SIGNING_KEY }}
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL || 'https://inkbox.ai' }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
INKBOX_VOICEMAIL_DETECTION: "disabled"
run: bash scripts/live-aut.sh

- name: Run ${{ matrix.scenario }}
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/live-channels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ concurrency:
jobs:
live:
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
max-parallel: 1 # legs share the AUT identity → one at a time
Expand All @@ -51,7 +52,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: a4dd76b534e33c0a8352148d8f0e9ab25199f05b
ref: 73f18a2b8c0e9dc6887c5663e6e904d54869927e
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand All @@ -72,7 +73,8 @@ jobs:
nohup node tests/live/mock-openai.mjs 8088 > "$RUNNER_TEMP/mock.log" 2>&1 &
echo $! > "$RUNNER_TEMP/mock.pid"
for i in $(seq 1 10); do
curl -sf http://127.0.0.1:8088/v1/models >/dev/null && { echo "mock model ready"; exit 0; }
curl -sf --connect-timeout 1 --max-time 3 \
http://127.0.0.1:8088/v1/models >/dev/null && { echo "mock model ready"; exit 0; }
sleep 1
done
echo "::error::mock model did not start"; cat "$RUNNER_TEMP/mock.log"; exit 1
Expand All @@ -85,6 +87,7 @@ jobs:
AUT_INKBOX_SIGNING_KEY: ${{ secrets.AUT_INKBOX_SIGNING_KEY }}
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
INKBOX_VOICEMAIL_DETECTION: "disabled"
run: bash scripts/live-aut.sh

- name: Run live channel tests (${{ matrix.mode }})
Expand All @@ -101,7 +104,7 @@ jobs:
# Failure-only: these logs carry live phone/email content and this
# repo's Action logs are public.
- name: Dump logs (on failure only)
if: failure()
if: failure() || cancelled()
run: |
echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true
echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true
Expand All @@ -115,7 +118,7 @@ jobs:
kill "$(cat "$RUNNER_TEMP/mock.pid" 2>/dev/null)" 2>/dev/null || true

- name: Upload artifacts (on failure only)
if: failure()
if: failure() || cancelled()
uses: actions/upload-artifact@v7
with:
name: live-logs-${{ matrix.mode }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/live-external-events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: a4dd76b534e33c0a8352148d8f0e9ab25199f05b
ref: 73f18a2b8c0e9dc6887c5663e6e904d54869927e
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand All @@ -65,6 +65,7 @@ jobs:
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
INKBOX_WEBHOOK_SECRET_GITHUB: ${{ secrets.LIVE_GITHUB_WEBHOOK_SECRET }}
INKBOX_VOICEMAIL_DETECTION: "disabled"
run: bash scripts/live-aut.sh

- name: Run external-event test
Expand Down
26 changes: 18 additions & 8 deletions .github/workflows/live-voice.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
name: Live — voice calls (Inkbox STT/TTS + Realtime)
name: Live — voice calls (Voice AI + Realtime + Inkbox TTS/STT)

# Boots the AUT gateway plus a driver process that bridges the other side of a
# real phone call over its own Inkbox tunnel. Two matrix legs:
# real phone call over its own Inkbox tunnel. Three available matrix legs:
# inbound_inkbox — driver calls the agent; agent answers Inkbox STT/TTS.
# outbound_realtime — driver texts "call me"; agent calls back on Realtime.
# outbound_hosted — driver requests a Voice AI call and one exact post-call SMS.
# Each leg verifies the stored call transcript shows the agent spoke to the
# caller. Real model + real calls — the priciest suite — so it runs only on
# ready (non-draft) PRs + manual dispatch, and shares the AUT tunnel lock.
Expand Down Expand Up @@ -47,18 +48,19 @@ concurrency:
jobs:
voice:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
max-parallel: 1 # legs share the AUT + driver identities → one at a time
matrix:
scenario: ${{ fromJSON(inputs.include_inbound && '["inbound_inkbox","outbound_realtime"]' || '["outbound_realtime"]') }}
scenario: ${{ fromJSON(inputs.include_inbound && '["inbound_inkbox","outbound_realtime","outbound_hosted"]' || '["outbound_realtime","outbound_hosted"]') }}

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: a4dd76b534e33c0a8352148d8f0e9ab25199f05b
ref: 73f18a2b8c0e9dc6887c5663e6e904d54869927e
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand All @@ -82,8 +84,9 @@ jobs:
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
INKBOX_VOICE_ENABLED: "true"
# Inbound leg proves the Inkbox STT/TTS path; the others prove Realtime.
INKBOX_REALTIME_ENABLED: ${{ matrix.scenario == 'inbound_inkbox' && 'false' || 'true' }}
INKBOX_VOICE_STACK: ${{ matrix.scenario == 'outbound_hosted' && 'inkbox_voice_ai' || (matrix.scenario == 'outbound_realtime' && 'openai_realtime' || 'inkbox_tts_stt') }}
INKBOX_VOICEMAIL_DETECTION: "disabled"
INKBOX_REALTIME_ENABLED: ${{ matrix.scenario == 'outbound_realtime' && 'true' || 'false' }}
INKBOX_REALTIME_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: bash scripts/live-aut.sh

Expand All @@ -93,6 +96,13 @@ jobs:
INKBOX_BASE_URL: ${{ vars.INKBOX_BASE_URL }}
VOICE_DRIVER_STATE: ${{ runner.temp }}/voice_driver_state.json
run: |
if [ "${{ matrix.scenario }}" = "outbound_hosted" ]; then
HOSTED_MARKER="$(node scripts/nato-marker.mjs "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT")"
echo "HOSTED_POST_CALL_MARKER=$HOSTED_MARKER" >> "$GITHUB_ENV"
export VOICE_DRIVER_LINE="After we hang up, send me one SMS. Create the post-call action now with this exact SMS body: $HOSTED_MARKER. Read those five words back to me after the action is saved. Do not send it during the call."
export VOICE_DRIVER_LISTEN=180
export VOICE_DRIVER_AUTO_STOP=false
fi
nohup node tests/live/voice-driver.mjs > "$RUNNER_TEMP/driver.log" 2>&1 &
echo $! > "$RUNNER_TEMP/driver.pid"
for _ in $(seq 1 30); do # up to ~90s
Expand All @@ -115,7 +125,7 @@ jobs:

# Failure-only: these logs carry live call content and this repo is public.
- name: Dump logs (on failure only)
if: failure()
if: failure() || cancelled()
run: |
echo "=== gateway.log ==="; cat "$AUT_GATEWAY_LOG" || true
echo "=== serve.log ==="; tail -n 100 "$AUT_SERVE_LOG" || true
Expand All @@ -130,7 +140,7 @@ jobs:
sleep 3 # let the driver revert its number on exit

- name: Upload artifacts (on failure only)
if: failure()
if: failure() || cancelled()
uses: actions/upload-artifact@v7
with:
name: voice-logs-${{ matrix.scenario }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: a4dd76b534e33c0a8352148d8f0e9ab25199f05b
ref: 73f18a2b8c0e9dc6887c5663e6e904d54869927e
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand All @@ -39,7 +39,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: a4dd76b534e33c0a8352148d8f0e9ab25199f05b
ref: 73f18a2b8c0e9dc6887c5663e6e904d54869927e
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand All @@ -65,7 +65,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: a4dd76b534e33c0a8352148d8f0e9ab25199f05b
ref: 73f18a2b8c0e9dc6887c5663e6e904d54869927e
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand Down
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.2.8 (unreleased)

- Adds explicit Inkbox Voice AI, OpenAI Realtime API, and Inkbox TTS/STT phone-call stacks to setup.
- Routes hosted calls through Voice AI and reconciles `call.ended` commitments in the OpenCode session.
- Guards hosted post-call SMS with an exact-target durable journal and a bounded correction policy.
- Uses `@inkbox/sdk` 0.5.9 and disables voicemail detection throughout live call CI.

## 0.2.7 (unreleased)

- Adds safely framed matched-contact memories to inbound email, SMS, iMessage,
Expand Down Expand Up @@ -31,7 +38,7 @@
existing approval and recipient-allowlist controls.
- Adds paginated task and message history with direction, participant,
lifecycle, context, role, keyword, and timestamp filters.
- The plugin requires `@inkbox/sdk` 0.5.8 or newer.
- The plugin uses exactly `@inkbox/sdk` 0.5.9.

## 0.1.0 (unreleased)

Expand Down
27 changes: 18 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ That's the whole setup. The clone lives in `~/.inkbox-opencode/app`; the
plugin is installed into your **global opencode config** (`~/.config/opencode`)
with a one-file wrapper. The wizard creates a fresh Inkbox agent via
self-signup (or takes an existing API key), enables iMessage, provisions a
dedicated phone number, waits for your START opt-in, validates an OpenAI key
for Realtime voice, mints the webhook signing key, picks the agent's working
dedicated phone number, waits for your START opt-in, configures the phone call
voice stack, mints the webhook signing key, picks the agent's working
directory, and offers to **keep the gateway running on every boot**. When it
finishes, text, email, or call your agent and it answers.

Expand Down Expand Up @@ -223,10 +223,14 @@ Outbound calls can originate from either line, chosen with `origination` on
iMessage; the underlying number is never surfaced.

When `origination` is omitted the plugin uses whichever line exists, and
prefers the dedicated number when both do. Note that `inkbox_place_call`
currently requires an audio bridge: pass `clientWebsocketUrl` per call or set
the `callWebsocketUrl` option (env `INKBOX_CALL_WEBSOCKET_URL`) — Inkbox
connects to that WebSocket for the call's media.
prefers the dedicated number when both do. OpenAI Realtime and Inkbox TTS/STT
calls require an audio bridge: pass `clientWebsocketUrl` per call or set the
`callWebsocketUrl` option (env `INKBOX_CALL_WEBSOCKET_URL`). Inkbox Voice AI
handles media itself and receives the call's required `purpose` as its task brief.
Hosted outbound calls do not send a per-call authority override: Inkbox applies
the saved Voice AI default. The wizard changes that server-side default only
with an admin credential and records `INKBOX_VOICE_AI_AUTHORITY_MODE` as a local
mirror so `doctor` can report configuration drift.

## Configuration reference

Expand All @@ -236,7 +240,10 @@ connects to that WebSocket for the call's media.
| `identity` | `INKBOX_IDENTITY` (also `INKBOX_AGENT_IDENTITY`, `INKBOX_AGENT_HANDLE`) | Agent handle (required) |
| `baseUrl` | `INKBOX_BASE_URL` | API base URL override |
| `signingKey` | `INKBOX_SIGNING_KEY` | Webhook signature key (future inbound use) |
| `callWebsocketUrl` | `INKBOX_CALL_WEBSOCKET_URL` | Audio-bridge WebSocket for `inkbox_place_call` |
| `callWebsocketUrl` | `INKBOX_CALL_WEBSOCKET_URL` | Audio bridge used by OpenAI Realtime and Inkbox TTS/STT calls |
| `phoneVoiceStack` | `INKBOX_VOICE_STACK` | `inkbox_voice_ai`, `openai_realtime`, or `inkbox_tts_stt` |
| `voiceAiAuthorityMode` | `INKBOX_VOICE_AI_AUTHORITY_MODE` | Local mirror of saved Voice AI authority: `contact_scoped` or `yolo` |
| `voicemailDetection` | `INKBOX_VOICEMAIL_DETECTION` | Optional explicit `enabled` / `disabled`; omission uses the Inkbox API default |
| `vault.keyEnvVar` | — (default `INKBOX_VAULT_KEY`) | Which env var holds the vault unlock key |
| `tools.enable` / `tools.disable` | — | Tool gating (names, groups, `"all"`) |
| `outbound.approval` | — | `"ask"` (default) / `"allowlist"` / `"auto"` |
Expand Down Expand Up @@ -312,8 +319,10 @@ inbound events. What it does:
decline") and time out to a decline.
- **Control commands** (whole-message): `/clear`, `/stop`, `/status`,
`/health`, `/resume`, `/usage`.
- **Voice** (on by default with the gateway): the agent answers calls. Realtime
auto-enables when an OpenAI key is present (`INKBOX_REALTIME_API_KEY`, or
- **Voice** (on by default with the gateway): setup offers Inkbox Voice AI,
OpenAI Realtime API, and Inkbox TTS/STT. Voice AI handles the live call and
notifies OpenCode after it ends; the two local stacks keep the call attached
to the OpenCode gateway. Realtime uses `INKBOX_REALTIME_API_KEY` (or
`OPENAI_API_KEY` as the backstop) and runs the call as a live raw-audio
conversation with in-call actions; otherwise Inkbox handles speech-to-text
and text-to-speech. Opt out with `INKBOX_VOICE_ENABLED=false` (stop answering)
Expand Down
75 changes: 75 additions & 0 deletions docs/live-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Live CI

These Actions exercise the installed plugin against live Inkbox identities. Each component Action supports reusable and manual execution; credential-gated tests skip outside configured live jobs. Tests use current-run markers or pre-request snapshots so stale records cannot pass.

## Full stack e2e

Runs the reusable Actions in sequence for ready same-repository pull requests, manual dispatches, and successful canary runs on `main`. The orchestrated voice call includes the conditional inbound scenario.

### `full-stack`

**Proves:** Every live suite passes as one required gate. **Flow:** 1. Run channels. 2. Run Agent2Agent. 3. Run voice. 4. Run external events. 5. Fail unless every suite succeeded.

## Live — Agent2Agent

Runs all four scenarios serially with both live identity credentials.

### `inbound-single`

**Proves:** The plugin completes one inbound A2A task. **Flow:** 1. Send a tagged task. 2. Wait for completion. 3. Require the tag in task history.

### `inbound-multi`

**Proves:** An inbound task can request and consume follow-up input. **Flow:** 1. Send a tagged task. 2. Wait for `input-required`. 3. Reply in the same task. 4. Require both tags at completion.

### `outbound-single`

**Proves:** The agent delegates work without completing its outer task early. **Flow:** 1. Request delegation. 2. Find the tagged worker task. 3. Complete it remotely. 4. Require its result in the outer completion.

### `outbound-multi`

**Proves:** Delegation preserves a worker's input round trip. **Flow:** 1. Start a delegated task. 2. Receive its input request. 3. Reply through the agent. 4. Complete the worker. 5. Require its result in the outer task.

## Live — agent channels (email + SMS)

The `mock` matrix leg runs only the deterministic tests; the `real` leg runs only the real-model tests. Both require live identity credentials.

### `email — mock model: the nonce travels inbound → model → reply → delivery`

**Proves:** The complete email transport works deterministically. **Flow:** 1. Snapshot inbound email IDs. 2. Send a unique nonce. 3. Wait for a fresh reply. 4. Require the nonce and mock marker.

### `email — real model: replies with actual content`

**Proves:** The real agent can answer over email. **Flow:** 1. Snapshot inbound email IDs. 2. Request a fixed acknowledgement. 3. Wait for a fresh reply. 4. Reject error text and require the acknowledgement.

### `SMS — mock model: the nonce travels inbound → model → reply → delivery`

**Proves:** The complete SMS transport works deterministically. **Flow:** 1. Send a unique nonce. 2. Wait for a fresh inbound reply. 3. Require the nonce and mock marker.

### `SMS — real model: reports its own identity when asked`

**Proves:** The real agent receives context and answers over SMS. **Flow:** 1. Read the agent mailbox. 2. Ask for that address by SMS. 3. Wait for a fresh reply. 4. Require the exact address.

## Live — voice calls (Voice AI + Realtime + Inkbox TTS/STT)

Requires both live identity credentials and a real model. Outbound Realtime and Voice AI always run; inbound TTS/STT runs only when `include_inbound` is true.

### `inbound: driver calls, agent answers via Inkbox TTS/STT and replies`

**Proves:** Inbound client-media calling uses Inkbox speech services. **Flow:** 1. Snapshot agent calls. 2. Place a call with voicemail detection disabled. 3. Require two-way speech. 4. Verify the persisted call policy and speech mode. 5. Hang up.

### `outbound: 'call me' text → agent calls back on the Realtime path and replies`

**Proves:** A message-triggered callback uses Realtime. **Flow:** 1. Snapshot both owners' calls. 2. Text the request. 3. Require exactly one fresh paired call after duplicate grace. 4. Require two-way speech, Realtime flags, and disabled voicemail detection. 5. Hang up.

### `outbound: Voice AI call settles one exact-target post-call SMS`

**Proves:** Hosted calling completes one durable post-call action. **Flow:** 1. Snapshot both call owners and sender-side SMS rows. 2. Request a hosted call. 3. Require one fresh pair, reason, saved authority, and disabled voicemail detection. 4. Before hangup, require caller intent and a matching open action. 5. Hang up. 6. Require completed reconciliation and exactly one current-marker SMS to the caller after duplicate grace.

## Live — external events (webhook → agent acts)

Runs only with live identity credentials, a real model, the webhook signing secret, and the gateway log used to correlate the exact turn.

### `rejects forged GitHub hooks and completes a valid real-model turn`

**Proves:** External events are authenticated before agent execution. **Flow:** 1. Send an invalidly signed event and require rejection with no turn. 2. Send a valid event. 3. Require acceptance. 4. Wait for the exact request's completed agent turn.
Loading