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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ INKBOX_SIGNING_KEY=whsec_xxxxxxxxxxxx
# INKBOX_ALLOWED_USERS=+15551234567,me@example.com # optional local allowlist
# INKBOX_REQUIRE_SIGNATURE=true
# INKBOX_BRIDGE_PORT=8767
# INKBOX_CODEX_AUTO_APPROVE_INKBOX_TOOLS=true # skip per-call prompts for Inkbox MCP tools only

# --- Realtime voice (optional; requires INKBOX_REALTIME_ENABLED=true) ---
# INKBOX_REALTIME_ENABLED=true
# INKBOX_REALTIME_API_KEY=sk-realtime
# OPENAI_API_KEY=sk-openai-fallback
# INKBOX_REALTIME_MODEL=gpt-realtime-2
# INKBOX_REALTIME_VOICE=cedar
# INKBOX_REALTIME_FALLBACK_TO_INKBOX_STT_TTS=true

# --- Codex ---
CODEX_PROJECT_DIR=/path/to/the/repo/codex/should/work/in
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ inkbox-codex doctor
inkbox-codex run
```

`inkbox-codex setup` walks you through everything and writes `.env`: create a fresh Inkbox agent via self-signup (or bring an existing API key), pick or create the identity, attach the Codex avatar to the agent's contact card (auto for a new self-signup agent; offered for an existing one with no avatar), provision a phone number, wait for your `START` opt-in, optionally enable OpenAI Realtime voice (validating your key), connect iMessage, mint a webhook signing key, choose the project directory, and set up autostart. Rerun it anytime to reconfigure. Prefer to wire `.env` by hand? Copy `.env.example` to `.env` and fill in `INKBOX_API_KEY`, `INKBOX_IDENTITY`, `INKBOX_SIGNING_KEY`, and `CODEX_PROJECT_DIR` yourself.
`inkbox-codex setup` walks you through everything and writes `.env`: create a fresh Inkbox agent via self-signup (or bring an existing API key), pick or create the identity, attach the Codex avatar to the agent's contact card (auto for a new self-signup agent; offered for an existing one with no avatar), provision a phone number, wait for your `START` opt-in, optionally enable OpenAI Realtime voice (validating your key), connect iMessage, mint a webhook signing key, choose the project directory, choose whether to trust Inkbox MCP tools without repeated allow prompts, and set up autostart. Rerun it anytime to reconfigure. Prefer to wire `.env` by hand? Copy `.env.example` to `.env` and fill in `INKBOX_API_KEY`, `INKBOX_IDENTITY`, `INKBOX_SIGNING_KEY`, and `CODEX_PROJECT_DIR` yourself.

On startup the bridge opens an Inkbox tunnel, wires mail/text/iMessage webhook subscriptions and the incoming-call channel to it, and routes everything into Codex sessions.

Expand Down Expand Up @@ -140,7 +140,7 @@ Codex never silently runs anything destructive. The bridge starts `codex app-ser

## Sessions

Sessions are keyed by Inkbox contact, so one person = one conversation across channels. Codex session ids are persisted in `~/.inkbox-codex/sessions.json` and resumed across bridge restarts — your conversation picks up where it left off. Replies go out on the channel you last used (call replies fall back to SMS if you hang up before Codex finishes).
Sessions are keyed by Inkbox contact, so one person = one conversation across channels. Codex session ids are persisted in `~/.inkbox-codex/sessions.json` and resumed across bridge restarts — your conversation picks up where it left off. Replies go out on the channel you last used. If a voice call ends before Codex finishes a voice reply, that late voice reply is dropped instead of silently switching to SMS or email.

**Typing indicator.** While Codex works on a turn, the bridge keeps a typing indicator alive on your iMessage thread (refreshed every few seconds, since it expires) so you can see it's busy. SMS, email, and voice have no typing indicator, so this is iMessage-only.

