Skip to content

Shared iMessage-line calling, identity-scoped call config, external webhook injection (SDK 0.4.15 parity)#13

Open
dimavrem22 wants to merge 4 commits into
standardizationfrom
imessage-shared-calling-parity
Open

Shared iMessage-line calling, identity-scoped call config, external webhook injection (SDK 0.4.15 parity)#13
dimavrem22 wants to merge 4 commits into
standardizationfrom
imessage-shared-calling-parity

Conversation

@dimavrem22

@dimavrem22 dimavrem22 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Brings this plugin to parity with hermes-agent-plugin main (its #35 shared-line calling + #31 external events), completing the 0.4.15 adoption that #11 only pin-bumped.

Stacked on standardization (#8) — that PR rewrites gateway/realtime, so this builds on it rather than conflicting; GitHub retargets to main when #8 merges.

What's in here

  • SDK pin: inkbox>=0.4.15,<1.0.0 (restores the breaking-major ceiling).
  • Identity-scoped inbound-call config: identity.set_incoming_call_action(...) replaces the removed number-scoped phone_numbers.update call-config write, gated on (dedicated number OR iMessage enabled) with a guarded legacy fallback; WS-URL read prefers get_incoming_call_action().
  • Shared iMessage-line calling: origination (dedicated_number | shared_imessage_number) on inkbox_place_call with channel-aware resolution — explicit wins → single-line auto → both lines follow the current conversation's channel → unknown defaults to dedicated; legible message on no_shared_connection. Two-lines terminology in whoami (lines block), realtime instructions, prompts, docs. The shared line's number is never surfaced.
  • Wizard: iMessage step before the dedicated-number offer (copy mentions voice calls over the shared line), standalone number step with "Already provisioned" and a paid-tier fallback pointing at https://inkbox.ai/pricing, realtime offered when the identity has a number OR iMessage.
  • External webhook injection: classify-before-auth routing with provider registry (GitHub HMAC + Inkbox), third-party opt-in bypass, default-off passthrough waking the agent on unknown webhook types — integrated with this branch's webhook dedup.
  • Version 0.1.0 → 0.1.1.

Tests

With real inkbox SDK 0.4.15: 235 passed, 0 skipped. CI-like venv: 234 passed, 1 skipped. All standardization-branch tests stay green. New suites: webhook providers (530 lines), origination matrix, wizard flow.

Note: pre-existing fixtures on the base branch use a real-looking number (+16614031457) — flagged for a separate cleanup, not touched here.

CI parity

Second commit adopts the full CI stack from main onto this branch (the standardization base predates it): tests/canary/live-channels/live-voice workflows + tests/live + tests/contract, with pull_request branch filters widened to [main, standardization] so they fire on this stacked PR; the old ci.yml is dropped (superseded by tests.yml's identical unit lane). Also adds live-external-events.yml + its live tests — the live proof of the webhook-injection feature.

…webhook injection from the hermes-agent-plugin reference

Brings this bridge to parity with the hermes-agent-plugin reference
implementation (its tools.py / adapter.py / setup_wizard.py / realtime.py
on main) for the shared iMessage line and external event injection.

Two calling lines (dedicated number vs shared iMessage line):
- tools.py: inkbox_place_call gains an `origination` argument
  (dedicated_number / shared_imessage_number) with channel-aware
  auto-resolution — explicit choice wins; a single enabled line is used
  as-is; when BOTH lines exist the call follows the CURRENT
  conversation's channel (iMessage turn -> shared line, SMS/phone turn
  -> dedicated number; unknown -> dedicated); neither line -> clear
  error telling the agent to provision a number or enable iMessage.
  A shared-line call rejected with no_shared_connection returns a
  legible message (connect over iMessage first, or fall back to the
  dedicated number). The resolved origination is echoed in the result,
  with a TypeError retry for SDKs that predate the kwarg.
- Channel source: sessions already track the last inbound modality
  (ContactSession.mode); handle_inbound now mirrors it into
  ~/.inkbox-codex/channel_hints.json and each session stamps
  INKBOX_CODEX_CHAT_ID into its MCP tool-server env, so the stdio tool
  process resolves the current conversation's channel at call time.
- inkbox_whoami now returns a "lines" block labelling the dedicated
  phone line vs the shared iMessage line (whose number is managed by
  Inkbox and never surfaced), with per-line origination notes.
- Outbound call WS URL resolution prefers the identity-scoped
  incoming-call config row, then the legacy number-scoped field, then
  the tunnel host — so an iMessage-only identity can place calls.

Identity-scoped inbound-call config (gateway):
- _patch_identity_objects now registers the incoming-call action via
  identity.set_incoming_call_action (one row covers the dedicated
  number AND the shared iMessage line), gated on having a number OR
  imessage_enabled; the number-scoped phone_numbers.update remains only
  as a fallback when the SDK lacks the method (and is skipped for
  iMessage-only identities it cannot express).
- _handle_call_ws backfills remote number + direction through an
  identity-centered calls.get(call_id) round-trip when the upgrade
  carries no caller metadata (shared-line calls have no owning number).

Realtime instructions:
- RealtimeCallMeta.agent_imessage_enabled threads the identity's
  iMessage state into the instruction builder, which now names the
  dedicated line explicitly and describes the shared iMessage line
  without ever stating a number for it; calls follow the conversation's
  channel.

Setup wizard channel flow:
- iMessage step now runs FIRST (intro copy mentions voice calls over
  the same shared line) and returns bool(enabled).
- Dedicated-number provisioning is a STANDALONE step decoupled from
  identity creation/signup/API-key paths: prints "Already provisioned:
  <number>" when one exists, and on provisioning failure points at
  Inkbox paid tiers (https://inkbox.ai/pricing) plus the raw error and
  moves on.
- Realtime is offered when the identity has a number OR iMessage
  enabled (flag threaded explicitly since the local identity object may
  be stale).

External webhook injection (new webhook_providers/ package):
- Provider registry with drop-in modules (inkbox.py delegating to the
  SDK's verify_webhook, github.py verifying X-Hub-Signature-256);
  classify-before-auth in _handle_webhook: the source is identified by
  its signature header, verified with that source's secret
  (INKBOX_WEBHOOK_SECRET_<NAME> for third parties), and routing keys
  off the verified source — never the body's claimed event_type.
- Unknown event types wake the agent on a per-source external: session
  with an act vs do-not-act directive (verified vs unverified), bounded
  payload surfacing, and request-id dedup; default-off via
  INKBOX_EXTERNAL_EVENTS_ENABLED, with registered third-party providers
  bypassing the flag. External-thread replies are never delivered.

Also:
- inkbox SDK pin gets a floor AND ceiling (>=0.4.15,<1.0.0) in
  pyproject, the wizard install requirements, and the doctor/gateway
  hints.
- README: "Two calling lines" + "External events" sections, config
  reference rows, and tool-list updates; channel prompt gains a
  per-channel "Calling someone" section; .env.example documents the
  new vars.
- Version bump 0.1.0 -> 0.1.1 (pyproject + plugin manifest).
- Tests: origination resolution matrix (incl. channel-follow and
  explicit-wins), place-call handler paths, whoami lines block,
  identity-scoped incoming-call-action assertions (with legacy
  fallback), webhook-provider registry/classify/passthrough/dedup
  suite, wizard order + paid-tier fallback + iMessage-returns-bool +
  realtime-for-either-line, realtime two-lines instructions, session
  channel-hint + tool-env stamping, and call-record backfill.
  Full suite: 235 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dimavrem22 dimavrem22 marked this pull request as ready for review July 3, 2026 07:30
dimavrem22 and others added 3 commits July 3, 2026 07:54
Brings .github/workflows/{tests,canary,live-channels,live-voice}.yml plus
tests/live and tests/contract over from main (this branch predates the CI
stack; the old ci.yml is superseded by tests.yml's unit lane). Every
brought-over pull_request branches filter is widened to
[main, standardization] so the suites fire on PRs targeting this branch.

New live suite: live-external-events.yml boots the AUT gateway with
INKBOX_EXTERNAL_EVENTS_ENABLED=true and a per-run INKBOX_WEBHOOK_SECRET_GITHUB
(generated in the workflow, never committed), then runs two new tests:

- tests/live/test_external_event_intelligence.py — an Inkbox-signed CI
  escalation POSTed at the gateway's local /webhook; asserts the real model
  reasons "escalation -> call this contact" and actually dials the driver
  (polled via calls.list; driver parked on auto_reject).
- tests/live/test_external_event_github.py — the same escalation signed the
  GitHub way (X-Hub-Signature-256): a forged signature is 401'd and produces
  no call, a valid one wakes the agent and it phones the driver.

Same tunnel-lock concurrency group and ready-PR gating as the other live
suites; secrets reuse the existing CODEX_INKBOX_* / REMOTE_INKBOX_API_KEY /
OPENAI_API_KEY set. Contract suite verified locally against a real codex
binary (5 passed); full unit run 240 passed / 20 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live suites adopted from main assume the gateway auto-accepts Codex's
'allow the inkbox mcp server to run tool ...' elicitations, which main does
unconditionally. This branch gates that behind
INKBOX_CODEX_AUTO_APPROVE_INKBOX_TOOLS (default off), so every Inkbox tool
prompt was escalated as a poll no one answers: cross-channel sends, whoami,
and place_call all stalled until the test timeouts, and the pending poll
swallowed the next inbound message (the identity-test off-by-one). Set the
flag in all three live workflows' gateway env, next to
CODEX_APPROVAL_POLICY=never, for the same unattended-runner reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le name

Run 28649399588: the Inkbox-signed escalation passed but the GitHub one
timed out — the gateway log shows the turn WAS injected and the agent
auto-approved inkbox_list_contacts three times without ever reaching
inkbox_place_call. The suite hardcoded "Jane Doe" but only seeded that
contact when the AUT org had NO card for the driver number; ours already
carries one under a different name, so the agent was asked to call a
contact it could not resolve and (correctly) never dialed.

The envelope now addresses the driver by the name on the existing card,
seeding Jane Doe only when absent — the same resolution the Inkbox-signed
suite already uses, which is exactly why it passed in the same run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant