Skip to content
Draft
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ https://pushassistant.com/
Good coding agents should be useful beyond an open terminal.

Push makes the agent you already trust available through iMessage, Telegram,
or Slack. It can answer a message, continue a conversation, or run a Markdown
Slack, or Missive. It can answer a message, continue a conversation, or run a Markdown
job on a schedule. Give it clear context and a useful set of jobs, and it can
act as your AI chief of staff. Your assistant files stay in a Git repository
you own.
Expand All @@ -46,7 +46,7 @@ schedules to Claude Code, Codex, or Pi.

```mermaid
flowchart TD
Message["Message from you<br/>iMessage · Telegram · Slack"]
Message["Message from you<br/>iMessage · Telegram · Slack · Missive"]
Jobs["Scheduled<br/>Markdown jobs"]
Repo["Assistant repository<br/>SOUL.md · context · jobs"]
Push["Push<br/>message gateway · scheduler · history"]
Expand All @@ -63,7 +63,7 @@ flowchart TD
## What it does

- Runs on your Mac or Linux machine
- Connects private iMessage, Telegram, and Slack chats
- Connects private iMessage, Telegram, Slack, and Missive conversations
- Uses your existing Claude Code, Codex, or Pi setup
- Keeps conversations and job history between restarts
- Runs one-off or scheduled Markdown jobs
Expand Down
8 changes: 8 additions & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ allow_user_ids = [123456789]
# bot_token = "xoxb-..." # Prefer SLACK_BOT_TOKEN.
# allow_user_ids = ["U012ABCDEF"]

# Missive alternative. See docs/missive.md.
# channel = "missive"
# poll_interval = "3s" # At least one second per configured conversation.
# [missive]
# api_token = "..." # Prefer MISSIVE_API_TOKEN.
# conversation_ids = ["00000000-0000-0000-0000-000000000000"]
# allow_user_ids = ["00000000-0000-0000-0000-000000000000"]

# Required when a job contains an enabled cron trigger.
# [primary_delivery]
# channel = "telegram"
Expand Down
23 changes: 15 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Push Architecture

Push is one local Rust process. It receives from configured iMessage, Telegram,
and Slack channels, filters messages, loads the configured assistant repository,
Slack, and Missive channels, filters messages, loads the configured assistant repository,
runs a configured agent backend, and sends the
final reply.

Expand Down Expand Up @@ -55,7 +55,8 @@ Those belong to the selected backend.

Push polls channel adapters and shells out to local agent commands. It opens no
server port and accepts no inbound network connection. Telegram uses outbound
HTTPS long polling and Slack uses an outbound Socket Mode WebSocket.
HTTPS long polling, Slack uses an outbound Socket Mode WebSocket, and Missive
uses outbound REST polling.

The trust boundary is the messaging account plus the configured channel
allowlist.
Expand All @@ -67,9 +68,11 @@ flowchart LR
user([You]) -->|iMessage| db[(chat.db)]
user -->|private chat| tg[Telegram Bot API]
user -->|app DM| slack[Slack]
user -->|comment| missive[Missive REST API]
db -->|poll| push
tg -->|long poll| push
slack -->|Socket Mode| push
missive -->|poll comments| push
subgraph push[Push gateway]
poller[Channel poller] --> gateway[Gateway loop]
gateway --> worker[Per-thread worker]
Expand All @@ -88,15 +91,17 @@ flowchart LR
sender -->|osascript| db
sender -->|sendMessage| tg
sender -->|chat.postMessage| slack
sender -->|create post| missive
db -->|reply| user
tg -->|reply| user
slack -->|reply| user
missive -->|reply| user
```

## Channel Boundary

The gateway depends on one closed, compile-time channel contract. iMessage,
Telegram, and Slack implement it, and the `Channel` enum provides static dispatch. This is
Telegram, Slack, and Missive implement it, and the `Channel` enum provides static dispatch. This is
an internal Rust boundary, not a dynamic plugin system or a configuration
extension point.