Expand Down Expand Up @@ -192,13 +192,14 @@ Calls have two modes, chosen per call:
| `CODEX_PROJECT_DIR` | yes | cwd | Directory Codex works in. |
| `CODEX_MODEL` | no | CLI default | Model override for bridged sessions. |
| `INKBOX_REQUIRE_SIGNATURE` | no | `true` | Refuse unsigned inbound webhooks unless `false`. |
| `INKBOX_BASE_URL` | no | `https://inkbox.ai` | Override the Inkbox API base URL. |
| `INKBOX_BASE_URL` | no | SDK default | Override the Inkbox API base URL. |
| `INKBOX_PUBLIC_URL` | no | - | Public bridge URL. Omit to use an Inkbox tunnel. |
| `INKBOX_TUNNEL_NAME` | no | identity handle | Tunnel name override. |
| `INKBOX_ALLOWED_USERS` | no | - | Local allowlist (emails / E.164 numbers). Usually leave empty and use Inkbox contact rules. |
| `INKBOX_ALLOW_ALL_USERS` | no | `false` | Allow all senders admitted by Inkbox contact rules. |
| `INKBOX_BRIDGE_PORT` | no | `8767` | Local webhook server port. |
| `INKBOX_PERMISSION_TIMEOUT_S` | no | `600` | Seconds to wait for a permission/poll reply. |
| `INKBOX_CODEX_AUTO_APPROVE_INKBOX_TOOLS` | no | `false` | Auto-accept Codex MCP prompts for Inkbox tools only. The setup wizard writes `true` when you trust the agent to send through Inkbox without per-call approval. |
| `CODEX_BIN` | no | `codex` | Codex CLI executable to run. |
| `CODEX_SANDBOX` | no | `workspace-write` | App-server thread sandbox (`read-only`, `workspace-write`, `danger-full-access`). |
| `CODEX_APPROVAL_POLICY` | no | `on-request` | Codex approval policy for bridged turns. |
Expand All @@ -219,7 +220,7 @@ The agent reaches you (or third parties) through an in-process MCP server:
- `inkbox_list_text_conversations` · `inkbox_get_text_conversation` — browse SMS threads and history.
- `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_export_contact_vcard` — save, edit, and export contacts (vCard 4.0). Reads and writes are filtered server-side to what this identity may see.
- `inkbox_create_contact` · `inkbox_update_contact` · `inkbox_delete_contact` — save, edit, and remove contacts. Reads and writes are filtered server-side to what this identity may see. vCard export/import is not exposed.

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 All @@ -230,7 +231,7 @@ On a live call, the OpenAI Realtime voice agent additionally gets `consult_agent
3. Ask it to do something requiring a command (e.g. "run the tests") and verify you get a permission text; reply `1` and verify the result comes back.
4. Ask it something open-ended enough to trigger a poll; reply with a number.
5. Email the agent; verify the reply lands as an email on the same thread.
6. Call the number, ask what it's working on, hang up mid-answer, and verify the tail arrives as a text.
6. Call the number, ask what it's working on, hang up mid-answer, and verify the late voice tail is not silently sent as SMS or email.

## Development

Expand Down
6 changes: 3 additions & 3 deletions inkbox_codex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

try:
from . import daemon
from .config import read_config
from .config import inkbox_client_kwargs, read_config
from .doctor import print_doctor
from .setup_wizard import interactive_setup
except ImportError: # pragma: no cover - direct local import/test fallback
import daemon
from config import read_config
from config import inkbox_client_kwargs, read_config
from doctor import print_doctor
from setup_wizard import interactive_setup

Expand All @@ -24,7 +24,7 @@ def _cmd_whoami() -> int:
return 1
from inkbox import Inkbox

identity = Inkbox(api_key=cfg.api_key, base_url=cfg.base_url).get_identity(cfg.identity)
identity = Inkbox(**inkbox_client_kwargs(cfg.api_key, cfg.base_url)).get_identity(cfg.identity)
mailbox = getattr(identity, "mailbox", None)
phone = getattr(identity, "phone_number", None)
print(f"handle: {identity.agent_handle}")
Expand Down
14 changes: 13 additions & 1 deletion inkbox_codex/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
RealtimeConfig,
)

INKBOX_BASE_URL_DEFAULT = "https://inkbox.ai"
# Empty means "do not override"; the Inkbox SDK owns its API default.
INKBOX_BASE_URL_DEFAULT = ""
INKBOX_WS_PATH = "/phone/media/ws"
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8767
Expand Down Expand Up @@ -67,11 +68,21 @@ class BridgeConfig:
codex_bin: str = "codex"
codex_sandbox: str = "workspace-write"
codex_approval_policy: str = "on-request"
auto_approve_inkbox_tools: bool = False
permission_timeout_s: float = 600.0
# OpenAI Realtime voice (off unless the wizard validated a key)
realtime: RealtimeConfig = field(default_factory=RealtimeConfig)


def inkbox_base_url_kwargs(base_url: str | None = None) -> Dict[str, str]:
normalized = str(base_url or "").strip()
return {"base_url": normalized} if normalized else {}


def inkbox_client_kwargs(api_key: str, base_url: str | None = None) -> Dict[str, str]:
return {"api_key": api_key, **inkbox_base_url_kwargs(base_url)}


def _read_realtime_config() -> RealtimeConfig:
"""Build the Realtime voice config from the env.

Expand Down Expand Up @@ -122,6 +133,7 @@ def read_config(extra: Dict[str, Any] | None = None) -> BridgeConfig:
or extra.get("codex_approval_policy")
or "on-request"
).strip(),
auto_approve_inkbox_tools=env_flag("INKBOX_CODEX_AUTO_APPROVE_INKBOX_TOOLS", False),
permission_timeout_s=float(os.getenv("INKBOX_PERMISSION_TIMEOUT_S") or 600.0),
realtime=_read_realtime_config(),
)
6 changes: 3 additions & 3 deletions inkbox_codex/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from typing import List, Tuple

try:
from .config import read_config
from .config import inkbox_client_kwargs, read_config
except ImportError: # pragma: no cover - direct local import/test fallback
from config import read_config
from config import inkbox_client_kwargs, read_config


def run_doctor() -> List[Tuple[str, bool, str]]:
Expand Down Expand Up @@ -68,7 +68,7 @@ def run_doctor() -> List[Tuple[str, bool, str]]:
try:
from inkbox import Inkbox

identity = Inkbox(api_key=cfg.api_key, base_url=cfg.base_url).get_identity(cfg.identity)
identity = Inkbox(**inkbox_client_kwargs(cfg.api_key, cfg.base_url)).get_identity(cfg.identity)
mailbox = getattr(identity, "mailbox", None)
phone = getattr(identity, "phone_number", None)
detail = ", ".join(filter(None, [
Expand Down
Loading
Loading