Give your opencode agent its own Inkbox identity:
a mailbox, a dedicated phone number for SMS/MMS and voice, iMessage,
contacts, notes, and an encrypted credential vault — as native opencode tools.
Email · SMS / MMS · iMessage · Calls · Contacts · Notes · Vault
-
Supported hosts: OpenCode 1.15.0–1.x or 2.0.26–2.0.x. Verified on 1.15.0, 1.18.35, 1.19.0, and 2.0.26. V1 and V2 use separate plugin entrypoints; the installer selects the installed host version.
-
opencode installed and logged in. The plugin loads into real opencode sessions and the gateway drives an
opencode serve— install it (opencode.ai) and authenticate a model provider.inkbox-opencode doctorchecks for it.curl -fsSL https://opencode.ai/install | bash -
Node.js 20+ for V1, Node.js 22+ for the V2 gateway, and git. The installer builds the plugin from source.
-
macOS or Linux. Boot persistence uses a systemd user unit on Linux and a launchd agent on macOS.
-
An Inkbox agent — nothing to set up in advance; the setup wizard self-signs up for you (or takes an existing API key).
This builds the plugin, wires it into your global opencode config, puts
inkbox-opencode on your PATH, and runs the setup wizard:
curl -fsSL https://raw.githubusercontent.com/inkbox-ai/opencode-plugin/main/install.sh | bashThat'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, 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.
Re-running is safe — it updates in place (--no-setup installs only;
--start launches the background gateway when done). From a local checkout,
run ./install.sh instead; it uses the checkout in place. Rerun the wizard
anytime with inkbox-opencode setup (setup --print for the static
checklist), and check the wiring with inkbox-opencode doctor.
Rerun ./install.sh --no-setup from the checkout after upgrading OpenCode.
The installer backs up and updates its generated wrapper. Custom wrappers and
their options are preserved; migrate them using the V2 example below. Check
project-local wrappers too, then run inkbox-opencode doctor.
V2 uses OpenCode's authenticated shared service. Configure the same Inkbox
identity and channel settings for the service and gateway. Tool selection and
outbound approval options remain in the plugin wrapper. Environment variables loaded
by the gateway do not change an already-running service's environment. Update
the service's launch environment or use the shared ~/.inkbox/config credential
file, and restart OpenCode when configuration changes require it.
For an explicitly selected V2 server, set OPENCODE_SERVER_URL and
INKBOX_OPENCODE_SERVER_PASSWORD in both the plugin and gateway environments.
INKBOX_OPENCODE_SERVER_USERNAME defaults to opencode. These settings also
apply to standalone V2 servers. Stopping the gateway leaves an attached shared
service running.
Set INKBOX_GATEWAY_MODEL=provider/model to pin the gateway model. An interactive
session's model selection does not configure the default for new gateway sessions.
The gateway and plugin must share the same accessible INKBOX_OPENCODE_HOME
directory (default ~/.inkbox-opencode), including when using an explicit server.
Finish outstanding conversations and stop the gateway before switching host versions. If doctor or
startup reports unfinished work from another host, reconnect that host and
resolve the work first. Rerun the installer after switching back to V1 as well.
If using boot autostart, rerun inkbox-opencode autostart install after changing
host versions to update the service definition.
An agent can complete setup non-interactively after a human assigns it an existing identity handle, API base URL, and credential.
First, download the installer once, inspect that exact file, and then run the same file without opening the setup wizard. The trap removes the temporary copy whether inspection or installation succeeds or fails:
installer="$(mktemp)"
cleanup() { rm -f "$installer"; }
trap cleanup EXIT
curl -fsSL https://raw.githubusercontent.com/inkbox-ai/opencode-plugin/main/install.sh -o "$installer" &&
cat "$installer" &&
bash "$installer" --no-setup
install_status=$?
cleanup
trap - EXIT
test "$install_status" -eq 0Keep the credential out of command history, source control, and project instructions. Read it without echoing, expose it only to the bootstrap process, and remove the transient shell value immediately afterward:
read -rsp 'Inkbox API key: ' INKBOX_API_KEY && printf '\n'
export INKBOX_API_KEY
inkbox-opencode bootstrap --identity '<handle>' --base-url '<url>' --project-dir "$PWD" \
--voice-ai --rotate-signing-key --start-gateway || bootstrap_status=$?
unset INKBOX_API_KEY
printf 'Bootstrap exit status: %s\n' "${bootstrap_status:-0}"Replace the quoted placeholders with the assigned values. bootstrap validates
the exact identity rather than creating another one, scopes down an admin key
before saving it, preserves existing Voice AI settings, and starts or restarts
the detached gateway. Signing-key replacement is explicit because it transfers
verified webhook delivery away from gateways using the previous key. The command
prints a secret-redacted JSON result and is safe to resume.
By default, the resulting agent-scoped credential is stored in the plugin's
local ~/.inkbox-opencode/.env profile with owner-only permissions, not in the
project.
-
On
"status": "requires_human", show the human each entry inhumanActions. After they complete the requested action, rerun the same bootstrap command with the same identity; do not create or select a different identity. -
On
"status": "error", report the redactederror, runinkbox-opencode doctor, correct the reported configuration, credential, or connectivity problem, and retry the same bootstrap command. If the credential was not saved before the failure, supply it again through the same private mechanism. -
On
"status": "configured", verify the completed installation with:inkbox-opencode doctor
The plugin is distributed as source — clone and build it, then load it from a local wrapper:
git clone https://github.com/inkbox-ai/opencode-plugin.git ~/.inkbox-opencode/app
cd ~/.inkbox-opencode/app
npm install && npm run buildThen, from your opencode project, install the packed build and add a one-file plugin wrapper:
archive=$(cd ~/.inkbox-opencode/app && npm pack --silent | tail -1)
npm install "$HOME/.inkbox-opencode/app/$archive"
mkdir -p .opencode/pluginsCreate .opencode/plugins/inkbox.ts; for global installation use
~/.config/opencode/plugins/inkbox.ts and install the tarball from
~/.config/opencode instead. Configure only one wrapper or package
entry for each location.
OpenCode V2: options go inside options in this wrapper:
import InkboxPlugin from "@inkbox/opencode-plugin/v2";
export default {
...InkboxPlugin,
setup: (ctx: Parameters<typeof InkboxPlugin.setup>[0]) => InkboxPlugin.setup({
...ctx,
options: { ...ctx.options, tools: { enable: ["inkbox_place_call"] } },
}),
};OpenCode V1: options go inside the second argument below. Later examples
using InkboxPlugin(input, { ... }) show this V1 form; on V2 place the same
option object in options above.
import InkboxPlugin from "@inkbox/opencode-plugin";
export default async (input: any) => InkboxPlugin(input, {
// options go here — see the examples throughout this README
});Credentials come from environment variables (recommended — keys stay out of committed files):
export INKBOX_API_KEY=sk-ink-... # agent-scoped API key
export INKBOX_IDENTITY=your-agent # agent handleGet both at inkbox.ai/console. They also fall back
to ~/.inkbox/config (the same key = value file the Inkbox CLI and SDKs
read), or pass them as wrapper options:
export default async (input: any) => InkboxPlugin(input, {
apiKey: process.env.INKBOX_API_KEY,
identity: "your-agent",
});Sanity-check the setup by asking the agent to run inkbox_doctor — it reports
config, API reachability, key scope, the resolved identity, and which tool
groups are enabled.
35 tools are enabled by default; 21 more are opt-in (see Enabling more tools). Names are stable — treat renames as breaking.
| Group | Enabled by default | Opt-in |
|---|---|---|
a2a |
inkbox_a2a_call, inkbox_a2a_check, inkbox_a2a_reply, inkbox_list_a2a_tasks, inkbox_list_a2a_messages, inkbox_a2a_complete, inkbox_a2a_ask_caller, inkbox_a2a_fail |
— |
email |
inkbox_send_email, inkbox_list_unread_emails, inkbox_list_emails, inkbox_get_email, inkbox_get_email_thread |
inkbox_forward_email, inkbox_mark_emails_read |
sms |
inkbox_send_sms, inkbox_list_text_conversations, inkbox_get_text_conversation |
inkbox_list_texts, inkbox_get_text, inkbox_mark_text_read, inkbox_mark_text_conversation_read |
imessage |
inkbox_send_imessage, inkbox_list_imessage_conversations, inkbox_get_imessage_conversation |
inkbox_imessage_triage_number, inkbox_list_imessage_assignments, inkbox_send_imessage_reaction, inkbox_mark_imessage_conversation_read |
calls |
inkbox_list_calls, inkbox_list_call_transcripts |
inkbox_place_call |
contacts |
inkbox_lookup_contact, inkbox_get_contact, inkbox_list_contacts, inkbox_create_contact, inkbox_update_contact, inkbox_delete_contact |
— |
notes |
inkbox_list_notes, inkbox_get_note, inkbox_create_note |
inkbox_update_note, inkbox_delete_note |
contact-rules |
inkbox_list_mail_contact_rules, inkbox_list_phone_contact_rules, inkbox_list_imessage_contact_rules (read-only: rule changes are console-only) |
— |
access |
— | inkbox_list_note_access, inkbox_grant_note_access, inkbox_revoke_note_access |
vault |
— | inkbox_credentials_list, and by exact name only: inkbox_credentials_get_login, inkbox_credentials_get_api_key, inkbox_credentials_get_ssh_key, inkbox_totp_code |
diagnostics |
inkbox_whoami, inkbox_doctor |
— |
opencode sends every registered tool's spec to the model on each turn, so the default surface is deliberately lean. Enable more by exact name or by group, then restart opencode:
export default async (input: any) => InkboxPlugin(input, {
tools: { enable: ["inkbox_place_call", "inkbox_forward_email"] },
});tools.enable/tools.disableaccept tool names, group names, or"all". The more specific entry wins (name over group over"all"); at equal specificity, disable wins.- Vault plaintext reads are stricter:
inkbox_credentials_get_login,inkbox_credentials_get_api_key,inkbox_credentials_get_ssh_key, andinkbox_totp_codemust be enabled by exact name —"vault"or"all"never turns them on. Vault tools also need the unlock key inINKBOX_OPENCODE_VAULT_KEY(override the variable name with thevault.keyEnvVaroption). inkbox_doctorlists everything that's currently disabled, so the agent can tell you what to enable instead of silently lacking a capability.
Sends, calls, and A2A writes are gated before anything leaves:
- Approval prompts (default):
inkbox_send_email,inkbox_send_sms,inkbox_send_imessage,inkbox_forward_email,inkbox_place_call,inkbox_a2a_call, andinkbox_a2a_replyrequest approval through opencode's native permission system. Approve once, or persist an allow rule from the prompt. OpenCodeallowrules can approve without a prompt; configure explicitaskrules when prompts are required. - Recipient allowlist: set
outbound.allowedRecipients(exact email addresses / E.164 numbers) and anything else is rejected regardless of approval mode. - Unattended runs: prompts time out after 5 minutes
(
outbound.askTimeoutMs) with a clear error instead of hanging forever. For headless use, either pre-allow the tools in opencode'spermissionconfig, or setoutbound.approvalto"allowlist"(allowlist only — requires a non-emptyallowedRecipients) or"auto".
export default async (input: any) => InkboxPlugin(input, {
outbound: {
approval: "allowlist",
allowedRecipients: ["ceo@example.com", "+15551234567"],
},
});The repo bundles skills that teach the agent Inkbox etiquette — email
triage, SMS/iMessage response patterns, outbound calling, contact and
credential hygiene, troubleshooting. Point opencode at the clone's skills/
directory with one config line:
{
"skills": { "paths": ["$HOME/.inkbox-opencode/app/skills"] }
}(Or copy the skills/ directory into .opencode/skills/ to vendor it. Skills
already installed under .claude/skills/ are picked up by opencode's Claude
Code compatibility loading.)
An Inkbox identity gives the agent:
- A mailbox — a real email address; send, receive, reply with threading.
- A dedicated phone number — SMS/MMS and voice on the same line. New numbers take ~10–15 minutes to propagate to carriers; SMS to US numbers requires the recipient to opt in (text START) per carrier rules.
- iMessage — the agent does not get its own iMessage number. People connect to the agent through the shared Inkbox iMessage line, and each connected person gets a dedicated thread. It's recipient-first by design: no cold outreach; someone messages the agent first, then the agent can reply (and react, and even call) over that connection.
Outbound calls can originate from either line, chosen with origination on
inkbox_place_call:
dedicated_number— the agent's own number, the same line SMS uses. Can call anyone.shared_imessage_number— call a person over the shared iMessage line they already message the agent on. Only works for people connected over 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. 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.
| Option | Env var | Purpose |
|---|---|---|
apiKey |
INKBOX_API_KEY |
Agent-scoped API key (required) |
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 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_OPENCODE_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" |
outbound.allowedRecipients |
— | Exact-match recipient allowlist |
outbound.askTimeoutMs |
— | Approval prompt timeout (default 300000) |
apiKey, identity, baseUrl, and signingKey resolve in order: plugin
option → env var → ~/.inkbox/config.
Beyond outbound tools, the plugin can run an inbound gateway: a long-lived process that receives email, SMS, iMessage, and phone calls to the agent's identity and turns each into an opencode session that replies on the same channel. It is off by default.
you (phone) ── SMS / iMessage / email / call ──▶ Inkbox ──▶ tunnel ──▶ gateway
│
▼
opencode session
(Inkbox tools, in
your project dir)
Two ways to run it:
-
Sidecar (recommended) — a self-contained companion process:
inkbox-opencode run # or manage it as a daemon: inkbox-opencode start | status | stop
The sidecar needs an opencode server and finds one on its own: an explicit
gateway.serverUrl/OPENCODE_SERVER_URLmust answer. V2 discovers or starts the shared authenticated service and leaves it running when the gateway stops. V1 otherwise attaches to a server already onhttp://127.0.0.1:4096, and failing that it launches its own managedopencode serve(port4097by default; tune withgateway.serve.{bin,port}orINKBOX_OPENCODE_BIN/INKBOX_GATEWAY_SERVE_PORT). A managed server is stopped with the gateway, and its death takes the gateway down so a service manager restarts the pair. -
In-plugin — set
gateway.modeto"plugin"and run insideopencode serveitself. This needs the Inkbox tunnel to work under the host runtime (or a reachablegateway.publicUrl); the sidecar avoids that requirement.
Enable it and point sessions at a working directory:
export default async (input: any) => InkboxPlugin(input, {
gateway: {
enabled: true,
projectDirectory: "/path/to/agent/workspace",
voice: { enabled: true, realtime: { enabled: true } },
},
});The gateway needs a webhook signing key (INKBOX_SIGNING_KEY) to verify
inbound events. What it does:
- Email / SMS / iMessage arrive as sessions keyed per contact (one person, one ongoing conversation across channels); replies go back on the channel the message came in on, threaded for email. Delivery failures wake the agent to retry or switch channels, except native iMessage delivery failures, which are retained as quiet context without an automatic retry.
- Contact memories from verified inbound events are included as optional
background context by default. Set
gateway.contactMemories: falseorINKBOX_CONTACT_MEMORIES_ENABLED=falseto omit them from every channel. - Permission prompts raised inside a gateway session are relayed to the contact on their channel ("reply 1 to allow once, 2 to always allow, 3 to decline") and time out to a decline.
- A2A worker tasks acknowledge pickup immediately and send a short,
nonterminal progress update about every three minutes until they settle. Set
gateway.a2aProgressIntervalSecondsorINKBOX_A2A_PROGRESS_INTERVAL_SECONDS; use0to disable periodic updates. Progress stays in task history and does not start a separate requester turn. - Control commands (whole-message):
/clear,/stop,/status,/health,/resume,/usage. - 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(orOPENAI_API_KEYas the backstop) and runs the call as a live raw-audio conversation with in-call actions. Call media negotiates mono 16-bit PCM at 16 kHz, with streaming conversion to and from the realtime connection’s 24 kHz PCM. Older call streams retain 8 kHz compatibility. Otherwise Inkbox handles speech-to-text and text-to-speech. Opt out withINKBOX_VOICE_ENABLED=false(stop answering) orINKBOX_REALTIME_ENABLED=false(force Inkbox STT/TTS).inkbox_place_calldials out with a purpose loaded into the call.
Run inkbox-opencode doctor to check gateway readiness (API reachability,
identity, signing key, opencode server, tunnel/public URL).
Gateway shutdown preserves accepted native work. Permission requests observed during shutdown are durably declined only while their exact turn owner is still current. On reconnect or restart, the gateway reads native pending permissions and recovers only requests tied to a reclaimed turn's actual assistant parent and original reply route; unrelated sessions are left alone. Responses whose outcome is uncertain are retained, never blindly posted again. Readiness stays unconfirmed until the new attachment's native permission inventory succeeds.
Version 0.2.15 requires SDK 0.7.7 and supports sponsored email, MMS, and dedicated-line iMessage groups. Enable Companion mode and select a sponsor in the identity settings. Installing the plugin does not enable it.
The sponsor's qualifying group message loads all available authorized history into one input. Ordinary tracked messages and activated conversations use separate sessions, isolated from private contact conversations. Replies retain the group conversation and, for email, its approved audience and stored parent. MMS chats with identical participant sets are one logical conversation.
The sponsor must pass your gateway's local sender/contact allowlists. Contact blocks, consent requirements, and host tool approvals still apply. Historical commands and answers never execute. Only the prompted sender can answer an ordinary approval. Companion approval answers must come from the sender whose turn raised the prompt; local commands remain sponsor-only. Both must pass the current-message response gates. In mention mode, use @agent allow or @agent /stop.
The complete framed input is limited to 8 MiB. Oversized or unavailable initialization pauses the conversation without sending a partial input. The private gateway state.json journal retains the turn ID, host message ID, reply target, and failure state. A restart resumes pending hydration and reconciles known host submissions. If acceptance is uncertain, the turn and later group messages remain paused for operator investigation; webhook retries do not resubmit it. Do not delete the journal to retry an uncertain turn. Non-transport preparation failures stop retrying after five retries; transient transport failures keep a capped backoff. Consumed ordinary quiet context is bounded, while Companion source receipts and history anchors remain durable to prevent duplicate work.
INKBOX_GROUP_REPLY_MODE=auto|mention defaults to auto. Mention mode requires a whole @agent or @your-handle in the current message. For Companion email, the agent's mailbox in the current To recipients also counts; Cc/Bcc and quoted headers do not. Unaddressed group messages are saved as context without running the model or interrupting a reply.
INKBOX_COMPANION_RESPONSE_MODE=safe|relaxed defaults to safe. Safe mode wakes only on the current message's sender_access="direct"; sponsored or unknown access stays context-only. Relaxed allows any delivered participant to wake the agent, subject to the independent mention setting. Direct means receipt-time contact-rule admission, not permanent trust or permission to run tools. Both settings are available in the setup wizard.
Group SMS and iMessage use one session per conversation, including reactions; separate groups and direct messages stay separate. Automatic email replies use the SDK's stored-message reply-all operation, preserving To/Cc and threading. A complete model answer is journaled before delivery. Transient pre-submission or send-preparation failures retry with capped backoff without regenerating a completed answer. Signed live receipts use the saved initialization and sponsor reply anchor without repeated activation lookups.
Like the claude-code and codex bridges, the gateway can install itself as a boot/login service so the agent stays reachable without a shell:
inkbox-opencode autostart install
inkbox-opencode autostart status | uninstallOn Linux this writes and enables a systemd user unit
(~/.config/systemd/user/inkbox-opencode.service); on macOS, a launchd
agent (~/Library/LaunchAgents/ai.inkbox.opencode.plist). The service runs
inkbox-opencode run from the directory you installed in, launching its own
managed opencode serve — one service is the whole always-on stack.
Install-time details:
INKBOX_*andOPENAI_API_KEYfrom the installing shell are snapshotted to~/.inkbox-opencode/.env(chmod 600) and loaded by the service; real environment variables and~/.inkbox/configstill win/backstop. A./.envin the working directory layers underneath (see.env.example), filling any vars the snapshot doesn't set.- A fork-based
inkbox-opencode startdaemon is stopped first so two gateways never fight over the tunnel. - To keep a Linux service alive while logged out, enable lingering once:
sudo loginctl enable-linger $USER. inkbox-opencode statusreports both the background daemon and the boot service;inkbox-opencode uninstallremoves the service and local state.
One identity gets one tunnel: don't run an always-on gateway on the same identity your CI live suites use, or they will contend for it.
npm install
npm run typecheck # tsc
npm test # vitest (unit + contract)
npm run lint # biome
npm run build # emits dist/
bash scripts/smoke-loader.sh # packs the tarball and loads it through a real opencodeThe contract tests under tests/contract/ pin the opencode plugin API surface
this package depends on; CI runs them against @opencode-ai/plugin@latest
twice daily to catch upstream drift.
Inbound delivery (email/texts arriving as opencode sessions) is being
validated for a future release — see docs/gateway-spike.md for the current
findings.
Slack is off by default. Run inkbox-opencode setup to enable it, select the app's provisioning workspace, provide or renew App Configuration credentials through masked input, open the browser installation link, and wait for the matching workspace connection. Setup is bounded and can be resumed; an unknown app-creation outcome is never blindly repeated. For an already connected identity, set INKBOX_SLACK_ENABLED=true or gateway.slackEnabled: true and restart.
The eight Slack tools are inkbox_slack_list_connections, inkbox_slack_list_conversations, inkbox_slack_list_messages, inkbox_slack_search, inkbox_slack_send_message, inkbox_slack_upload_file, inkbox_slack_get_operation, and inkbox_slack_get_action. Search covers retained message text, not all workspace history or files; follow nextCursor even when a page is empty. Reuse the same idempotency key for a send retry and inspect unknown actions before doing anything else.
Ordinary Slack channel traffic starts a session only when addressed; later messages in that engaged thread can continue it. Unmentioned traffic in unrelated threads does not wake the agent or display activity. DMs and group DMs do not require a prior session.
Slack Companion context spans the channel within one identity, installation, and activation. Replies and approvals remain bound to the current native thread; a null thread stays top-level. The existing Safe/Relaxed and Auto/Mention settings apply. Quiet context makes no model/tool/activity calls. Local Slack allowlists may use a bare actor ID or the installation-workspace WORKSPACE_ID:ACTOR_ID; Companion also recognizes the canonical home-workspace author. Matching sender-profile fields provide bounded context, never additional authority.
Generated files can be delivered with inkbox_slack_upload_file: provide an actual local filePath (1 byte–10 MiB), connection/conversation, and idempotency key. The plugin reads the bytes itself and retains the active source thread. It checkpoints the native tool call before upload, never replays an unknown outcome, and returns the real operation status/file ID. Inspect uncertain outcomes with inkbox_slack_get_operation; text saying “attached” is not delivery.
Slack turns receive Slack-specific mrkdwn guidance; simple Markdown links/bold are repaired outside code. Progress is coalesced into native task updates when optional SDK streaming methods and backend capability are available; otherwise a single source-bound message is edited in place. The published SDK 0.7.15 is the supported baseline (^0.7.15 permits later compatible upgrades). Inline DMs stay inline. Unknown progress creations are not retried or replaced, and progress failure never blocks the final answer. Progress uses bounded generic tool labels, never arguments, file paths, raw results, or delegation IDs. The progress journal requires a local filesystem with hard-link support; do not share the same plugin state directory between containers with separate PID namespaces.
Verified native Stop acknowledges abort and confirms idle before releasing the queue; a later source can run after Stop or restart without replaying the old turn. An unverified stop or unknown crossed send remains fenced/inspectable rather than being guessed safe.
Top-level inline replies show eyes while working or awaiting approval; a failed reply clears eyes and adds an X. Native thread replies use Slack's working/awaiting-input/ready indicator, never eyes. Indicators are best-effort and cannot prove visible client behavior: verify the actual Slack UI during acceptance.
Set INKBOX_IMESSAGE_THREADED_REPLIES=true (or gateway.imessageThreadedReplies) to target known-source replies natively. Compatible fragments share a 750 ms quiet window capped at 2 seconds and reply to the first source. Different senders, explicit thread contexts, media, and Companion receipts are separate. Follow-ups queue without interrupting the active native turn. inkbox_get_imessage_thread and inkbox_get_imessage_conversation_thread provide bounded chronological reads; opaque thread IDs are not message IDs.
Before a targeted send or its local media upload, the bridge verifies the admitted source conversation and reads at most one native thread item. Unsupported endpoints fail explicitly; fetched history is not injected into Companion context. Safe read failures retain the completed reply for retry without another model turn.
An explicitly requested send to another recipient or conversation is independent: it uses the normal outbound approval and allowlist rules, does not inherit a native reply target, and does not suppress the answer to the current request. The originating turn must still be active before uploads and sends; Stop or disabling native replies prevents effects that have not crossed their send checkpoint. Stop cannot retract an in-flight send: its accepted or unknown result remains inspectable, including after restart. Shutdown or disabling a channel during safe reply preparation retains the saved answer without another model turn.
The bridge owns automatic and source-correlated tool reply targets and allows only API-controlled same-conversation fallback. Proactive turns do not inherit an earlier target. Native messages and reactions share the conversation session; reactions do not inherit a native reply source or interrupt active work. Delivery failures, including unmatched proactive sends, are retained as quiet context for later input, never automatic resend requests. Durable records checkpoint admission, model output, and send attempts. An uncertain send is retained without plain resend or model replay; it is not proof of failure or of visible phone delivery. Check the receiving phone for native UI acceptance.
Approval prompts serialize per conversation. Only explicit approval tokens answer them; Stop and fresh instructions are not swallowed. Shared-channel replies require the prompted author and exact native route. /health, the CLI doctor, and inkbox_doctor expose pending/unconfirmed/blocked counts separately from process liveness. Never delete the durable journal to clear an uncertainty warning.
Vault migration: export INKBOX_OPENCODE_VAULT_KEY for lazy optional unlocking. Existing custom vault.keyEnvVar settings still work. If you retain INKBOX_VAULT_KEY or a Vault key in the SDK's global config, the SDK may unlock eagerly; remove that global setting when migrating to the plugin-local variable. Locked metadata listing continues to work. Individual plaintext/TOTP reads recheck the configured identity's grants and refresh the secret; login payloads expose has_totp, never the TOTP seed. Existing typed tool names remain supported; inkbox_credentials_get_secret adds key-pair/other reads and must be enabled by its exact name.
Doctor distinguishes disabled channels, missing SDK capabilities, disconnected Slack workspaces, incomplete active webhook coverage, and unverified native backend support. An available source permits a bounded read-only native endpoint probe; absent source or tunnel URL remains explicitly unverified. These checks never send messages or modify subscriptions.