Expand All @@ -111,14 +116,15 @@ Each channel implementation owns these semantics:
reply target, approval sender/chat identity, and ordered route-key groups. Telegram
topics inherit a parent-chat route; iMessage retains its legacy unprefixed
route aliases. Slack keeps one workspace-scoped DM identity while targeting
the exact originating Slack message thread.
the exact originating Slack message thread. Missive keeps one thread per
allowlisted conversation.
- **Outbound delivery:** validate proactive targets, plan durable chunks, and
send one chunk to the exact accepted target. Replies never cross from one
channel loop to another.
- **Typing:** declare an optional refresh interval and send best-effort activity
updates. Typing failures do not fail an assistant turn.
- **Rich messages:** choose plain or rich delivery per chunk. iMessage keeps one
plain marked reply. Telegram and Slack own their formatting, size limits,
plain marked reply. Telegram, Slack, and Missive own their formatting, size limits,
splitting, and provider-specific addressing.
- **Retry:** expose the timeout and bounded retry cadence used for stored chat
replies. A send call is one attempt. Generated output is persisted before
Expand Down Expand Up @@ -271,7 +277,7 @@ accepted input survives a process restart without relying on an in-memory
WebSocket buffer. Ignored envelopes retain redacted rejection metadata for the
audit path without retaining message content.

With advanced `channels = ["imessage", "telegram", "slack"]` configuration, one
With advanced `channels = ["imessage", "telegram", "slack", "missive"]` configuration, one
coordinator starts an independent polling loop, acknowledgement tracker, and
thread queue map for each enabled provider. The loops share one locked state
store, canonical history database, backend runner set, and serialized audit log.
Expand Down Expand Up @@ -400,8 +406,9 @@ backend session.
An allowed inbound message can cause an agent to run tools. The sender filter is
the trust boundary. iMessage uses `imessage.self_handles` and
`imessage.allow_from`; Telegram uses stable numeric `telegram.allow_user_ids`
and `telegram.allow_chat_ids`; Slack uses stable `slack.allow_user_ids` member
IDs and verifies the authenticated workspace.
and `telegram.allow_chat_ids`. Slack uses stable `slack.allow_user_ids` member
IDs and verifies the authenticated workspace. Missive requires both
`missive.conversation_ids` and stable `missive.allow_user_ids`.

Push preserves sandbox, approval, permission-mode, and tool-list settings for
chats. Codex and Claude jobs bypass interactive permissions so unattended work
Expand Down
27 changes: 26 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ Slack requires both `SLACK_APP_TOKEN` and `SLACK_BOT_TOKEN`, or the matching
`slack.app_token` and `slack.bot_token` values in the private config. At least
one exact Slack member ID is required. See the [Slack guide](slack.md).

### Missive

```toml
poll_interval = "3s"

[missive]
conversation_ids = ["00000000-0000-0000-0000-000000000000"]
allow_user_ids = ["00000000-0000-0000-0000-000000000000"]
```

Missive requires `MISSIVE_API_TOKEN`, or `missive.api_token` in a private
config. Both an explicit conversation allowlist and an explicit user allowlist
are required. The polling interval must allow at least one second per configured
conversation. See the [Missive guide](missive.md).

Telegram voice notes are optional. Configure the shared voice provider with:

```toml
Expand All @@ -122,7 +137,7 @@ See [Voice Messages](telegram.md#voice-messages).
Use `channels` instead of `channel`:

```toml
channels = ["imessage", "telegram", "slack"]
channels = ["imessage", "telegram", "slack", "missive"]
agent = "codex"

[imessage]
Expand All @@ -145,6 +160,7 @@ provider does not stop the other.
must be enabled and the target must appear in that channel's allowlist.
Telegram topic targets use `"<chat-id>:<topic-id>"`.
Slack primary targets use an allowlisted member ID such as `U012ABCDEF`.
Missive primary targets use an allowlisted conversation ID.

## Routing

Expand Down Expand Up @@ -174,6 +190,7 @@ Thread keys are:
- `telegram:dm:<chat-id>`
- `telegram:dm:<chat-id>:topic:<topic-id>`
- `slack:dm:<workspace-id>:<dm-channel-id>`
- `missive:conversation:<conversation-id>`

## Agent permissions

Expand Down Expand Up @@ -218,6 +235,14 @@ requests. Review [permissions and security](security.md) before enabling jobs.
| `slack.bot_token` | `SLACK_BOT_TOKEN` fallback | Bot token used for `auth.test`, replies, and progress |
| `slack.allow_user_ids` | `[]` | Trusted stable Slack member IDs |

