Skip to content
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,17 @@ Then invite the app to the channel and pair in that channel/thread with `relay p
| pause delivery | `/pause` | `relay pause` | `/relay pause` |
| resume delivery | `/resume` | `relay resume` | `/relay resume` |
| disconnect binding | `/disconnect` | `relay disconnect` | `/relay disconnect` |
| create delegation task (opt-in shared rooms) | `/delegate <machine\|#capability> <goal>` | `relay delegate <machine\|#capability> <goal>` | `/relay delegate <machine\|#capability> <goal>` |
| control delegation task | `/task@<bot_username> <claim\|decline\|cancel\|status\|history> [task-id]` (or `/task <claim\|decline\|cancel\|status\|history> [task-id]` in private/other clients) | `relay task <claim\|decline\|cancel\|status\|history> [task-id]` | `/relay task <claim\|decline\|cancel\|status\|history> [task-id]` |

`quiet`, `normal`, `verbose`, and `completion-only` are valid progress modes. In quiet mode PiRelay keeps terminal notifications concise and offers `/full`/download actions for the full answer. In normal, verbose, and completion-only modes it sends the full final answer, splitting by paragraphs within platform limits and falling back to a Markdown document when an adapter supports files and the output is too large for a reasonable chat burst.

Remote `/disconnect` is scoped to the requesting chat/conversation only: it revokes that Telegram, Discord, or Slack binding and suppresses future session output/buttons there, without disconnecting other messengers that remain paired to the same Pi session. Local `/relay disconnect` is broader and disconnects the current session from all paired messenger bindings.

Remote `send-file` is requester-scoped: an authorized Telegram/Discord/Slack user may request a workspace-relative, validated path and PiRelay uploads it only back to that same conversation/thread. Targeted fan-out remains local-only via `/relay send-file <messenger|messenger:instance|all> <relative-path> [caption]`; remote forms must not include messenger targets such as `all` or `slack`.

Agent delegation is disabled by default and only applies in explicitly enabled shared rooms. Delegation task cards are visible room messages; bot-authored ordinary output remains inert, peer-bot trust is configured separately from human allow-lists, and claimed work is injected as a bounded delegated-task prompt with completion/failure reported back to the room.

## Prompt routing behavior

### When Pi is idle
Expand Down
4 changes: 3 additions & 1 deletion docs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ Run one PiRelay broker per machine. If the same bot/account is configured on mul

Discord has an opt-in live DM-first runtime backed by the Discord adapter and live client operations. Slack remains a DM-first foundation with mockable platform operations. Adapters normalize direct-message text, action callbacks, files/images, identity metadata, and platform limits into shared relay contracts. Discord guild messages and Slack channel events remain rejected by default unless explicitly enabled.

Shared-room parity is tracked in `docs/shared-room-parity.md`. Telegram supports addressed group commands and optional Bot-to-Bot Communication Mode when both BotFather bots enable it. Discord has the closest runtime parity for shared guild channels. Slack can parse app mentions, but ordinary channel text, channel-command fallback, and media routing are intentionally reported as unsupported until Slack gets Discord-like pre-routing.
Shared-room parity is tracked in `docs/shared-room-parity.md`. Telegram supports addressed group commands and optional Bot-to-Bot Communication Mode when both BotFather bots enable it. Discord supports gated guild-channel shared rooms with text fallbacks, mentions, buttons, and delegation task cards. Slack supports explicitly enabled and paired channel/thread control with app mentions, ordinary text, `relay <command>` fallbacks, and delegation text cards; media parity remains narrower than private-chat flows.

Agent delegation is handled above adapter-specific ingress as a structured task-card/control surface. Adapters may expose buttons or text fallbacks, but they must preserve sender bot metadata, reject untrusted peers before prompt injection, ignore local-bot/self-authored events, and keep arbitrary bot-authored output inert.

## Future adapters

Expand Down
26 changes: 25 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,31 @@ For no-federation shared rooms, use one dedicated bot/app identity per machine i

- Telegram: invite each machine bot to the group/supergroup. Enable BotFather Bot-to-Bot Communication Mode for both bots only when testing bot-authored workflows; `/command@bot` addressed commands remain the reliable privacy-mode fallback.
- Discord: enable guild-channel shared rooms only with dedicated applications, allowed guild ids, Message Content Intent, and channel permissions. Prefer `relay <command>` and mentions over platform slash-command assumptions.
- Slack: channel events and app mentions can be configured, but Slack shared-room ordinary text/channel command/media pre-routing is diagnostic/deferred until explicit runtime support exists. Keep channel control disabled unless you are testing that gap deliberately.
- Slack: channel events, app mentions, ordinary channel text, and `relay <command>` fallbacks are supported only after `allowChannelMessages`, shared-room enablement, app invitation, and explicit channel pairing are configured.

Agent delegation is disabled by default. Enable it per messenger instance with a `delegation` block, for example:

```json
{
"relay": { "machineId": "laptop", "capabilities": ["linux-tests"] },
"messengers": {
"discord": {
"default": {
"sharedRoom": { "enabled": true },
"delegation": {
"enabled": true,
"autonomy": "propose-only",
"trustedPeers": [
{ "peerId": "1234567890", "allowCreate": true, "targetMachineIds": ["laptop"] }
]
}
}
}
}
}
```

Supported autonomy levels are `off`, `propose-only`, `auto-claim-targeted`, and `auto-claim-safe-capability`. Peer-bot trust is separate from human `allowUserIds`; do not put tokens, hidden prompts, transcripts, or raw tool inputs in delegation task goals.

See `docs/shared-room-parity.md` for the current parity matrix.

Expand Down
10 changes: 6 additions & 4 deletions docs/shared-room-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

PiRelay shared rooms use one dedicated machine bot/app per Pi machine in the same messenger room. Defaults remain conservative: private chats are always the safest pairing surface, and shared-room control must be explicitly enabled per platform.

Agent delegation is an additional opt-in layer for shared rooms. When enabled, authorized humans or trusted peer bots can create visible task cards with `/delegate <machine|#capability> <goal>` and control them with `/task <claim|decline|cancel|status|history> [task-id]`. Bot-authored ordinary output remains inert; only validated delegation commands/actions are machine-actionable.

## Telegram

- Private chats: supported.
Expand All @@ -21,13 +23,13 @@ PiRelay shared rooms use one dedicated machine bot/app per Pi machine in the sam
## Slack

- Private chats: supported.
- Shared rooms: partially declared at adapter level, but Discord-like channel pre-routing is not yet runtime-parity.
- Shared rooms: supported for explicitly enabled channel/thread control when `allowChannelMessages`, shared-room enablement, app membership, and user authorization are configured.
- App mentions: detected and classified as local, remote, or ambiguous by user ID.
- Deferred/diagnostic-only shared-room surfaces: ordinary channel text, channel command fallback, and media attachments in shared rooms. Diagnostics should say this explicitly instead of implying full Telegram/Discord parity.
- Safe default: keep channel control disabled unless an implementation adds explicit pre-routing, authorization, active selection, non-target silence, and safe response handling.
- Ordinary channel text and `relay <command>` fallback: supported after channel pairing and active-selection checks; non-target machine bots stay silent.
- Safe default: keep channel control disabled unless the app is installed in the intended room with explicit user allow-lists and a tested pairing path.

## Capability summary

- Telegram: mentions/replies/platform addressed commands/media supported; ordinary text depends on group privacy and permissions.
- Discord: ordinary text/mentions/platform text prefix/media supported when guild channel mode is enabled and authorized.
- Slack: mentions can be parsed; ordinary text/platform commands/media shared-room routing is intentionally marked unsupported until Slack runtime pre-routing is implemented.
- Slack: app mentions, ordinary text, and platform command fallbacks are supported for explicitly enabled and paired channel/thread control; media parity remains narrower than private-chat flows.
30 changes: 24 additions & 6 deletions docs/slack-live-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Set these variables only in a secure local shell or CI secret store:
export PI_RELAY_SLACK_LIVE_ENABLED=true
export PI_RELAY_SLACK_LIVE_WORKSPACE_ID=T123...
export PI_RELAY_SLACK_LIVE_CHANNEL_ID=C123... # or G123... for private channels
export PI_RELAY_SLACK_LIVE_AUTHORIZED_USER_ID=U123...
export PI_RELAY_SLACK_LIVE_AUTHORIZED_USER_ID=U123... # must be your Slack user ID (the sender of manual commands)
export PI_RELAY_SLACK_LIVE_DRIVER_TOKEN=xoxp-or-test-driver-token
export PI_RELAY_SLACK_LIVE_EVENT_MODE=socket # default; use webhook only with external delivery
export PI_RELAY_SLACK_LIVE_REAL_AGENT=false # set true for real LLM-backed Pi agent runs
Expand All @@ -66,20 +66,38 @@ Optional:

