@nordbyte/nordrelay 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -0
- package/README.md +67 -6
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +77 -6
- package/dist/channel-adapter.js +11 -5
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +214 -1
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +15 -0
- package/dist/config.js +31 -6
- package/dist/context-key.js +10 -0
- package/dist/discord-bot.js +85 -26
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +20 -0
- package/dist/metrics.js +46 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +72 -274
- package/dist/remote-prompt.js +98 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-general-commands.js +14 -0
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/web-api-contract.js +8 -0
- package/dist/web-dashboard-pages.js +12 -0
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +12 -0
- package/dist/webui-assets/dashboard.js +427 -14
- package/package.json +3 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +373 -7
package/.env.example
CHANGED
|
@@ -264,6 +264,23 @@ NORDRELAY_VERSION_CACHE_TTL_MS=3600000
|
|
|
264
264
|
# Installed agent CLI version cache TTL.
|
|
265
265
|
NORDRELAY_CLI_VERSION_CACHE_TTL_MS=60000
|
|
266
266
|
|
|
267
|
+
# Peers
|
|
268
|
+
# Optional NordRelay-to-NordRelay federation. Pairing is explicit, authenticated, scoped, and TLS-protected.
|
|
269
|
+
# Expose the dedicated authenticated NordRelay peer API.
|
|
270
|
+
NORDRELAY_PEER_ENABLED=false
|
|
271
|
+
# Human-readable name shown to paired NordRelay instances.
|
|
272
|
+
NORDRELAY_PEER_NAME=
|
|
273
|
+
# Bind host for the peer API. Use 127.0.0.1 for local-only or a LAN/interface IP when explicitly exposing peers.
|
|
274
|
+
NORDRELAY_PEER_HOST=127.0.0.1
|
|
275
|
+
# Port for the peer API.
|
|
276
|
+
NORDRELAY_PEER_PORT=31979
|
|
277
|
+
# Optional public URL other instances should use for this node.
|
|
278
|
+
NORDRELAY_PEER_PUBLIC_URL=
|
|
279
|
+
# Serve the peer API over HTTPS with an automatically generated local certificate.
|
|
280
|
+
NORDRELAY_PEER_TLS_ENABLED=true
|
|
281
|
+
# Reject plaintext peer serving on non-loopback hosts.
|
|
282
|
+
NORDRELAY_PEER_REQUIRE_TLS=true
|
|
283
|
+
|
|
267
284
|
# Voice
|
|
268
285
|
# Optional voice transcription settings.
|
|
269
286
|
# Whisper fallback API key.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# NordRelay
|
|
2
2
|
|
|
3
|
-
NordRelay is a remote control plane for coding agents across messaging channels. The current implementation connects Codex, Pi, Hermes, OpenClaw, and Claude Code coding-agent sessions to Telegram and
|
|
3
|
+
NordRelay is a remote control plane for coding agents across messaging channels and paired NordRelay instances. The current implementation connects Codex, Pi, Hermes, OpenClaw, and Claude Code coding-agent sessions to Telegram, Discord, the WebUI, and trusted peer nodes, keeps independent sessions per chat, thread, forum topic, DM, or remote target, streams replies and tool activity back to the active channel, supports files, photos, voice input, model controls, session browsing, retry/abort, CLI handback, and scoped multi-host control.
|
|
4
4
|
|
|
5
5
|
The repo is both a local Codex marketplace and a standalone Node app. The plugin lives in `plugins/nordrelay/`; the full bot runtime lives in `src/` and uses `@openai/codex-sdk` for Codex, Pi RPC mode for Pi, the Hermes API Server for Hermes, the OpenClaw Gateway WebSocket RPC surface for OpenClaw, and the Claude Agent SDK for Claude Code.
|
|
6
6
|
|
|
@@ -50,6 +50,17 @@ Adapter architecture:
|
|
|
50
50
|
- `/agents` shows available/planned agent adapters and whether Codex, Pi, Hermes, OpenClaw, and Claude Code are enabled.
|
|
51
51
|
- Shared command-action renderers and a channel runtime contract keep inbound commands, outbound messages, typing, files, inline actions, and streaming-ready delivery separate from channel-specific API calls.
|
|
52
52
|
|
|
53
|
+
Peer federation:
|
|
54
|
+
|
|
55
|
+
- Optional NordRelay-to-NordRelay pairing lets one instance operate agents on trusted Ubuntu, macOS, Windows, LAN, or remote hosts.
|
|
56
|
+
- Peer serving is disabled by default and uses a dedicated API port separate from the dashboard.
|
|
57
|
+
- Pairing requires an explicit one-time invitation code, Ed25519 node identity verification, a per-peer shared secret, request HMAC signatures, timestamp/nonce replay protection, and TLS fingerprint pinning.
|
|
58
|
+
- Peer scopes restrict which remote WebUI/API actions are allowed, including read, prompt, queue, file, diagnostic, log, and session permissions.
|
|
59
|
+
- Peer records can also restrict allowed agent ids, allowed workspace roots, and per-peer workspace aliases such as `app=/srv/app`.
|
|
60
|
+
- The WebUI has a Peers page plus a local/remote target selector; dashboard pages, SSE streaming, queue actions, artifact downloads, health checks, and the global session browser proxy through the selected peer when allowed.
|
|
61
|
+
- Telegram and Discord expose `/peers` and `/target` so a linked user can choose whether prompts run locally or on a paired NordRelay instance.
|
|
62
|
+
- Remote prompts stream text, tool status, turn completion, and errors back to the originating Telegram or Discord context.
|
|
63
|
+
|
|
53
64
|
Codex runtime:
|
|
54
65
|
|
|
55
66
|
- Uses `@openai/codex-sdk` to start, resume, and stream Codex threads.
|
|
@@ -162,7 +173,7 @@ Telegram output:
|
|
|
162
173
|
|
|
163
174
|
Discord input and output:
|
|
164
175
|
|
|
165
|
-
- Enable Discord with `DISCORD_ENABLED=true` and `DISCORD_BOT_TOKEN`.
|
|
176
|
+
- Enable Discord with `DISCORD_ENABLED=true` and `DISCORD_BOT_TOKEN`. If a requested chat adapter is missing its token, NordRelay disables that adapter and keeps running as long as another chat adapter is usable.
|
|
166
177
|
- Set `DISCORD_CLIENT_ID` to let NordRelay register slash commands automatically.
|
|
167
178
|
- `DISCORD_COMMAND_MODE=both` supports slash commands and `/command` text messages. Set it to `slash` if the bot should not read message commands.
|
|
168
179
|
- `DISCORD_MESSAGE_CONTENT_ENABLED=true` lets regular Discord messages become prompts. The matching privileged intent must also be enabled in the Discord Developer Portal.
|
|
@@ -200,6 +211,7 @@ Operations:
|
|
|
200
211
|
- Plugin command/skill starts, stops, restarts, and inspects the connector process.
|
|
201
212
|
- Manual process commands support `start`, `stop`, `restart`, `status`, `update`, and `foreground`.
|
|
202
213
|
- Telegram admin commands support `/logs`, `/diagnostics`, `/support`, `/restart`, and `/update` for NordRelay and agent CLIs.
|
|
214
|
+
- `nordrelay peer identity`, `list`, `invite`, `add`, `test`, and `revoke` manage secure peer federation from the CLI.
|
|
203
215
|
- `nordrelay update`, `/update`, and the WebUI update button detect the install type: npm installs update with `npm install -g @nordbyte/nordrelay@latest`; source checkouts pull `origin/main`, install dependencies, run check, tests, and build, then restart if the connector is running.
|
|
204
216
|
- `/update agents`, `/update <agent>`, `/update install <agent>`, `/update jobs`, `/update log <id>`, `/update cancel <id>`, and `/update input <id> <text>` manage Codex, Pi, Hermes, OpenClaw, and Claude Code updater or installer jobs from Telegram.
|
|
205
217
|
- `/logs` renders redacted connector, NordRelay update, and agent update logs with local-time timestamps, levels, file path, last-modified time, and highlighted warnings/errors.
|
|
@@ -217,7 +229,7 @@ Operations:
|
|
|
217
229
|
- On first WebUI startup without an admin account, NordRelay shows a setup wizard for creating the first admin; remote setup requires the one-time token printed in the server console.
|
|
218
230
|
- The WebUI has responsive header/sidebar/footer navigation, live chat streaming, session controls, queue/artifact/log/diagnostic views, and settings management.
|
|
219
231
|
- The WebUI supports light and dark themes, tabbed settings groups, paginated session browsing, and chat uploads for images, documents, and audio transcription.
|
|
220
|
-
- The WebUI exposes REST and SSE endpoints for chat streaming, sessions, settings, queue, artifacts, logs, health, diagnostics, and redacted diagnostics bundle export.
|
|
232
|
+
- The WebUI exposes REST and SSE endpoints for chat streaming, sessions, settings, queue, artifacts, logs, health, diagnostics, peers, and redacted diagnostics bundle export.
|
|
221
233
|
- The dashboard can bind to `127.0.0.1` or `0.0.0.0`; user login and session cookies are mandatory in both modes.
|
|
222
234
|
- Telegram can run with long polling or an HTTP webhook via `TELEGRAM_TRANSPORT=webhook`.
|
|
223
235
|
- Version freshness checks are cached with `NORDRELAY_VERSION_CACHE_TTL_MS`, and installed agent CLI version checks are cached with `NORDRELAY_CLI_VERSION_CACHE_TTL_MS`, to keep `/version` and adapter health responsive.
|
|
@@ -318,6 +330,33 @@ User and chat access management:
|
|
|
318
330
|
- Telegram group chats are disabled until an admin enables them from the WebUI or runs `/register_chat` inside the group.
|
|
319
331
|
- Discord guild channels are disabled until an admin enables them from the WebUI or runs `/register_channel` inside the channel.
|
|
320
332
|
|
|
333
|
+
Peer setup:
|
|
334
|
+
|
|
335
|
+
1. On each host that should accept peer connections, set `NORDRELAY_PEER_ENABLED=true` in `~/.nordrelay/nordrelay.env`.
|
|
336
|
+
2. Keep `NORDRELAY_PEER_TLS_ENABLED=true` and `NORDRELAY_PEER_REQUIRE_TLS=true` for LAN or internet use.
|
|
337
|
+
3. Use `NORDRELAY_PEER_HOST=127.0.0.1` for local-only testing, a LAN/interface IP for trusted local networks, or keep the peer API behind a TLS reverse proxy/VPN for internet access.
|
|
338
|
+
4. Set `NORDRELAY_PEER_PUBLIC_URL=https://host.example:31979` when other hosts cannot reach the bind address directly.
|
|
339
|
+
5. Restart NordRelay on the accepting host and create an invitation:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
nordrelay peer invite --name workstation --scopes inspect,sessions.read,sessions.write,prompt.send,prompt.abort,queue.read,queue.write,files.read,files.write,diagnostics.read,logs.read
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
6. On the controlling host, run the printed command:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
nordrelay peer add https://workstation.example:31979 --code one-time-code
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
7. Confirm the connection:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
nordrelay peer list
|
|
355
|
+
nordrelay peer test <peer-id>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Use `--workspace-aliases app=/srv/app,demo=/home/me/demo` on invites when a controller should be able to start remote sessions with short workspace names. Use the WebUI Peers page for the same invite, pair, enable/disable, test, alias, global-session, and revoke workflow. Use `/peers` from Telegram or Discord to inspect paired nodes and `/target <peer-id>` or `/target local` to choose where subsequent prompts run.
|
|
359
|
+
|
|
321
360
|
Codex authentication:
|
|
322
361
|
|
|
323
362
|
- Preferred local setup: run `codex login` on the host before starting the connector.
|
|
@@ -403,6 +442,9 @@ nordrelay restart
|
|
|
403
442
|
nordrelay stop
|
|
404
443
|
nordrelay foreground
|
|
405
444
|
nordrelay web
|
|
445
|
+
nordrelay peer list
|
|
446
|
+
nordrelay peer invite
|
|
447
|
+
nordrelay peer add https://peer.example:31979 --code one-time-code
|
|
406
448
|
```
|
|
407
449
|
|
|
408
450
|
Source checkout process commands:
|
|
@@ -417,6 +459,7 @@ node plugins/nordrelay/scripts/nordrelay.mjs foreground
|
|
|
417
459
|
node plugins/nordrelay/scripts/nordrelay.mjs user list
|
|
418
460
|
node plugins/nordrelay/scripts/nordrelay.mjs doctor
|
|
419
461
|
node plugins/nordrelay/scripts/nordrelay.mjs web
|
|
462
|
+
node plugins/nordrelay/scripts/nordrelay.mjs peer list
|
|
420
463
|
```
|
|
421
464
|
|
|
422
465
|
NPM shortcuts:
|
|
@@ -472,6 +515,7 @@ The dashboard is a second NordRelay client next to Telegram. It can:
|
|
|
472
515
|
- Inspect a per-agent capability matrix showing model, reasoning, launch, fast mode, attachments, activity, usage, auth, login/logout, and handback support.
|
|
473
516
|
- Check NordRelay and agent CLI versions, then start Codex, Pi, Hermes, OpenClaw, or Claude Code updates from outdated rows or installs from not-installed rows with live output, cancel, delete-log, and stdin response controls.
|
|
474
517
|
- Build dashboard CSS and client JavaScript from modular source assets through esbuild, then serve them as authenticated static assets instead of inline HTML.
|
|
518
|
+
- Pair, test, enable/disable, and revoke NordRelay peers, then switch the dashboard target between the local instance and paired remote instances.
|
|
475
519
|
|
|
476
520
|
Dashboard API endpoints are served under `/api/*`. Streaming uses `GET /api/events`.
|
|
477
521
|
|
|
@@ -504,6 +548,8 @@ Run NordRelay behind your reverse proxy so the public URL forwards to `http://12
|
|
|
504
548
|
- `/channels` shows available and planned messaging adapters.
|
|
505
549
|
- `/agents` shows available and planned coding-agent adapters.
|
|
506
550
|
- `/agent` selects the active agent for this Telegram context.
|
|
551
|
+
- `/peers` shows configured NordRelay peer instances.
|
|
552
|
+
- `/target local|<peer-id>` selects whether prompts for this chat run locally or on a paired peer.
|
|
507
553
|
- `/link <code>` links the Telegram account to a NordRelay user.
|
|
508
554
|
- `/whoami` shows the linked NordRelay user, groups, and permissions.
|
|
509
555
|
- `/register_chat` enables the current Telegram group or forum chat for NordRelay when the linked user has user-management permission.
|
|
@@ -575,6 +621,7 @@ Discord supports slash commands and `/command` text messages for the shared comm
|
|
|
575
621
|
- `/prompt <text>` is available for slash-command-only deployments where regular message content is disabled.
|
|
576
622
|
- `/link <code>` consumes Discord link codes created in the WebUI or with `nordrelay user discord-link-code`.
|
|
577
623
|
- `/queue`, `/sessions`, `/agent`, `/model`, `/reasoning`, `/launch`, `/artifacts`, `/update`, and `/stop` use Discord buttons where component limits allow.
|
|
624
|
+
- `/peers` and `/target local|<peer-id>` use the same paired-instance target selection as Telegram.
|
|
578
625
|
- `/artifacts latest`, `/artifacts zip latest`, `/artifacts images`, `/artifacts docs`, `/artifacts search <text>`, and `/artifacts delete <turn-id>` are available in Discord.
|
|
579
626
|
- Unsafe launch profiles require explicit confirmation with `/launch <profile-id> confirm`.
|
|
580
627
|
- Discord does not support Telegram reactions or Telegram webhook transport; typing, message edits, attachments, files, DMs, guild channels, and threads are supported.
|
|
@@ -760,7 +807,7 @@ Voice transcription uses `OPENAI_API_KEY`, not `CODEX_API_KEY`.
|
|
|
760
807
|
Telegram:
|
|
761
808
|
|
|
762
809
|
- `TELEGRAM_ENABLED`: starts the Telegram adapter. Defaults to `true`.
|
|
763
|
-
- `TELEGRAM_BOT_TOKEN`:
|
|
810
|
+
- `TELEGRAM_BOT_TOKEN`: BotFather token. Required for the Telegram adapter to start.
|
|
764
811
|
- `TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS`: minimum interval for normal Telegram API sends. Defaults to `80`.
|
|
765
812
|
- `TELEGRAM_EDIT_MIN_INTERVAL_MS`: minimum interval for Telegram message edits. Defaults to `1200`.
|
|
766
813
|
- `TELEGRAM_TRANSPORT`: `polling` or `webhook`. Defaults to `polling`.
|
|
@@ -780,7 +827,7 @@ Telegram:
|
|
|
780
827
|
Discord:
|
|
781
828
|
|
|
782
829
|
- `DISCORD_ENABLED`: starts the Discord adapter. Defaults to `false`.
|
|
783
|
-
- `DISCORD_BOT_TOKEN`:
|
|
830
|
+
- `DISCORD_BOT_TOKEN`: Discord bot token. Required for the Discord adapter to start.
|
|
784
831
|
- `DISCORD_CLIENT_ID`: Discord application/client id used for slash-command registration.
|
|
785
832
|
- `DISCORD_GUILD_IDS`: optional comma-separated guild ids for instant guild slash-command registration.
|
|
786
833
|
- `DISCORD_ALLOWED_GUILD_IDS`: optional guild allow-list before user/group permissions are checked.
|
|
@@ -795,9 +842,21 @@ User management:
|
|
|
795
842
|
- Users, groups, Telegram identities, Telegram group-chat access, Discord identities, Discord channel access, and web sessions are stored in `~/.nordrelay/users.json`.
|
|
796
843
|
- Manage users in the WebUI Users page or with `nordrelay user list`, `create-admin`, `create`, `reset-password`, `link-telegram`, `link-discord`, `link-code`, and `discord-link-code`.
|
|
797
844
|
- Built-in groups are `admin`, `user`, and `readonly`.
|
|
798
|
-
- Group permissions include `inspect`, `sessions.read`, `sessions.write`, `prompt.send`, `prompt.abort`, `files.read`, `files.write`, `settings.read`, `settings.write`, `auth.manage`, `diagnostics.read`, `logs.read`, `logs.clear`, `queue.read`, `queue.write`, `updates.run`, `system.restart`, `users.read`, `users.write`, and `
|
|
845
|
+
- Group permissions include `inspect`, `sessions.read`, `sessions.write`, `prompt.send`, `prompt.abort`, `files.read`, `files.write`, `settings.read`, `settings.write`, `auth.manage`, `diagnostics.read`, `logs.read`, `logs.clear`, `queue.read`, `queue.write`, `updates.run`, `system.restart`, `users.read`, `users.write`, `audit.read`, `peers.read`, `peers.write`, and `peers.connect`.
|
|
799
846
|
- Custom groups can also restrict access to specific agent ids, workspace roots, Telegram chat ids, and Discord channel ids.
|
|
800
847
|
|
|
848
|
+
Peers:
|
|
849
|
+
|
|
850
|
+
- `NORDRELAY_PEER_ENABLED`: starts the dedicated peer API. Defaults to `false`.
|
|
851
|
+
- `NORDRELAY_PEER_NAME`: optional human-readable node name shown to paired instances.
|
|
852
|
+
- `NORDRELAY_PEER_HOST`: peer API bind host. Defaults to `127.0.0.1`.
|
|
853
|
+
- `NORDRELAY_PEER_PORT`: peer API port. Defaults to `31979`.
|
|
854
|
+
- `NORDRELAY_PEER_PUBLIC_URL`: optional URL other instances should use to reach this node.
|
|
855
|
+
- `NORDRELAY_PEER_TLS_ENABLED`: serves the peer API over HTTPS with an automatically generated local certificate. Defaults to `true`.
|
|
856
|
+
- `NORDRELAY_PEER_REQUIRE_TLS`: refuses plaintext peer serving on non-loopback hosts. Defaults to `true`.
|
|
857
|
+
- Peer identity, TLS certificate, peers, and invitations are stored under `~/.nordrelay/identity.json`, `~/.nordrelay/tls/`, and `~/.nordrelay/peers.json`.
|
|
858
|
+
- Peer invitations expire after at most 24 hours even if a longer lifetime is requested.
|
|
859
|
+
|
|
801
860
|
Agent selection:
|
|
802
861
|
|
|
803
862
|
- `NORDRELAY_CODEX_ENABLED`: enables Codex contexts. Defaults to `true`.
|
|
@@ -956,12 +1015,14 @@ Unsafe profiles are intentionally gated. Telegram asks for confirmation before a
|
|
|
956
1015
|
- Link Telegram accounts only to active NordRelay users that should control agents remotely.
|
|
957
1016
|
- Enable Telegram group/forum chats only when the whole chat context is trusted for the permissions granted to linked users.
|
|
958
1017
|
- Review group permissions before granting `prompt.send`, `prompt.abort`, `files.write`, `settings.write`, `updates.run`, `system.restart`, or `users.write`.
|
|
1018
|
+
- Review peer scopes before granting `peers.write`, `peers.connect`, broad `prompt.send`, or unrestricted workspace roots to a paired instance.
|
|
959
1019
|
- Treat `danger-full-access` as equivalent to shell access on the host.
|
|
960
1020
|
- Treat uploaded files as untrusted input. They are staged inside the active workspace so the selected sandbox policy still matters.
|
|
961
1021
|
- Keep `CODEX_API_KEY`, `HERMES_API_KEY`, `OPENCLAW_GATEWAY_TOKEN`, `OPENCLAW_GATEWAY_PASSWORD`, and `OPENAI_API_KEY` in `~/.nordrelay/nordrelay.env` or host secret management.
|
|
962
1022
|
- In group chats, remember that any linked user with prompt permissions can prompt the selected agent in that chat context.
|
|
963
1023
|
- Use `TOOL_VERBOSITY=summary` or `errors-only` when command output may include sensitive data.
|
|
964
1024
|
- Review and unsafe launch profiles add a Telegram approve/deny gate before each turn starts.
|
|
1025
|
+
- Keep the peer API disabled unless needed. For internet use, expose it only through a firewall, VPN, or hardened reverse proxy; keep TLS enabled and revoke unused peers with `nordrelay peer revoke <peer-id>`.
|
|
965
1026
|
|
|
966
1027
|
## Troubleshooting
|
|
967
1028
|
|
package/dist/access-control.js
CHANGED
|
@@ -20,6 +20,9 @@ export const ALL_PERMISSIONS = [
|
|
|
20
20
|
"users.read",
|
|
21
21
|
"users.write",
|
|
22
22
|
"audit.read",
|
|
23
|
+
"peers.read",
|
|
24
|
+
"peers.write",
|
|
25
|
+
"peers.connect",
|
|
23
26
|
];
|
|
24
27
|
export const ADMIN_GROUP_ID = "admin";
|
|
25
28
|
export const USER_GROUP_ID = "user";
|
|
@@ -71,6 +74,8 @@ const COMMAND_PERMISSIONS = new Map([
|
|
|
71
74
|
["health", "inspect"],
|
|
72
75
|
["version", "inspect"],
|
|
73
76
|
["channels", "inspect"],
|
|
77
|
+
["peers", "peers.read"],
|
|
78
|
+
["target", "peers.connect"],
|
|
74
79
|
["agents", "inspect"],
|
|
75
80
|
["tasks", "inspect"],
|
|
76
81
|
["progress", "inspect"],
|
|
@@ -148,7 +153,7 @@ export function permissionForCallbackData(callbackData) {
|
|
|
148
153
|
if (callbackData.startsWith("approval_") || callbackData.startsWith("codex_abort:") || callbackData.startsWith("agent_abort:")) {
|
|
149
154
|
return "prompt.abort";
|
|
150
155
|
}
|
|
151
|
-
if (callbackData.startsWith("queue_")) {
|
|
156
|
+
if (callbackData.startsWith("queue_") || callbackData.startsWith("peer_queue_")) {
|
|
152
157
|
return "queue.write";
|
|
153
158
|
}
|
|
154
159
|
if (callbackData.startsWith("artifact_delete")) {
|
package/dist/activity-events.js
CHANGED
|
@@ -11,7 +11,7 @@ export function activityCategoryForType(type) {
|
|
|
11
11
|
return "artifact";
|
|
12
12
|
if (/^(auth|login|logout)/.test(type))
|
|
13
13
|
return "auth";
|
|
14
|
-
if (/^(user_|group_|telegram_chat_|telegram_link|discord_channel_|discord_link|permission_|access_|lock_)/.test(type))
|
|
14
|
+
if (/^(user_|group_|telegram_chat_|telegram_link|discord_channel_|discord_link|peer_|permission_|access_|lock_)/.test(type))
|
|
15
15
|
return "security";
|
|
16
16
|
if (/^(tool_|cli_tool)/.test(type))
|
|
17
17
|
return "tool";
|
|
@@ -32,7 +32,7 @@ export function auditCategoryForAction(action) {
|
|
|
32
32
|
return "security";
|
|
33
33
|
if (/^auth_/.test(action))
|
|
34
34
|
return "auth";
|
|
35
|
-
if (/^(permission_|user_|group_|telegram_|discord_)/.test(action))
|
|
35
|
+
if (/^(permission_|user_|group_|telegram_|discord_|peer_)/.test(action))
|
|
36
36
|
return "security";
|
|
37
37
|
if (/^(artifact|file)/.test(action))
|
|
38
38
|
return "artifact";
|
package/dist/bot-preferences.js
CHANGED
|
@@ -117,6 +117,7 @@ function normalizePreferences(value) {
|
|
|
117
117
|
voiceBackend: isVoiceBackendPreference(candidate.voiceBackend) ? candidate.voiceBackend : undefined,
|
|
118
118
|
voiceLanguage: typeof candidate.voiceLanguage === "string" ? candidate.voiceLanguage : candidate.voiceLanguage === null ? null : undefined,
|
|
119
119
|
voiceTranscribeOnly: typeof candidate.voiceTranscribeOnly === "boolean" ? candidate.voiceTranscribeOnly : undefined,
|
|
120
|
+
targetPeerId: typeof candidate.targetPeerId === "string" ? candidate.targetPeerId : candidate.targetPeerId === null ? null : undefined,
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
function pruneEmptyPreferences(preferences) {
|
package/dist/bot.js
CHANGED
|
@@ -12,6 +12,7 @@ import { formatSessionLabel } from "./bot-ui.js";
|
|
|
12
12
|
import { BotPreferencesStore, isQuietNow, } from "./bot-preferences.js";
|
|
13
13
|
import { renderAgentUpdateJobAction } from "./channel-actions.js";
|
|
14
14
|
import { ChannelCommandService } from "./channel-command-service.js";
|
|
15
|
+
import { runChannelPeerPrompt } from "./channel-peer-prompt.js";
|
|
15
16
|
import { deliverChannelAction } from "./channel-runtime.js";
|
|
16
17
|
import { agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
17
18
|
import { getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
@@ -24,6 +25,7 @@ import { escapeHTML } from "./format.js";
|
|
|
24
25
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
25
26
|
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
26
27
|
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
28
|
+
import { RemoteRelayClient } from "./peer-client.js";
|
|
27
29
|
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
28
30
|
import { configureRedaction, redactText } from "./redaction.js";
|
|
29
31
|
import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
|
|
@@ -984,6 +986,76 @@ export function createBot(config, registry) {
|
|
|
984
986
|
].join("\n");
|
|
985
987
|
await safeReply(ctx, html, { fallbackText: plain, replyMarkup: keyboard });
|
|
986
988
|
};
|
|
989
|
+
const remoteClient = new RemoteRelayClient();
|
|
990
|
+
const handleRemoteUserPrompt = async (ctx, contextKey, chatId, prompt) => {
|
|
991
|
+
const targetPeerId = preferencesStore.get(contextKey).targetPeerId ?? undefined;
|
|
992
|
+
const parsed = parseContextKey(contextKey);
|
|
993
|
+
const messageThreadId = parsed.messageThreadId;
|
|
994
|
+
return runChannelPeerPrompt({
|
|
995
|
+
targetPeerId,
|
|
996
|
+
contextKey,
|
|
997
|
+
prompt,
|
|
998
|
+
remoteClient,
|
|
999
|
+
editMinIntervalMs: config.telegramEditMinIntervalMs,
|
|
1000
|
+
typingIntervalMs: TYPING_INTERVAL_MS,
|
|
1001
|
+
sendTyping: () => sendChatActionSafe(ctx.api, chatId, "typing", messageThreadId),
|
|
1002
|
+
sendResponse: async (text) => {
|
|
1003
|
+
const message = await sendTextMessage(ctx.api, chatId, escapeHTML(text), {
|
|
1004
|
+
fallbackText: text,
|
|
1005
|
+
messageThreadId,
|
|
1006
|
+
});
|
|
1007
|
+
return message.message_id;
|
|
1008
|
+
},
|
|
1009
|
+
editResponse: (messageId, text) => safeEditMessage(bot, chatId, messageId, escapeHTML(text), {
|
|
1010
|
+
fallbackText: text,
|
|
1011
|
+
}),
|
|
1012
|
+
sendTurnStart: (remotePrompt) => safeReply(ctx, `<b>Remote peer working on:</b>\n${escapeHTML(remotePrompt)}`, {
|
|
1013
|
+
fallbackText: `Remote peer working on:\n${remotePrompt}`,
|
|
1014
|
+
}),
|
|
1015
|
+
sendToolStart: (toolName) => safeReply(ctx, `<b>Remote tool:</b> <code>${escapeHTML(toolName)}</code>`, {
|
|
1016
|
+
fallbackText: `Remote tool: ${toolName}`,
|
|
1017
|
+
}),
|
|
1018
|
+
sendQueued: async (queueId) => {
|
|
1019
|
+
const keyboard = queueId ? new InlineKeyboard().text("Cancel queued message", `peer_queue_cancel:${targetPeerId}:${queueId}`) : undefined;
|
|
1020
|
+
await safeReply(ctx, escapeHTML(`Remote prompt queued${queueId ? `: ${queueId}` : ""}.`), {
|
|
1021
|
+
fallbackText: `Remote prompt queued${queueId ? `: ${queueId}` : ""}.`,
|
|
1022
|
+
replyMarkup: keyboard,
|
|
1023
|
+
});
|
|
1024
|
+
},
|
|
1025
|
+
sendCompleted: () => safeReply(ctx, escapeHTML("Remote turn completed."), { fallbackText: "Remote turn completed." }),
|
|
1026
|
+
sendFailure: (message) => safeReply(ctx, escapeHTML(`Remote peer failed: ${message}`), {
|
|
1027
|
+
fallbackText: `Remote peer failed: ${message}`,
|
|
1028
|
+
}),
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
1031
|
+
bot.callbackQuery(/^peer_queue_cancel:([^:]+):([a-z0-9]+)$/, async (ctx) => {
|
|
1032
|
+
const targetPeerId = ctx.match?.[1];
|
|
1033
|
+
const queueId = ctx.match?.[2];
|
|
1034
|
+
const contextKey = contextKeyFromCtx(ctx);
|
|
1035
|
+
if (!targetPeerId || !queueId || !contextKey) {
|
|
1036
|
+
await ctx.answerCallbackQuery();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
await remoteClient.webProxy(targetPeerId, {
|
|
1041
|
+
method: "POST",
|
|
1042
|
+
path: "/api/queue",
|
|
1043
|
+
body: { action: "cancel", id: queueId },
|
|
1044
|
+
contextKey,
|
|
1045
|
+
}, telegramActivityActor(ctx), contextKey);
|
|
1046
|
+
await ctx.answerCallbackQuery({ text: `Cancelled remote queued prompt ${queueId}.` });
|
|
1047
|
+
const chatId = ctx.chat?.id;
|
|
1048
|
+
const messageId = ctx.callbackQuery.message?.message_id;
|
|
1049
|
+
if (chatId && messageId) {
|
|
1050
|
+
await safeEditMessage(bot, chatId, messageId, escapeHTML(`Cancelled remote queued prompt ${queueId}.`), {
|
|
1051
|
+
fallbackText: `Cancelled remote queued prompt ${queueId}.`,
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
await ctx.answerCallbackQuery({ text: friendlyErrorText(error), show_alert: true });
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
987
1059
|
const handleUserPrompt = async (ctx, contextKey, chatId, session, prompt, options = {}) => {
|
|
988
1060
|
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
989
1061
|
return;
|
|
@@ -995,6 +1067,9 @@ export function createBot(config, registry) {
|
|
|
995
1067
|
...rawEnvelope,
|
|
996
1068
|
activityActor: rawEnvelope.activityActor ?? telegramActivityActor(ctx),
|
|
997
1069
|
};
|
|
1070
|
+
if (!options.fromQueue && await handleRemoteUserPrompt(ctx, contextKey, chatId, envelope)) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
998
1073
|
if (!options.fromQueue && await denyIfLocked(ctx, contextKey, session)) {
|
|
999
1074
|
return;
|
|
1000
1075
|
}
|
|
@@ -1887,6 +1962,7 @@ export function createBot(config, registry) {
|
|
|
1887
1962
|
isTopicContext,
|
|
1888
1963
|
replyChannelAction,
|
|
1889
1964
|
commandService,
|
|
1965
|
+
preferencesStore,
|
|
1890
1966
|
});
|
|
1891
1967
|
registerTelegramAgentCommands({
|
|
1892
1968
|
bot,
|
|
@@ -1914,14 +1990,9 @@ export function createBot(config, registry) {
|
|
|
1914
1990
|
registerTelegramPreferenceCommands({
|
|
1915
1991
|
bot,
|
|
1916
1992
|
config,
|
|
1993
|
+
commandService,
|
|
1917
1994
|
preferencesStore,
|
|
1918
1995
|
getContextSession,
|
|
1919
|
-
getEffectiveMirrorMode,
|
|
1920
|
-
getEffectiveNotifyMode,
|
|
1921
|
-
getEffectiveQuietHours,
|
|
1922
|
-
getEffectiveVoiceBackend,
|
|
1923
|
-
getEffectiveVoiceLanguage,
|
|
1924
|
-
isVoiceTranscribeOnly,
|
|
1925
1996
|
});
|
|
1926
1997
|
registerTelegramDiagnosticsCommands({
|
|
1927
1998
|
bot,
|
package/dist/channel-adapter.js
CHANGED
|
@@ -45,7 +45,8 @@ export class TelegramChannelAdapter {
|
|
|
45
45
|
label = "Telegram";
|
|
46
46
|
capabilities = new Set(TELEGRAM_CAPABILITIES);
|
|
47
47
|
describe() {
|
|
48
|
-
const
|
|
48
|
+
const requested = process.env.TELEGRAM_ENABLED !== "false";
|
|
49
|
+
const enabled = requested && Boolean(process.env.TELEGRAM_BOT_TOKEN);
|
|
49
50
|
return {
|
|
50
51
|
id: this.id,
|
|
51
52
|
label: this.label,
|
|
@@ -53,8 +54,10 @@ export class TelegramChannelAdapter {
|
|
|
53
54
|
status: "available",
|
|
54
55
|
enabled,
|
|
55
56
|
notes: enabled
|
|
56
|
-
? "Telegram bot runtime is enabled
|
|
57
|
-
:
|
|
57
|
+
? "Telegram bot runtime is enabled."
|
|
58
|
+
: requested
|
|
59
|
+
? "Telegram bot runtime is disabled because TELEGRAM_BOT_TOKEN is missing."
|
|
60
|
+
: "Telegram bot runtime is disabled.",
|
|
58
61
|
};
|
|
59
62
|
}
|
|
60
63
|
}
|
|
@@ -63,7 +66,8 @@ export class DiscordChannelAdapter {
|
|
|
63
66
|
label = "Discord";
|
|
64
67
|
capabilities = new Set(DISCORD_CAPABILITIES);
|
|
65
68
|
describe() {
|
|
66
|
-
const
|
|
69
|
+
const requested = process.env.DISCORD_ENABLED === "true";
|
|
70
|
+
const enabled = requested && Boolean(process.env.DISCORD_BOT_TOKEN);
|
|
67
71
|
return {
|
|
68
72
|
id: this.id,
|
|
69
73
|
label: this.label,
|
|
@@ -72,7 +76,9 @@ export class DiscordChannelAdapter {
|
|
|
72
76
|
enabled,
|
|
73
77
|
notes: enabled
|
|
74
78
|
? "Discord bot runtime is enabled."
|
|
75
|
-
:
|
|
79
|
+
: requested
|
|
80
|
+
? "Discord bot runtime is disabled because DISCORD_BOT_TOKEN is missing."
|
|
81
|
+
: "Enable with DISCORD_ENABLED=true and DISCORD_BOT_TOKEN.",
|
|
76
82
|
};
|
|
77
83
|
}
|
|
78
84
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const textOption = (name = "value", description = "Value", required = false) => ({
|
|
2
|
+
type: 3,
|
|
3
|
+
name,
|
|
4
|
+
description,
|
|
5
|
+
required,
|
|
6
|
+
});
|
|
7
|
+
export const CHANNEL_COMMANDS = [
|
|
8
|
+
{ name: "start", description: "Welcome and status", discordDescription: "Start or inspect the current NordRelay context" },
|
|
9
|
+
{ name: "help", description: "Command reference", discordDescription: "Show Discord adapter help" },
|
|
10
|
+
{ name: "prompt", description: "Send a prompt to the selected agent", telegram: false, discordOptions: [textOption("text", "Prompt text", true)] },
|
|
11
|
+
{ name: "link", description: "Link account to NordRelay user", telegramDescription: "Link Telegram to NordRelay user", discordDescription: "Link this Discord account with a NordRelay code", discordOptions: [textOption("value", "Link code", true)] },
|
|
12
|
+
{ name: "whoami", description: "Show your NordRelay user", discordDescription: "Show linked NordRelay user" },
|
|
13
|
+
{ name: "register_chat", description: "Admin: enable this group chat", discord: false },
|
|
14
|
+
{ name: "register_channel", description: "Enable this Discord channel for NordRelay", telegram: false },
|
|
15
|
+
{ name: "channels", description: "Messaging adapter status", discordDescription: "Show channel adapters" },
|
|
16
|
+
{ name: "peers", description: "NordRelay peer status", discordDescription: "Show paired NordRelay instances" },
|
|
17
|
+
{ name: "target", description: "Select local or peer target", discordDescription: "Select local or peer target", discordOptions: [textOption("value", "local or peer id")] },
|
|
18
|
+
{ name: "agents", description: "Agent adapter status", discordDescription: "Show agent adapters" },
|
|
19
|
+
{ name: "agent", description: "Select agent", discordDescription: "Select or show the active agent", discordOptions: [textOption("value", "Agent id")] },
|
|
20
|
+
{ name: "new", description: "Start a new thread", discordDescription: "Create a new session", discordOptions: [textOption("value", "Workspace path")] },
|
|
21
|
+
{ name: "session", description: "Current thread details", discordDescription: "Show the active session" },
|
|
22
|
+
{ name: "sessions", description: "Browse and switch threads", discordDescription: "Browse recent sessions", discordOptions: [textOption("query", "Search query")] },
|
|
23
|
+
{ name: "switch", description: "Switch to a thread by ID", discordDescription: "Switch to a session", discordOptions: [textOption("thread_id", "Thread id", true)] },
|
|
24
|
+
{ name: "attach", description: "Bind a session to this topic", discordDescription: "Attach a session", discordOptions: [textOption("thread_id", "Thread id", true)] },
|
|
25
|
+
{ name: "handback", description: "Hand session back to CLI", discordDescription: "Hand the active session back to the native CLI" },
|
|
26
|
+
{ name: "sync", description: "Sync active session from CLI state", discordDescription: "Sync from local agent state" },
|
|
27
|
+
{ name: "pinned", description: "Show pinned threads" },
|
|
28
|
+
{ name: "pin", description: "Pin current or given thread", discordOptions: [textOption("value", "Thread id")] },
|
|
29
|
+
{ name: "unpin", description: "Unpin current or given thread", discordOptions: [textOption("value", "Thread id")] },
|
|
30
|
+
{ name: "retry", description: "Resend the last prompt", discordDescription: "Retry the last prompt" },
|
|
31
|
+
{ name: "queue", description: "Show queued prompts", discordDescription: "Show or manage queue", discordOptions: [textOption("action", "pause/resume/clear/run/cancel/top/up/down"), textOption("id", "Queue id")] },
|
|
32
|
+
{ name: "cancel", description: "Cancel a queued prompt", discordOptions: [textOption("value", "Queue id", true)] },
|
|
33
|
+
{ name: "clearqueue", description: "Clear queued prompts", discordDescription: "Clear queue" },
|
|
34
|
+
{ name: "artifacts", description: "List or resend generated files", discordDescription: "List or send artifacts", discordOptions: [textOption("value", "zip <turn-id>")] },
|
|
35
|
+
{ name: "workspaces", description: "List allowed workspaces" },
|
|
36
|
+
{ name: "abort", description: "Cancel current operation", discordDescription: "Abort the active task" },
|
|
37
|
+
{ name: "stop", description: "Cancel current operation", discordDescription: "Abort the active task" },
|
|
38
|
+
{ name: "launch", description: "Select launch profile", discordOptions: [textOption("value", "Launch profile id")] },
|
|
39
|
+
{ name: "launch_profiles", description: "Select launch profile", discordOptions: [textOption("value", "Launch profile id")] },
|
|
40
|
+
{ name: "fast", description: "Toggle fast mode", discordOptions: [textOption("value", "on/off")] },
|
|
41
|
+
{ name: "model", description: "View and change model", discordDescription: "Select or show models", discordOptions: [textOption("value", "Model id")] },
|
|
42
|
+
{ name: "effort", description: "Set reasoning effort", discordDescription: "Select reasoning effort", discordOptions: [textOption("value", "Reasoning value")] },
|
|
43
|
+
{ name: "reasoning", description: "Set reasoning effort", discordDescription: "Select reasoning effort", discordOptions: [textOption("value", "Reasoning value")] },
|
|
44
|
+
{ name: "mirror", description: "Control CLI mirroring", discordDescription: "Set mirror mode", discordOptions: [textOption("value", "off/status/final/full")] },
|
|
45
|
+
{ name: "notify", description: "Control notifications", discordDescription: "Set notification mode", discordOptions: [textOption("value", "off/minimal/all")] },
|
|
46
|
+
{ name: "auth", description: "Check auth status", discordDescription: "Show selected agent auth status" },
|
|
47
|
+
{ name: "login", description: "Start authentication", discordDescription: "Start selected agent login" },
|
|
48
|
+
{ name: "logout", description: "Sign out", discordDescription: "Sign out of the selected agent" },
|
|
49
|
+
{ name: "voice", description: "Voice transcription status", discordDescription: "Show or change voice settings", discordOptions: [textOption("value", "transcribe-only on/off")] },
|
|
50
|
+
{ name: "tasks", description: "Current turn progress", discordDescription: "Show recent tasks", discordOptions: [textOption("value", "Limit")] },
|
|
51
|
+
{ name: "progress", description: "Current turn progress", discordDescription: "Show current turn progress" },
|
|
52
|
+
{ name: "activity", description: "Thread activity timeline", discordDescription: "Show recent activity", discordOptions: [textOption("value", "Limit")] },
|
|
53
|
+
{ name: "audit", description: "Admin: recent audit events", discordDescription: "Show recent audit events", discordOptions: [textOption("value", "Limit")] },
|
|
54
|
+
{ name: "status", description: "Connector runtime status", discordDescription: "Show status" },
|
|
55
|
+
{ name: "health", description: "Connector health report", discordDescription: "Show health" },
|
|
56
|
+
{ name: "version", description: "Connector version", discordDescription: "Show versions" },
|
|
57
|
+
{ name: "logs", description: "Admin: show connector logs", discordDescription: "Show logs", discordOptions: [textOption("value", "Target and line count")] },
|
|
58
|
+
{ name: "diagnostics", description: "Admin: connector diagnostics", discordDescription: "Show diagnostics" },
|
|
59
|
+
{ name: "support", description: "Admin: export diagnostics bundle", discordDescription: "Show support diagnostics" },
|
|
60
|
+
{ name: "lock", description: "Lock session writes to you", discordDescription: "Lock this context" },
|
|
61
|
+
{ name: "unlock", description: "Release session write lock", discordDescription: "Unlock this context" },
|
|
62
|
+
{ name: "locks", description: "List session write locks", discordDescription: "List locks" },
|
|
63
|
+
{ name: "restart", description: "Admin: restart connector", discordDescription: "Restart NordRelay" },
|
|
64
|
+
{ name: "update", description: "Admin: update connector or agents", discordDescription: "Update NordRelay or agents", discordOptions: [textOption("target", "jobs, install, log, cancel, input, or agent id"), textOption("agent", "Agent id or job id"), textOption("input", "Text for update input")] },
|
|
65
|
+
];
|
|
66
|
+
export function telegramCommandCatalog() {
|
|
67
|
+
return CHANNEL_COMMANDS
|
|
68
|
+
.filter((entry) => entry.telegram !== false)
|
|
69
|
+
.map((entry) => ({
|
|
70
|
+
command: entry.name,
|
|
71
|
+
description: entry.telegramDescription ?? entry.description,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
export function discordCommandCatalog() {
|
|
75
|
+
return CHANNEL_COMMANDS
|
|
76
|
+
.filter((entry) => entry.discord !== false)
|
|
77
|
+
.map((entry) => ({
|
|
78
|
+
name: entry.name,
|
|
79
|
+
description: entry.discordDescription ?? entry.description,
|
|
80
|
+
options: entry.discordOptions ?? [],
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
export function discordHelpCommandList() {
|
|
84
|
+
return discordCommandCatalog()
|
|
85
|
+
.filter((entry) => !["start", "help", "prompt"].includes(entry.name))
|
|
86
|
+
.map((entry) => `/${entry.name}`)
|
|
87
|
+
.join(", ");
|
|
88
|
+
}
|