helmpilot
v0.4.5
Published
Helmpilot desktop channel plugin for OpenClaw — interactive tools and gateway RPC bridge
Downloads
42
Maintainers
Readme
helmpilot-channel
The Helmpilot Desktop Channel plugin for OpenClaw — registers Helmpilot as a full-featured channel in the OpenClaw gateway, providing interactive tools, outbound messaging, and relay-based cross-network tunneling.
Channel ID: helmpilot
Config Section: channels.helmpilot
Architecture
Helmpilot is a desktop client that communicates with OpenClaw via WebSocket RPC. This plugin makes Helmpilot a first-class channel in the OpenClaw ecosystem, following the same plugin pattern as Slack, Telegram, Discord, and other channels.
Relay Tunnel
When configured with a relay URL, the plugin establishes a transparent tunnel:
Helmpilot Desktop ──WS(deviceKey)──→ Helmpilot Relay ←──WS(channelKey)── helmpilot-channel plugin
(transparent) │
bridge WS
↕
ws://127.0.0.1:<port>
(local gateway)The relay forwards raw WebSocket messages without parsing. The plugin acts as a bridge: it connects to the relay as the "gateway side", then opens a local WebSocket to the OpenClaw gateway. Messages are transparently forwarded between these two connections.
Interactive Tool Flow
Agent calls hp_ask_user({ questions: [...] })
↓
OpenClaw emits tool.start event → Helmpilot client renders QuestionCard UI
↓
Plugin execute() blocks on a Promise (keyed by toolCallId)
↓
User fills answers → Helmpilot client calls helmpilot.respond via WS RPC
↓
Gateway method resolves the Promise → returns answers as tool result
↓
Agent sees actual user answers, continues reasoningPlugin Structure
plugins/helmpilot-channel/
index.ts ← Plugin entry (definePluginEntry) — tools, RPC, channel registration
channel-plugin.ts ← ChannelPlugin definition (meta/config/gateway/startAccount)
tools.ts ← Tool schemas, PendingMap (Symbol.for key)
bridge.ts ← LocalBridge — WS tunnel to local gateway with ping/pong heartbeat
outbound.ts ← Outbound adapter (sendText/sendMedia via relay)
relay-client.ts ← RelayClient with Ed25519 auth + auto-reconnect + keepalive
relay-registry.ts ← Module-level Map<accountId, RelayClient> singleton
adapters.ts ← Messaging/Setup/Status/Pairing/Security adapters
types.ts ← Question/Answer TypeBox schemas
preload.cjs ← CJS→ESM bridge + SDK symlink
openclaw.plugin.json ← Manifest (declares channel: "helmpilot")
package.json ← Package metadata and OpenClaw compatibility
__tests__/ ← Outbound adapter + config adapter testsChannel Adapters
The plugin implements the full ChannelPlugin interface:
| Adapter | Source | Purpose |
|---------|--------|---------|
| outbound | outbound.ts | Send text/media to Helmpilot client via relay (deliveryMode: 'direct', textChunkLimit: 4000) |
| messaging | adapters.ts | Target normalization and resolution |
| setup | adapters.ts | Account config validation and application |
| status | adapters.ts | Channel summary, account snapshot, probe |
| pairing | adapters.ts | Device key pairing (idLabel: 'Device Key') |
| security | adapters.ts | Security policy adapter |
| config | Inline in plugin | Account CRUD, enable/disable, account resolution |
Outbound Adapter
Sends messages as OpenClaw event frames through the relay:
// Format sent to relay
{
type: 'event',
event: 'helmpilot.outbound',
payload: {
kind: 'text' | 'media',
text?: string,
mediaUrl?: string,
messageId: string, // Format: msg_<uuid>
replyToId?: string,
identity?: { name?, avatar?, emoji? }
}
}Tools
hp_ask_user
Present structured questions to the user in the Helmpilot desktop client. Blocks until the user submits answers. No fixed timeout — waits indefinitely until the user responds. Cleanup relies on connection/session lifecycle boundaries.
{
questions: [
{
header: "Language", // Short unique ID (max 50 chars)
question: "Which language?", // Display text (max 500 chars)
options: [ // Optional predefined choices
{ label: "Python", recommended: true },
{ label: "TypeScript" },
{ label: "Rust", description: "Systems programming" }
],
multiSelect: false, // Allow multiple selections
allowFreeformInput: true // Allow typing a custom answer
}
]
}hp_send_msg
Send a notification or structured message to the Helmpilot client. Non-blocking — delivers the message for display as a distinct notification card, separate from inline chat stream. Supports Markdown in the message body.
{
message: "Deployment **completed** successfully!", // Markdown content
type: "success", // "info" (default) | "success" | "warning" | "error"
title: "Deploy Status" // Optional notification title
}hp_send_file
Send a file to the Helmpilot client for local display/writing. Non-blocking — the tool returns immediately after relaying the content.
Gateway RPC Methods
helmpilot.respond
Called by the Helmpilot client to deliver user answers to a pending hp_ask_user call.
{ "toolCallId": "...", "answers": "User's formatted answer string" }Configuration
CLI Quick Setup
The fastest way to configure helmpilot-channel is via the OpenClaw CLI:
# Full non-interactive setup (all three fields at once)
openclaw channels add --channel helmpilot \
--url wss://relay.example.com \
--code helmpilot-abc123 \
--token ck_xyz789
# Interactive wizard mode (guided step-by-step)
openclaw channels add --channel helmpilot
# Partial update (e.g. rotate channel key only)
openclaw channels add --channel helmpilot --token ck_new_key| CLI Flag | Config Field | Validation | Example |
|----------|-------------|------------|---------|
| --url | channels.helmpilot.relayUrl | Must be ws://, wss://, http://, or https:// | wss://relay.example.com |
| --code | channels.helmpilot.channelId | Must start with helmpilot- | helmpilot-abc123 |
| --token | channels.helmpilot.channelKey | Must start with ck_ | ck_xyz789 |
Tip: The
channelIdandchannelKeyare generated by the Relay server during provisioning (triggered from the Helmpilot desktop client). Copy them from the Helmpilot setup screen into the CLI command above.
Manual Configuration
Add to your OpenClaw config (openclaw.json):
Single account (simple)
{
"channels": {
"helmpilot": {
"enabled": true,
"relayUrl": "wss://relay.example.com",
"channelKey": "ck_xxxxx..."
}
}
}Multi-account
{
"channels": {
"helmpilot": {
"accounts": {
"work": { "relayUrl": "wss://relay.example.com", "channelKey": "ck_aaa..." },
"home": { "relayUrl": "wss://relay.example.com", "channelKey": "ck_bbb..." }
}
}
}
}Secret storage for channelKey
To avoid storing channelKey in plaintext, use any of these formats:
// Env template — resolved from process.env at startup
"channelKey": "${HELMPILOT_CHANNEL_KEY}"
// SecretRef (env source)
"channelKey": { "source": "env", "id": "HELMPILOT_CHANNEL_KEY" }
// SecretRef (file source)
"channelKey": { "source": "file", "id": "/run/secrets/helmpilot-channel-key" }| Field | Type | Required | Purpose |
|-------|------|----------|---------|
| enabled | boolean | No (default: true) | Enable/disable the channel |
| relayUrl | string | Yes (for relay mode) | Relay service WebSocket URL |
| channelId | string | Auto-populated | Channel ID from relay registration |
| channelKey | string | SecretRef | Yes (for relay mode) | Channel key — supports env templates and SecretRef |
Installation
openclaw plugins install helmpilot-channelOr add to your workspace's plugin load paths:
# Local development (symlink)
ln -s /path/to/Helmpilot/plugins/helmpilot-channel ~/.openclaw/extensions/helmpilot-channelRelay Client
RelayClient (relay-client.ts) manages the gateway-side WebSocket connection to the relay:
- Ed25519 auth: Auto-generates key pair, handles challenge-response with relay
- Legacy fallback: Falls back to
?key=<channelKey>URL param if Ed25519 keys not registered - Auto-reconnect: Exponential backoff from 1s to 30s
- Keepalive: Sends
{"__relay":true,"type":"ping"}every 25 seconds - Flush handling: Recognizes
flush_start/flush_endrelay markers for buffered message batches - States:
'disconnected' | 'connecting' | 'authenticating' | 'connected' | 'error'
Local Bridge
LocalBridge (bridge.ts) manages the WS connection to the local OpenClaw gateway:
- Transparent tunnel: Forwards raw WS frames between relay and local gateway
- Ping/pong heartbeat: 20s interval + 10s timeout for dead connection detection
- Lifecycle: Opened per
startAccount(), closed on disconnect
Process Architecture
The gateway loads plugin registration twice:
- Connection-level:
registerGatewayMethod()handlers (client-specific) - Per-agent:
registerTool()handlers (agent-scoped)
These scopes don't share closure variables. We use globalThis[Symbol.for('helmpilot.pendingMap')]
as a process-level shared mailbox between helmpilot.respond (connection) and
hp_ask_user execute (agent).
Multi-Account Isolation
Each account gets its own RelayClient + LocalBridge pair, registered in relay-registry.ts.
Outbound messages are routed to the correct relay via getRelay(accountId) — the null-fallback
has been removed to prevent cross-account message leakage.
The helmpilot.respond RPC handler requires an explicit toolCallId to prevent resolving
a pending request from a different account's agent.
Testing
# All plugin tests
npx vitest run plugins/helmpilot-channel/__tests__/
# Individual test files
npx vitest run plugins/helmpilot-channel/__tests__/outbound.test.ts # Outbound adapter (10 cases)
npx vitest run plugins/helmpilot-channel/__tests__/config-adapter.test.ts # Config/multi-account (varies)
npx vitest run plugins/helmpilot-channel/__tests__/setup-adapter.test.ts # CLI setup adapter (16 cases)License
MIT
