Skip to content
Open
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 .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ jobs:
node-version: "22"

- name: Install bridge + test deps
run: pip install -e . pytest
run: |
pip install \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest

# @alpha is the prerelease channel cut from codex main near-daily — the
# freshest main build available without compiling the host from source.
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/live-channels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ jobs:
mkdir -p "$RUNNER_TEMP/codex-home" "$RUNNER_TEMP/project"

- name: Install bridge + test deps
run: pip install -e . pytest
run: |
pip install \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest

# @alpha is the prerelease channel cut from codex main near-daily — the
# freshest main build available without compiling the host from source.
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/live-external-events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ jobs:
mkdir -p "$RUNNER_TEMP/codex-home" "$RUNNER_TEMP/project"

- name: Install bridge + test deps
run: pip install -e . pytest
run: |
pip install \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest

# @alpha is the prerelease channel cut from codex main near-daily — the
# freshest main build available without compiling the host from source.
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/live-voice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ jobs:
# 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]'
run: |
pip install \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-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.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
- name: Install bridge + test deps
run: |
uv pip install --system \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@199bbd27c8dab2f70e379de52ca9cc910b0e141d#subdirectory=sdk/python" \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest

# @alpha is the prerelease channel cut from codex main near-daily — the
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ The agent reaches you (or third parties) through an in-process MCP server:
- `inkbox_list_imessage_conversations` · `inkbox_get_imessage_conversation` — browse iMessage threads and history (find the `conversation_id` to send into).
- `inkbox_lookup_contact` · `inkbox_list_contacts` · `inkbox_get_contact` — resolve and read address-book contacts (reverse-lookup by email/phone, free-text search, or full record by id).
- `inkbox_create_contact` · `inkbox_update_contact` · `inkbox_delete_contact` — save, edit, and remove organization-wide contacts. Changes affect the shared address book. vCard export/import is not exposed.
- `inkbox_a2a_call` · `inkbox_a2a_check` · `inkbox_a2a_reply` — delegate work to another agent and follow its task.
- `inkbox_list_a2a_tasks` · `inkbox_list_a2a_messages` — page and search this identity's inbound and outbound A2A history, with participant, task, context, role, state, and timestamp filters.
- `inkbox_a2a_complete` · `inkbox_a2a_ask_caller` · `inkbox_a2a_fail` — commit the outcome of a verified inbound A2A task. These tools are rejected outside that task's isolated session.

The bridge requires Inkbox SDK 0.5.6 or newer.

On a live call, the OpenAI Realtime voice agent additionally gets `consult_agent`, `register_post_call_action` / `edit_post_call_action` / `delete_post_call_action`, and `hang_up_call` — see [Voice](#voice).

Expand Down
106 changes: 106 additions & 0 deletions inkbox_codex/a2a_delegations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Durable caller-side A2A delegation routing records."""

from __future__ import annotations

import json
import os
import threading
import time
from pathlib import Path
from typing import Any, Dict, Optional
from urllib.parse import urlsplit

_LOCK = threading.Lock()


def _path() -> Path:
root = Path(
os.getenv("INKBOX_CODEX_HOME")
or (Path.home() / ".inkbox-codex")
)
return root / "a2a_delegations.json"


def _origin(url: str) -> str:
parsed = urlsplit(url)
return f"{parsed.scheme.lower()}://{parsed.netloc.lower()}"


def _read() -> Dict[str, Dict[str, Any]]:
try:
loaded = json.loads(_path().read_text())
return loaded if isinstance(loaded, dict) else {}
except FileNotFoundError:
return {}


def _write(records: Dict[str, Dict[str, Any]]) -> None:
path = _path()
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(records, indent=2, sort_keys=True) + "\n")
tmp.chmod(0o600)
os.replace(tmp, path)
path.chmod(0o600)


def record_before_send(
*,
identity_id: str,
rpc_url: str,
card_url: str,
message_id: str,
session_key: Optional[str],
context_id: Optional[str] = None,
task_id: Optional[str] = None,
) -> str:
"""Persist retry and routing data before the SendMessage request."""
origin = _origin(rpc_url)
context_key = context_id or f"pending:{message_id}"
key = f"{identity_id}|{origin}|{context_key}"
with _LOCK:
records = _read()
records[key] = {
"identity_id": identity_id,
"origin": origin,
"card_url": card_url,
"context_id": context_id,
"task_id": task_id,
"message_id": message_id,
"session_key": session_key,
"updated_at": time.time(),
}
_write(records)
return key


def promote_after_send(
pending_key: str,
*,
context_id: str,
task_id: str,
) -> None:
"""Promote a provisional record to its canonical context key."""
with _LOCK:
records = _read()
record = records.pop(pending_key, None)
if record is None:
return
record["context_id"] = context_id
record["task_id"] = task_id
record["updated_at"] = time.time()
key = (
f"{record['identity_id']}|{record['origin']}|{context_id}"
)
records[key] = record
_write(records)


def find_by_task(task_id: str) -> Optional[Dict[str, Any]]:
"""Return the newest local delegation record for a remote task."""
matches = [
record
for record in _read().values()
if str(record.get("task_id") or "") == task_id
]
return max(matches, key=lambda item: item.get("updated_at", 0), default=None)
10 changes: 10 additions & 0 deletions inkbox_codex/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import hashlib
import os
from dataclasses import dataclass, field
from pathlib import Path
Expand Down Expand Up @@ -51,6 +52,15 @@ def channel_hints_path() -> Path:
return root / "channel_hints.json"


def a2a_turn_context_path(chat_id: str) -> Path:
"""Return the trusted cross-process context file for one Codex session."""
root = Path(os.getenv("INKBOX_CODEX_HOME") or (Path.home() / ".inkbox-codex"))
path = root / "a2a_turn_contexts"
path.mkdir(parents=True, exist_ok=True)
digest = hashlib.sha256(chat_id.encode()).hexdigest()
return path / f"{digest}.json"


def env_flag(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
Expand Down
2 changes: 1 addition & 1 deletion inkbox_codex/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def run_doctor() -> List[Tuple[str, bool, str]]:
import inkbox # noqa: F401
checks.append(("inkbox SDK", True, "installed"))
except ImportError:
checks.append(("inkbox SDK", False, "pip install 'inkbox>=0.5.1,<1.0.0'"))
checks.append(("inkbox SDK", False, "pip install 'inkbox>=0.5.6,<1.0.0'"))

try:
import aiohttp # noqa: F401
Expand Down
Loading