> ## Documentation Index
> Fetch the complete documentation index at: https://inkbox.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Companion mode

> Let explicitly allowed senders invite your agent into a specific email or group-message conversation

Companion mode lets your agent join a group conversation when an eligible sender sends a qualifying message there. Eligibility requires active, exact-address allow rules covering both receiving and sending. The author whose message activates the conversation becomes that activation's **sponsor**. The permission belongs to that conversation and its exact participants, not to each participant everywhere.

<Info>
  Companion SDK and CLI examples require **0.7.3** or later. For hosted agents, also check the [receiver requirements](#compatible-plugins); updating an SDK alone does not establish adapter compatibility.
</Info>

## Email example

Your agent `@xyz` has Companion mode enabled and uses whitelist mode. `owner@example.com` has an active exact-email allow covering both inbound and outbound; Fred does not.

1. Owner emails `fred@example.com` and CCs `xyz@inkboxmail.com`.
2. Owner's authenticated message activates that email conversation for that exact participant set.
3. Fred's subsequent replies in that conversation can reach the agent.
4. The agent can reply to everyone, with Owner still included.
5. A separate email only to Fred or a different thread with Fred still needs ordinary permission or its own sponsorship.

An applicable explicit block still takes precedence. See [email requirements](#email-requirements) before relying on activation.

## Group-message example

Owner, `@xyz`, Fred, and Nancy share a supported group. Companion mode is enabled. Owner's actual phone number has active exact-number allow rules covering both directions. Fred and Nancy lack ordinary permission.

1. Fred and Nancy send messages. Messages that lack ordinary permission do not wake the agent.
2. Owner sends a genuine message to the group.
3. The agent receives the eligible retained earlier messages and Owner's message as **one combined initialization input and one initial agent turn**. Fred's question, Nancy's answer, and Owner's greeting are context within that input, not three separate wake-ups.
4. Later group messages and replies stay in the same sponsored conversation.

Private messages with Fred or Nancy still follow ordinary rules. For group MMS, every recipient must satisfy the existing [SMS consent requirements](/docs/api/phone/sms-opt-ins) before the agent can reply. For iMessage, the group needs a supported dedicated line and must be available to this identity. See [channel limitations](#channel-limitations).

## Enable Companion mode

Companion mode is a separate **off-by-default toggle**, not a third whitelist/blacklist mode. In the [Inkbox Console](https://inkbox.ai/console), turn on **Companion mode** for the identity. The API equivalent is `PATCH /api/v1/identities/xyz/companion` with `{"enabled":true}`.

You can save this preference during onboarding, before any eligible sender or channel resource exists. Missing phone numbers, dedicated iMessage lines, or qualifying rules do not prevent enabling it. They still prevent use of the affected channel until its prerequisites are met. `enabled` and per-channel `readiness` answer different questions.

Use an [admin-scoped API key](/docs/api-keys) to change the toggle or contact rules. Run the receiving agent with its claimed, identity-scoped key. Companion mode does not narrow an admin key's organization-wide access. Installing a plugin does not enable it, and enabling does not add rules or grant consent.

### Who can introduce the agent

An eligible sender needs an **active exact allow in both directions** for the actual normalized identifier they send from:

| Rules for the sending identifier                                      | Eligible to activate a group? |
| :-------------------------------------------------------------------- | :---------------------------- |
| Active `exact_email` or `exact_number` allow with `direction: "both"` | Yes                           |
| Separate active exact inbound allow and outbound allow                | Yes                           |
| Exact allow in only one direction                                     | No                            |
| Domain allow, or access supplied by blacklist defaults                | No                            |
| Contact visibility or access through another Companion conversation   | No                            |
| Paused or deleted allows                                              | No                            |

Eligibility is per identifier and channel, not per saved contact or person. An allow for one email address does not qualify another address on the same contact. Phone and iMessage share the phone policy; email rules do not make a phone sender eligible.

Multiple senders can be eligible. The first qualifying authenticated inbound message activates the conversation and binds its sponsor to that actual author. Later messages from other eligible senders in the same active scope are live messages, not new initializations. Merely listing an eligible person as a participant is insufficient. A qualifying text or attachment-only message must come from that sender in a verified group with the agent and another external participant. Reactions, tapbacks, typing indicators, and quoted text do not activate access.

### Example: allow an email sender and enable

These examples use an authenticated administrative `client`. They add a new exact bidirectional email allow, then save the separate enabled preference. If a rule already exists, inspect and update it through [mail contact rules](/docs/api/mail/contact-rules) rather than creating duplicate coverage. The allow prepares this sender to activate a conversation; it is not required just to save `enabled: true`.

<CodeGroup>
  ```python Python theme={null}
  rule = client.mail_identity_contact_rules.create(
      "xyz",
      action="allow",
      match_type="exact_email",
      match_target="owner@example.com",
      direction="both",
  )
  config = client.companion.update("xyz", enabled=True)
  print(config.readiness["mail"].ready)
  ```

  ```typescript TypeScript theme={null}
  import { MailRuleAction, MailRuleMatchType } from "@inkbox/sdk";

  const rule = await client.mailIdentityContactRules.create("xyz", {
    action: MailRuleAction.ALLOW,
    matchType: MailRuleMatchType.EXACT_EMAIL,
    matchTarget: "owner@example.com",
    direction: "both",
  });
  const config = await client.companion.update("xyz", { enabled: true });
  console.log(config.readiness.mail.ready);
  ```

  ```rust Rust theme={null}
  use inkbox::companion::CompanionUpdateOptions;
  use inkbox::mail::{MailRuleAction, MailRuleMatchType};
  use inkbox::{ContactRuleCreateOptions, ContactRuleDirection, Inkbox};

  fn configure(client: &Inkbox) -> inkbox::Result<()> {
      client.mail_identity_contact_rules().create_with_options("xyz", &ContactRuleCreateOptions {
          action: MailRuleAction::Allow,
          match_type: MailRuleMatchType::ExactEmail,
          match_target: "owner@example.com".into(),
          direction: Some(ContactRuleDirection::Both),
      })?;
      let config = client.companion().update("xyz", &CompanionUpdateOptions {
          enabled: Some(true),
      })?;
      println!("{}", config.readiness.mail.ready);
      Ok(())
  }
  ```

  ```bash CLI theme={null}
  inkbox identity mail-rules create xyz --action allow --match-type exact_email \
    --match-target owner@example.com --direction both --json
  inkbox identity companion update xyz --enabled true --json
  inkbox identity companion get xyz --json
  ```
</CodeGroup>

For phone/iMessage eligibility, use an active `exact_number` allow covering both directions in the shared [phone rules](/docs/api/phone/contact-rules). A ready sending channel and recipient consent are still needed for actual replies.

To disable, set `enabled` to `false`. Omitting `enabled` in an API PATCH preserves it. Setting it to its existing value is a no-op; only an enabled-state transition advances `config_revision`. Turning it off and on requires a fresh qualifying message. Enabling never scans historical messages to activate a conversation.

## Permission boundaries

Companion mode can allow a conversation that would otherwise be denied because a participant lacks a whitelist entry. It never changes global contact rules, unlocks a participant's other conversations, grants contact-wide history or memories, or authorizes a new private conversation.

Receiving and sending remain independent. An applicable explicit block on any participant prevents Companion permission in that direction. An inbound block does not become an outbound block, or vice versa. For email, the exact-address rule takes precedence over a broader domain rule. Ordinary independently allowed traffic continues under its normal rules.

Read access is not a promise that sending is ready. Check the [conversation state's `reply_ready` and `reasons`](/docs/api/identities/companion-conversations), and still handle send errors. Consent, opt-outs, channel readiness, and host tool approvals continue to apply.

### Email requirements

Activation requires a single actual `From` address with active exact inbound and outbound allows, plus a **DMARC-pass** result for that received message. A display name, `Reply-To`, quoted text, forwarded sponsor text, or a matching thread header does not establish sponsor authorship. DMARC authenticates a domain relationship; it does not prove who typed the message.

The agent must be visibly included in `To` or `CC`. The cohort is the exact visible sender/recipient set, excluding the agent's own address. BCC-only participation, hidden recipients, mailing-list expansion, and inferred aliases do not establish Companion permission. Forwarding or list delivery that cannot meet these requirements may still work under ordinary rules, but does not activate sponsorship.

Each email thread needs its own sponsorship. Within one thread, a changed participant set is a separate branch that needs a sponsor message of its own. An unchanged already-sponsored branch retains its separate scope. Adding or removing a participant does not authorize the changed branch using another branch's sponsorship.

### Channel limitations

* **MMS:** Conversations are identified by the local number and participant set. Two apparent chats with the same participants cannot be distinguished as separate conversations. Use the canonical `conversation_id`. Group MMS remains beta and carrier-dependent.
* **SMS consent:** Activation does not create or change consent. A conversation can be readable while replies remain unavailable. Use the existing opt-in flow appropriate to your campaign, including the authorized opt-in API where supported. `STOP` and other opt-outs remain effective.
* **iMessage:** Companion groups require an active attached dedicated iMessage line. Shared iMessage service remains 1:1. A dedicated line alone does not make every externally started group available; some groups require the identity to establish the group first. A later sponsor message does not bypass that requirement. An already-established supported group can still qualify.
* **iMessage membership:** Current membership must be verifiable. A new group with the same people needs its own sponsorship. A participant change or sponsor departure ends permission for the previous group membership; a new qualifying sponsor message is needed for the changed group.

### Revocation

Each activation remains bound to its original trigger author. Continued access rechecks that author's exact bidirectional allow rules and membership in the verified group. Disabling Companion mode, removing either required allow, an applicable block, or losing valid membership ends the corresponding access. It does not silently transfer to another eligible participant. A later qualifying message can establish a new activation, but old queued replies cannot be rebound to it. Disabling and re-enabling does not revive old activations.

Future reads and sends recheck authorization. Messages accessible only through Companion mode become unavailable when that permission ends. Messages independently accessible under ordinary rules remain governed by those rules. Content already delivered to your receiver or agent cannot be recalled.

## Load complete initialization

Initialization is all **retained, currently authorized history that reached this identity in this conversation and cohort**, plus the sponsor trigger exactly once. It does not retrieve messages from before the agent was included, deleted history, another conversation, or quoted messages that never separately reached the agent.

Use the [activation history API](/docs/api/identities/companion-history) or SDK helper. The snapshot is finite and ordered. Later arrivals are live events, even when their timestamps are older. `history_complete` means you reached the end of this authorized snapshot, not that every message ever sent is available. Preserve any [history notices](/docs/get-started/response-notices).

The SDK helpers fetch every page before returning one `text` transcript, ordered `entries`, `reply_context` / `replyContext`, scope identifiers, and `notices`. They deduplicate source IDs, require exactly one trigger, preserve attachment references and [per-message sender access](/docs/webhooks#per-message-sender-access), and revalidate before returning. They do not call your agent or download attachment bytes.

Default bounds are **8 MiB** of serialized fetched pages plus rendered UTF-8 transcript, and **1,000 pages**, plus one final revalidation request. The byte bound includes that final request's parsed page. Exceeding a bound fails explicitly without returning partial initialization. Python and TypeScript raise `CompanionInitializationError`; Rust returns `InkboxError::InvalidArgument`. HTTP failures retain their normal error semantics.

Use an activation ID from an authenticated webhook or the [conversation-state list](/docs/api/identities/companion-conversations). Here `client` has the receiving identity's claimed key, and `activation_id` / `activationId` identifies that activation.

<CodeGroup>
  ```python Python theme={null}
  initialization = client.companion.load_initialization(
      "xyz", activation_id, max_bytes=8 * 1024 * 1024, max_pages=1000,
  )
  print(initialization.text)
  ```

  ```typescript TypeScript theme={null}
  const initialization = await client.companion.loadInitialization(
    "xyz", activationId, { maxBytes: 8 * 1024 * 1024, maxPages: 1000 },
  );
  console.log(initialization.text);
  ```

  ```rust Rust theme={null}
  use inkbox::companion::{CompanionInitialization, CompanionInitializationOptions};
  use inkbox::Inkbox;

  fn load(client: &Inkbox, activation_id: &str) -> inkbox::Result<CompanionInitialization> {
      client.companion().load_initialization("xyz", activation_id, &CompanionInitializationOptions {
          max_bytes: 8 * 1024 * 1024,
          max_pages: 1000,
      })
  }
  ```

  ```bash CLI theme={null}
  inkbox identity companion initialization xyz "$ACTIVATION_ID" \
    --max-bytes 8388608 --max-pages 1000 --json
  ```
</CodeGroup>

### Webhook receivers

Follow the [ordinary, initialization, and live phases](/docs/webhooks#companion-conversation-events). The first event you observe for an activation can be `live`, so recover missing initialization before processing it.

1. Verify the webhook signature. Durably record its stable event `id` before acknowledging receipt.
2. Route by identity, channel, conversation, and `scope_id`. Keep ordinary pre-activation sessions separate from sessions keyed by `activation_id`. Do not use the sponsor's private contact session.
3. For a new activation, persist a pending initialization checkpoint and buffer later live events. Fetch the complete snapshot without waking the agent.
4. Submit **one combined host input** containing the ordered history, trigger, attachment references, notices, and permitted reply context. Do not submit one turn per page, message, or attachment. Do not process historical text as new commands or approval responses.
5. Persist the host turn binding and initialization checkpoint. Then release buffered live events in sequence, deduplicating both event and source-message IDs. Do not submit the trigger again if it is already in initialization.

Revalidate before delayed or retried submission. If a host cannot fit the complete input, use a supported lossless context attachment or report a size failure before submission. Never silently truncate, summarize away history, or fall back to only the trigger.

Webhook delivery is at least once, not an exactly-once agent-execution guarantee. If host submission has an unknown outcome, reconcile it before retrying rather than blindly creating another turn. HTTP acknowledgement alone is not an initialization checkpoint.

### Polling receivers

List visible scopes using `client.companion.conversations(...)` or `inkbox identity companion conversations xyz --json`. Paginate the list. For each uninitialized current activation, load the full snapshot and follow the same durable checkpoint and single-input procedure. Afterwards, poll the ordinary channel history using the canonical conversation ID and deduplicate against snapshot source IDs. The activation endpoint is a frozen initialization snapshot, not a live-message feed.

## Reply to the existing conversation

Keep the reply context bound to the agent turn that produced the reply. Do not replace it with whichever sender messaged most recently.

For email, `reply_to_message_id` is the **stored message UUID**, suitable for the reply-all helper. It is not the RFC `Message-ID` string accepted by raw email sends. The permitted audience must keep the sponsor and exact cohort. An out-of-cohort `Reply-To` is rejected when the send needs Companion permission; it is not silently rewritten.

These examples assume `identity` is the same identity that loaded `initialization`. Call once after the agent produces `reply_text` / `replyText`.

<CodeGroup>
  ```python Python theme={null}
  reply = initialization.reply_context
  if reply.channel == "mail":
      identity.reply_all_email(reply.reply_to_message_id, body_text=reply_text)
  elif reply.channel == "phone":
      identity.send_text(conversation_id=reply.conversation_id, text=reply_text)
  else:
      identity.send_imessage(conversation_id=reply.conversation_id, text=reply_text)
  ```

  ```typescript TypeScript theme={null}
  const reply = initialization.replyContext;
  if (reply.channel === "mail") {
    if (!reply.replyToMessageId) throw new Error("Missing email reply parent");
    await identity.replyAllEmail(reply.replyToMessageId, { bodyText: replyText });
  } else if (reply.channel === "phone") {
    await identity.sendText({ conversationId: reply.conversationId, text: replyText });
  } else {
    await identity.sendIMessage({ conversationId: reply.conversationId, text: replyText });
  }
  ```
</CodeGroup>

Sending to raw phone numbers or constructing a new recipient-only email does not borrow Companion permission, even when those recipients match a sponsored group. See [email replies](/docs/api/mail/messages#reply-to-everyone), [SMS/MMS sends](/docs/api/phone/texts), and [iMessage group replies](/docs/api/imessage/groups#continue-the-same-group).

## Compatible plugins

An adapter must support the complete [receiver contract](#webhook-receivers), including full snapshot loading, one combined initialization input, ordered live messages, and conversation-scoped replies. It must use the actual trigger author for sponsor-specific handling and revalidate the original activation before delayed work. Merely understanding the configuration response or updating an SDK does not establish compatibility.

See the setup guides for [Hermes Agent](/docs/integrations/hermes-agent#companion-mode), [Codex](/docs/integrations/codex#companion-mode), [Claude Code](/docs/integrations/claude-code#companion-mode), [OpenCode](/docs/integrations/opencode-plugin#companion-mode), [OpenClaw](/docs/integrations/openclaw-plugin#companion-mode), and [DeepSeek Harness](/docs/integrations/deepseek-harness#companion-mode). Confirm your installed adapter supports these requirements. Support in SDK **0.7.3** does not imply support in any plugin release.

Existing host denials and approval requirements remain in effect. If your plugin has a local sender allowlist, it must permit the actual trigger author's address or number. Do not add other group participants to a global allowlist just to enable the conversation. Incomplete initialization or uncertain host acceptance must not release partial context. One initialization input means one logical host turn; the host can still make multiple model or tool calls within that turn.

## API references

* [Configuration and channel readiness](/docs/api/identities/companion)
* [Visible conversation state and reply readiness](/docs/api/identities/companion-conversations)
* [Paginated activation history](/docs/api/identities/companion-history)
* [Webhook metadata and recovery](/docs/webhooks#companion-conversation-events)
