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
18 changes: 15 additions & 3 deletions .github/workflows/helm-boundary-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ name: HELM Boundary Check

on:
pull_request:
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
validate:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
cache-dependency-path: "packages/js/helm-tool-wrapper/package-lock.json"

- uses: actions/setup-python@v6
with:
Expand All @@ -28,6 +32,14 @@ jobs:
run: npm test
working-directory: packages/js/helm-tool-wrapper

- name: Install channel bridge dependencies
run: npm install
working-directory: packages/js/helm-channel-bridge

- name: Test channel bridge
run: npm test
working-directory: packages/js/helm-channel-bridge

- name: Test Python wrapper
run: python -m unittest discover packages/python/helm_tool_wrapper/tests

Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ validate: test-js test-python samples verify-samples

test-js:
cd packages/js/helm-tool-wrapper && npm install && npm test
cd packages/js/helm-channel-bridge && npm install && npm test

test-python:
python3 -m unittest discover packages/python/helm_tool_wrapper/tests
Expand Down
19 changes: 19 additions & 0 deletions integrations/telegram/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Telegram + HELM

Telegram gives operators a phone surface for human-in-the-loop control of
governed agent sessions. HELM evaluates every inbound command before it may
execute — fail closed, receipted, deny by default.

Use `ChannelBridge`, `TelegramTransport`, and `createKernelEvaluator(...)`
from `@mindburn/helm-channel-bridge` (`packages/js/helm-channel-bridge`).

- Bot token via the `HELM_TELEGRAM_BOT_TOKEN` environment variable only.
- DMs only, explicit chat-ID allowlist, empty allowlist denies everyone.
- Every command materializes as a Kernel-evaluated turn; unknown commands are
denied. `autoPermission` only for explicitly allowlisted routine read-only
commands (`help`/`list`/`status` by default).
- `ask_human` questions are relayed to the chat; answers are Kernel-evaluated
before being routed back into the suspended turn.

WhatsApp/baileys is intentionally not supported (unofficial protocol,
account-ban and ToS risk).
99 changes: 99 additions & 0 deletions packages/js/helm-channel-bridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# @mindburn/helm-channel-bridge

HELM-governed channel bridge: human-in-the-loop by phone. Inbound chat
commands (Telegram today) become Kernel-evaluated governed turns; pending
`ask_human` questions are relayed to the phone and the answers routed back —
every hop receipted by the HELM AI Kernel.

This is a HELM-compatible example adapter. Command-bridge and transport
mechanisms are adapted from the Apache-2.0
[Rowboat](https://github.com/rowboatlabs/rowboat) project's ChannelBridge and
Telegram transport; the implementation here is original and adds HELM
governance semantics Rowboat does not have (Rowboat runs channel turns with
`autoPermission: true` and no per-command policy evaluation).

## Governance model (fail closed)

- **Every inbound command is Kernel-evaluated.** `help`, `list`, `status`,
`resume`, `new`, `stop`, chat turns, and `ask_human` answers each produce a
`/api/v1/evaluate` preflight with a distinct action URN
(`channel.<transport>.command.<name>`, `channel.<transport>.turn.run`,
`channel.<transport>.ask_human.answer`). Only an explicit `ALLOW` dispatches.
- **Deny by default.** Unknown verdicts, `ESCALATE`, evaluator outages, and
malformed responses are treated as denials. Unknown slash-commands are
denied locally without evaluation or dispatch.
- **autoPermission is allowlist-only.** It is granted only to commands in the
operator's explicit allowlist (default: the routine read-only commands
`help`/`list`/`status`). Chat turns run with `autoPermission: false` so tool
effects inside the turn still need Kernel/permission approval. Adding
`"chat"` to the allowlist restores Rowboat-style permission-less turns —
a deliberate, risky operator choice.
- **Transport is fail closed.** Telegram DMs only (group chats are ignored —
any member could otherwise drive the bridge), an explicit chat-and-sender-ID
allowlist (empty = deny everyone), an offset confirmed only after the
inbound handler settles (at-least-once delivery after a persistence failure),
and terminal handling for revoked tokens (401/404).

## Credentials

The Telegram bot token is read from the `HELM_TELEGRAM_BOT_TOKEN` environment
variable **only** — never from config files, command arguments, or inbound
messages. It is never logged or embedded in message text.

```bash
export HELM_TELEGRAM_BOT_TOKEN=... # from @BotFather
```

## Usage

```ts
import {
ChannelBridge,
TelegramTransport,
createKernelEvaluator,
telegramOptionsFromEnv,
} from "@mindburn/helm-channel-bridge";

const bridge = new ChannelBridge({
transportName: "telegram",
evaluator: createKernelEvaluator({
tenantId: process.env.HELM_TENANT_ID!,
apiKey: process.env.HELM_API_KEY!,
}),
sessions: myGovernedSessions, // ChannelSessions implementation
turnEvents: myTurnEventBus, // ChannelTurnEventSource implementation
});

const transport = new TelegramTransport(
telegramOptionsFromEnv(process.env, {
allowFrom: ["123456789"], // your Telegram chat ID
stateFile: ".helm/telegram-offset.json",
onInbound: (senderKey, chatId, text) =>
bridge.handleInbound(senderKey, text, (msg) => transport.send(chatId, msg)),
}),
);

await transport.start();
```

`ChannelSessions` / `ChannelTurnEventSource` are minimal interfaces your
governed runtime implements (principal-scoped session listing, create session,
send message, stop turn, respond to ask_human, settle-event stream). The
bridge only ever calls them after a Kernel `ALLOW`.

## Demo of a non-dispatching path

See `src/bridge.test.ts`: a `DENY` verdict (with receipt ID) blocks dispatch
and is reported to the sender; `ESCALATE`, unknown verdicts, evaluator
outages, and unknown slash-commands are all denied without touching the turn
engine.

## Development

```bash
npm install
npm test
```

Tests use a fake transport, fake session engine, fake evaluator, and an
in-memory event bus — no network, no credentials.
48 changes: 48 additions & 0 deletions packages/js/helm-channel-bridge/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions packages/js/helm-channel-bridge/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@mindburn/helm-channel-bridge",
"version": "0.1.0",
"description": "HELM-governed channel bridge: human-in-the-loop by phone with Kernel-evaluated turns (Telegram transport)",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/index.js",
"dist/index.d.ts",
"dist/bridge.js",
"dist/bridge.d.ts",
"dist/evaluator.js",
"dist/evaluator.d.ts",
"dist/telegram.js",
"dist/telegram.d.ts",
"README.md"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "npm run build && node --test \"dist/**/*.test.js\""
},
"keywords": [
"helm",
"ai",
"agents",
"hitl",
"telegram",
"channel",
"receipts"
],
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/Mindburn-Labs/helm-agent-integrations",
"directory": "packages/js/helm-channel-bridge"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^6.0.3"
}
}
Loading
Loading