### Missive

| Setting | Default | Purpose |
| --- | --- | --- |
| `missive.api_token` | `MISSIVE_API_TOKEN` fallback | API token; the environment value is used when this is omitted |
| `missive.conversation_ids` | `[]` | Exact conversations whose comments Push may poll and reply to |
| `missive.allow_user_ids` | `[]` | Trusted stable Missive comment-author IDs |

### Voice

| Setting | Default | Purpose |
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ cargo test --locked
| CLI and startup | `src/main.rs` |
| TOML loading and validation | `src/config.rs` |
| Channel-neutral boundary | `src/channel.rs` |
| iMessage, Telegram, and Slack adapters | `src/imessage/`, `src/telegram.rs`, `src/slack.rs` |
| iMessage, Telegram, Slack, and Missive adapters | `src/imessage/`, `src/telegram.rs`, `src/slack.rs`, `src/missive.rs` |
| Gateway, queues, workers, delivery | `src/gateway/` |
| Claude Code, Codex, and Pi adapters | `src/claude.rs`, `src/codex.rs`, `src/pi.rs` |
| Canonical SQLite history | `src/history.rs` |
Expand Down
25 changes: 23 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Quickstart

This guide gets one private chat working with one coding-agent backend. Start
with Telegram or Slack on macOS or Linux, or iMessage on macOS. Add multiple channels,
with Telegram, Slack, or Missive on macOS or Linux, or iMessage on macOS. Add multiple channels,
routes, and scheduled jobs after the basic path passes `push doctor`.

## 1. Check the requirements

You need:

- Apple Silicon macOS or x86_64 Linux for the current prebuilt release
- macOS for iMessage, or macOS/Linux for Telegram or Slack
- macOS for iMessage, or macOS/Linux for Telegram, Slack, or Missive
- Claude Code, Codex, or Pi installed, authenticated, and runnable by the same
user that will run Push
- Git for the assistant repository created by `push init`
Expand Down Expand Up @@ -142,6 +142,27 @@ practical structure for identity, context, shared skills, jobs, and evals.
Read the [Slack guide](slack.md) for app setup, scopes, token storage,
filtering, and recovery behavior.

=== "Missive"

Create a Missive API token, choose one private conversation, and collect
the stable IDs for that conversation and the trusted comment author. Put
the token in the service environment, then edit `~/.push/config.toml`:

```toml
channel = "missive"
agent = "codex"
assistant_root = "~/Code/assistant"
poll_interval = "3s"

[missive]
conversation_ids = ["00000000-0000-0000-0000-000000000000"]
allow_user_ids = ["00000000-0000-0000-0000-000000000000"]
```

Set `MISSIVE_API_TOKEN` for the service. Read the [Missive
guide](missive.md) for the comments-only command model, rate limit,
allowlists, and first-run behavior.

Replace `codex` with `claude` for Claude Code or `pi` for Pi. Pi must already
have a configured model provider or authenticated account for the service user.

Expand Down
9 changes: 5 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ hide:
# Build your own 24/7 AI chief of staff.

Push turns Claude Code, Codex, or Pi into an always-on personal assistant.
Message it from iMessage, Telegram, or Slack, give it recurring jobs, and let it
Message it from iMessage, Telegram, Slack, or Missive, give it recurring jobs, and let it
handle work in the background.

<p class="push-actions">
Expand Down Expand Up @@ -92,7 +92,7 @@ sends the result back when the work is done.
## One lightweight bridge. Your agent does the work.

<div class="push-steps" markdown="0">
<div><span>01</span><strong>Message your assistant</strong><p>Use iMessage, Telegram, or Slack from wherever you are.</p></div>
<div><span>01</span><strong>Message your assistant</strong><p>Use iMessage, Telegram, Slack, or Missive from wherever you are.</p></div>
<div><span>02</span><strong>Push starts the work</strong><p>It restores the conversation and runs your chosen coding agent in the background.</p></div>
<div><span>03</span><strong>Get the result</strong><p>Push saves the response and sends it back to the same chat.</p></div>
</div>
Expand Down Expand Up @@ -128,8 +128,9 @@ finish without an operator.

---