```bash
export PI_RELAY_SLACK_LIVE_TIMEOUT_MS=120000 # defaults to 300000 when PI_RELAY_SLACK_LIVE_REAL_AGENT=true
export PI_RELAY_SLACK_LIVE_BOT_A_INSTANCE_ID=slack-live-a
export PI_RELAY_SLACK_LIVE_BOT_B_INSTANCE_ID=slack-live-b
export PI_RELAY_SLACK_LIVE_BOT_A_DISPLAY_NAME='PiRelay Slack A'
export PI_RELAY_SLACK_LIVE_BOT_B_DISPLAY_NAME='PiRelay Slack B'
export PI_RELAY_SLACK_LIVE_BOT_A_INSTANCE_ID=pirelay__mini_ # machine id used in relay delegate machine arguments
export PI_RELAY_SLACK_LIVE_BOT_B_INSTANCE_ID=pirelay__work_ # machine id used in relay delegate machine arguments
export PI_RELAY_SLACK_LIVE_BOT_A_DISPLAY_NAME='pirelay__mini_'
export PI_RELAY_SLACK_LIVE_BOT_B_DISPLAY_NAME='pirelay__work_'
export PI_RELAY_SLACK_LIVE_DELEGATION_ENABLED=false # set true to enable delegation-only live coverage
export PI_RELAY_SLACK_LIVE_DELEGATION_AUTONOMY=auto-claim-targeted
export PI_RELAY_SLACK_LIVE_DELEGATION_REQUIRE_HUMAN_APPROVAL=false
export PI_RELAY_SLACK_LIVE_DELEGATION_MANUAL=false # optional interactive/manual message-post mode for local runs

# If your Slack bots now have different names, update *_INSTANCE_ID (and *_DISPLAY_NAME for log readability)
# so the printed commands line up with the names you want to target.
```

The harness writes per-instance config files under a temporary directory, points each Pi process at a distinct `PI_RELAY_CONFIG`/`PI_RELAY_STATE_DIR`, and passes the relevant Slack token/signing-secret/app-level token values via environment variables. Temporary state is deleted during teardown so repeated runs do not reuse stale local bindings. The live harness enables a test-only pre-seeded binding path for its disposable channel so targeted prompts exercise real runtime prompt routing and completion notifications without committing pairing codes. Set `PI_RELAY_SLACK_LIVE_REAL_AGENT=true` when `PI_RELAY_SLACK_LIVE_BOT_A_PI_COMMAND` and `PI_RELAY_SLACK_LIVE_BOT_B_PI_COMMAND` launch real LLM-backed Pi agents; this switches the prompt wording to an explicit marker-only instruction and increases the default timeout to five minutes while still asserting only that the marker appears. Production Socket Mode uses the same token shape: a bot token (`xoxb-...`) plus an app-level token (`xapp-...`) with `connections:write`. Prefer namespaced PiRelay config (`tokenEnv`, `signingSecretEnv`, and `appTokenEnv`) for non-test runs; `PI_RELAY_SLACK_BOT_USER_ID`/`slack.botUserId` is only a non-secret fallback when startup `auth.test` discovery is unavailable. The live harness also enables the bounded history-polling fallback for diagnostics, but production prompt routing should use Socket Mode events.
The harness writes per-instance config files under a temporary directory, points each Pi process at a distinct `PI_RELAY_CONFIG`/`PI_RELAY_STATE_DIR`, and passes the relevant Slack token/signing-secret/app-level token values via environment variables. Temporary state is deleted during teardown so repeated runs do not reuse stale local bindings. The live harness enables a test-only pre-seeded binding path for its disposable channel so targeted prompts exercise real runtime prompt routing and completion notifications without committing pairing codes. Set `PI_RELAY_SLACK_LIVE_REAL_AGENT=true` when `PI_RELAY_SLACK_LIVE_BOT_A_PI_COMMAND` and `PI_RELAY_SLACK_LIVE_BOT_B_PI_COMMAND` launch real LLM-backed Pi agents; this switches the prompt wording to an explicit marker-only instruction and increases the default timeout to five minutes while still asserting only that the marker appears. In that mode, per-instance broker namespaces are also enabled so multiple bot apps can run on the same machine without session collisions. `PI_RELAY_SLACK_LIVE_REAL_AGENT=false` uses lightweight stub-style sessions intended for command-routing checks, so `/status` output may show offline even while delegation messaging still works for live validation. Production Socket Mode uses the same token shape: a bot token (`xoxb-...`) plus an app-level token (`xapp-...`) with `connections:write`. Prefer namespaced PiRelay config (`tokenEnv`, `signingSecretEnv`, and `appTokenEnv`) for non-test runs; `PI_RELAY_SLACK_BOT_USER_ID`/`slack.botUserId` is only a non-secret fallback when startup `auth.test` discovery is unavailable. The live harness also enables the bounded history-polling fallback for diagnostics, but production prompt routing should use Socket Mode events.

## Running locally

```bash
npm run test -- tests/slack-live-integration.test.ts
./run-slack-live-test.sh
```