Set up private [iMessage](channels/imessage.md), [Telegram](telegram.md), or
[Slack](slack.md) conversations with narrow sender allowlists.
Set up private [iMessage](channels/imessage.md), [Telegram](telegram.md),
[Slack](slack.md), or [Missive](missive.md) conversations with narrow
sender allowlists.

[:octicons-arrow-right-24: Configure channels](configuration.md#channels)

Expand Down
69 changes: 69 additions & 0 deletions docs/missive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Missive

Push can use comments in explicitly allowlisted Missive conversations as a
private command channel. It polls Missive's REST API over outbound HTTPS and
returns each assistant reply as a Missive post in the same conversation. It
does not expose a webhook, send email, or create a draft.

## Configure access

Create a Missive API token for the account that will run Push, then put it in
the service environment:

```sh
export MISSIVE_API_TOKEN="..."
```

Choose the exact conversation IDs Push may read and the exact Missive user IDs
whose comments may invoke the assistant:

```toml
channel = "missive"
agent = "codex"
assistant_root = "~/Code/assistant"
poll_interval = "3s"

[missive]
conversation_ids = ["00000000-0000-0000-0000-000000000000"]
allow_user_ids = ["00000000-0000-0000-0000-000000000000"]
```

You may set `missive.api_token` in a private config instead, but the environment
variable is safer for a service. Push rejects an inline Missive token when the
config is inside the Git-versioned assistant repository.

Missive limits continuous polling to one request per second. Push makes one
request per configured conversation during a normal poll, so
`poll_interval` must be at least one second for every configured conversation.
For example, three conversations require `poll_interval = "3s"` or longer.

## Message behavior

- Only new comments are commands. Other email or chat activity is ignored.
- Both the conversation ID and comment author ID must be allowlisted.
- The first successful startup records current comments and does not replay
them into the assistant.
- Stable Missive comment IDs are deduplicated in
`<state_path>.missive-inbox.db` before dispatch.
- Rejected comment content is cleared before it is stored locally.
- Replies are Markdown posts in the exact originating conversation. They do
not send or modify the underlying email.
- Posts longer than Missive's 8,000-character Markdown limit are split into
durable chunks.

Run the normal preflight after configuring the channel:

```sh
push doctor
push
```

To use scheduled delivery, the target is one configured conversation ID:

```toml
[primary_delivery]
channel = "missive"
target = "00000000-0000-0000-0000-000000000000"
```

Missive voice messages and typing indicators are not supported.
6 changes: 5 additions & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ Keep these allowlists narrow:
- `imessage.self_handles` and `imessage.allow_from`
- `telegram.allow_user_ids` and `telegram.allow_chat_ids`
- `slack.allow_user_ids`
- `missive.conversation_ids` and `missive.allow_user_ids`

Use stable numeric Telegram IDs. Usernames are mutable and are not accepted as
security identities. Treat a lost phone, shared messaging account, or
compromised allowed sender as access to the assistant.
Slack allowlists use stable member IDs, never mutable display names.
Missive requires both the exact conversation and the stable comment-author ID;
it never treats an email address or display name as authorization.

## Agent permissions

Expand Down Expand Up @@ -70,7 +73,7 @@ Keep them on local durable storage with permissions restricted to the service
user. Keep the assistant directory in its own private Git repository. Never
put real config secrets, state, audit logs, or databases in
that repository. An explicit `assistant_root` config stored inside it cannot
contain inline Telegram, Slack, or OpenAI credentials; use the matching environment
contain inline Telegram, Slack, Missive, or OpenAI credentials; use the matching environment
variable or move the config outside. When `voice.openai_api_key` is configured,
`push doctor` requires the config file to be private on Unix:

Expand All @@ -83,6 +86,7 @@ chmod 600 ~/.push/config.toml
Push opens no inbound server port. iMessage reads local state. Telegram uses
outbound HTTPS long polling and Slack uses an authenticated outbound Socket
Mode WebSocket. Neither needs a webhook.
Missive uses outbound HTTPS polling and likewise needs no webhook.
This reduces exposure, but it does not make an allowed message harmless.

## Durable questions
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ nav:
- iMessage: channels/imessage.md
- Telegram: telegram.md
- Slack: slack.md
- Missive: missive.md
- Use Push:
- Jobs and schedules: jobs.md
- Permissions and security: security.md
Expand Down
Loading