`run-slack-live-test.sh` is parameterizable. Use the `--delegation` option (and optional `--test`) for the new live delegation suite:

```bash
./run-slack-live-test.sh --delegation
./run-slack-live-test.sh --test tests/slack-live-delegation.test.ts --delegation
./run-slack-live-test.sh --delegation --manual-delegation --test tests/slack-live-delegation.test.ts
```

In manual mode, the test prints the exact machine ID and display name it expects you to target, plus the command examples, so you can copy-paste directly into Slack. Delegation task cards use Slack buttons for claim, decline, cancel, and status actions when callbacks are available; the card text also includes `relay task ...` fallback commands for manual copy-paste or environments where button callbacks are unavailable. In real-agent mode, the delegation test waits for a completed task card with a bounded `Result` summary rather than stopping at the running handoff card.

When `PI_RELAY_SLACK_LIVE_ENABLED` or required credentials are absent, the test is skipped and prints which configuration is missing. The normal `npm test` run is safe without live Slack secrets.

## CI guidance
Expand Down
3 changes: 3 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ These adapter foundations are DM-first and use channel-specific credentials/conf
25. Pair one Pi session to Telegram and Slack or Discord, send remote `/disconnect` from only one conversation, then complete a Pi turn from the still-paired messenger; verify the disconnected conversation receives no completion, progress, image/file, or button output while `/sessions` still returns safe no-paired-session guidance there.
26. Repeat the previous step while a turn is already running; verify a disconnect racing with completion suppresses output to the revoked conversation.
27. From local Pi, run `/relay disconnect` and verify all Telegram, Discord, and Slack bindings for the current session are revoked and no messenger can continue controlling that session until re-paired.
28. In a shared room with delegation enabled, send `/delegate <machine> run a harmless status check`; verify a visible task card appears, non-target machine bots stay silent, `/task claim <id>` injects a bounded delegated-task prompt only into the claimant session, and completion is reported back to the room.
29. Repeat with an untrusted bot-authored `/delegate` message and verify no task is created, no prompt is injected, and no media/download/callback side effects occur.
30. Repeat with a trusted peer bot targeting a different machine and verify the local broker stays silent except for local observation state.

## 7. Optional Telegram two-bot shared-room smoke checklist

Expand Down
10 changes: 6 additions & 4 deletions extensions/relay/adapters/discord/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
import { assertCanSendOutboundFile, channelTextChunks, decodeOutboundFileData } from "../../core/channel-adapter.js";
import type { DiscordRelayConfig } from "../../core/types.js";
import type { SharedRoomAddressing } from "../../core/shared-room.js";
import { parseDelegationInvocation } from "../../commands/delegation.js";

export interface DiscordApiOperations {
connect?(handler: (event: DiscordGatewayEvent) => Promise<void>): Promise<void>;
Expand Down Expand Up @@ -216,8 +217,9 @@ export function discordGatewayEventToChannelEvent(event: DiscordGatewayEvent, co
: discordMessageToChannelEvent(event.payload as DiscordMessagePayload, config);
}

export function discordMessageToChannelEvent(message: DiscordMessagePayload, config: Pick<DiscordRelayConfig, "allowedImageMimeTypes" | "maxFileBytes" | "applicationId" | "clientId">): ChannelInboundMessage | undefined {
if (message.author.bot || message.webhook_id) return undefined;
export function discordMessageToChannelEvent(message: DiscordMessagePayload, config: Pick<DiscordRelayConfig, "allowedImageMimeTypes" | "maxFileBytes" | "applicationId" | "clientId" | "delegation">): ChannelInboundMessage | undefined {
if (message.webhook_id) return undefined;
if (message.author.bot && (!config.delegation?.enabled || !parseDelegationInvocation(message.content ?? "", { prefixes: ["relay"] }))) return undefined;
Comment thread
zikolach marked this conversation as resolved.
const conversation = discordConversation(message.channel_id, message.guild_id);
const sender = discordIdentity(message.author, message.guild_id);
return {
Expand Down Expand Up @@ -326,13 +328,13 @@ function discordConversation(channelId: string, guildId?: string): ChannelConver
};
}

function discordIdentity(user: { id: string; username?: string; global_name?: string; discriminator?: string }, guildId?: string): ChannelIdentity {
function discordIdentity(user: { id: string; username?: string; global_name?: string; discriminator?: string; bot?: boolean }, guildId?: string): ChannelIdentity {
return {
channel: DISCORD_CHANNEL,
userId: user.id,
username: user.username,
displayName: user.global_name ?? user.username,
metadata: { discriminator: user.discriminator, guildId },
metadata: { discriminator: user.discriminator, guildId, isBot: user.bot === true },
};
}

Expand Down
Loading