Merge pull request #2 from TNovs1/claude/assurebot-final-aZsva

Update
This commit is contained in:
TNovs1 2026-01-30 03:24:42 -05:00 committed by GitHub
commit dcbf37ccde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 2555 additions and 734 deletions

562
README.md
View File

@ -1,518 +1,114 @@
# 🦞 OpenClaw — Personal AI Assistant
# AssureBot
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.png">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.png" alt="OpenClaw" width="500">
</picture>
</p>
**Lean, secure, self-hosted AI assistant for Railway.**
<p align="center">
<strong>EXFOLIATE! EXFOLIATE!</strong>
</p>
Your AI agent that runs on your infrastructure, answers only to you, and you can actually audit.
<p align="center">
<a href="https://github.com/openclaw/openclaw/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/openclaw/openclaw/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://github.com/openclaw/openclaw/releases"><img src="https://img.shields.io/github/v/release/openclaw/openclaw?include_prereleases&style=for-the-badge" alt="GitHub release"></a>
<a href="https://discord.gg/clawd"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
</p>
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/assurebot)
**OpenClaw** is a *personal AI assistant* you run on your own devices.
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, Microsoft Teams, WebChat), plus extension channels like BlueBubbles, Matrix, Zalo, and Zalo Personal. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
## Why AssureBot?
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
| Full OpenClaw | AssureBot |
|---------------|-----------|
| 12+ channels | Telegram only |
| File-based config | Env vars only |
| Plugins/extensions | None (locked down) |
| Desktop/mobile apps | Headless server |
| Complex setup | One-click deploy |
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/start/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-clawdbot) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
**Trade-off**: Less features, more trust.
Preferred setup: run the onboarding wizard (`openclaw onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Works with npm, pnpm, or bun.
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
## Features
**Subscriptions (OAuth):**
- **[Anthropic](https://www.anthropic.com/)** (Claude Pro/Max)
- **[OpenAI](https://openai.com/)** (ChatGPT/Codex)
- **Telegram Bot** — Allowlist-only access, no public commands
- **Image Analysis** — Send photos for AI analysis (Claude Vision / GPT-4V)
- **Webhook Receiver** — Authenticated HTTP endpoint for integrations
- **Docker Sandbox** — Isolated code execution (no network, dropped caps)
- **Cron Scheduler** — Time-based recurring tasks
- **Full Audit Log** — JSONL logs of every interaction
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for longcontext strength and better promptinjection resistance. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
## Quick Start
## Models (selection + auth)
- Models config + CLI: [Models](https://docs.openclaw.ai/concepts/models)
- Auth profile rotation (OAuth vs API keys) + fallbacks: [Model failover](https://docs.openclaw.ai/concepts/model-failover)
## Install (recommended)
Runtime: **Node ≥22**.
### Environment Variables
```bash
npm install -g openclaw@latest
# or: pnpm add -g openclaw@latest
# Required
TELEGRAM_BOT_TOKEN=your_bot_token
ALLOWED_USERS=123456789,987654321 # Telegram user IDs
openclaw onboard --install-daemon
# AI Provider (one required)
ANTHROPIC_API_KEY=sk-ant-...
# or
OPENAI_API_KEY=sk-...
# Optional
WEBHOOK_SECRET=auto-generated-if-empty
AUDIT_LOG_PATH=/data/audit.jsonl
SANDBOX_ENABLED=true
```
The wizard installs the Gateway daemon (launchd/systemd user service) so it stays running.
### Deploy to Railway
## Quick start (TL;DR)
1. Click the deploy button above
2. Set environment variables
3. Your bot is live
Runtime: **Node ≥22**.
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started)
### Run Locally
```bash
openclaw onboard --install-daemon
openclaw gateway --port 18789 --verbose
# Send a message
openclaw message send --to +1234567890 --message "Hello from OpenClaw"
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/Microsoft Teams/Matrix/Zalo/Zalo Personal/WebChat)
openclaw agent --message "Ship checklist" --thinking high
```
Upgrading? [Updating guide](https://docs.openclaw.ai/install/updating) (and run `openclaw doctor`).
## Development channels
- **stable**: tagged releases (`vYYYY.M.D` or `vYYYY.M.D-<patch>`), npm dist-tag `latest`.
- **beta**: prerelease tags (`vYYYY.M.D-beta.N`), npm dist-tag `beta` (macOS app may be missing).
- **dev**: moving head of `main`, npm dist-tag `dev` (when published).
Switch channels (git + npm): `openclaw update --channel stable|beta|dev`.
Details: [Development channels](https://docs.openclaw.ai/install/development-channels).
## From source (development)
Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly.
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
cd secure
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
pnpm openclaw onboard --install-daemon
# Dev loop (auto-reload on TS changes)
pnpm gateway:watch
pnpm start
```
Note: `pnpm openclaw ...` runs TypeScript directly (via `tsx`). `pnpm build` produces `dist/` for running via Node / the packaged `openclaw` binary.
### Docker
## Security defaults (DM access)
OpenClaw connects to real messaging surfaces. Treat inbound DMs as **untrusted input**.
Full security guide: [Security](https://docs.openclaw.ai/gateway/security)
Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Google Chat/Slack:
- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dm.policy="pairing"` / `channels.slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message.
- Approve with: `openclaw pairing approve <channel> <code>` (then the sender is added to a local allowlist store).
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`).
Run `openclaw doctor` to surface risky/misconfigured DM policies.
## Highlights
- **[Local-first Gateway](https://docs.openclaw.ai/gateway)** — single control plane for sessions, channels, tools, and events.
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android.
- **[Multi-agent routing](https://docs.openclaw.ai/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
- **[First-class tools](https://docs.openclaw.ai/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes).
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — wizard-driven setup with bundled/managed/workspace skills.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=openclaw/openclaw&type=date&legend=top-left)](https://www.star-history.com/#openclaw/openclaw&type=date&legend=top-left)
## Everything we built so far
### Core platform
- [Gateway WS control plane](https://docs.openclaw.ai/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.openclaw.ai/web), and [Canvas host](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [wizard](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor).
- [Pi agent runtime](https://docs.openclaw.ai/concepts/agent) in RPC mode with tool streaming and block streaming.
- [Session model](https://docs.openclaw.ai/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.openclaw.ai/concepts/groups).
- [Media pipeline](https://docs.openclaw.ai/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.openclaw.ai/nodes/audio).
### Channels
- [Channels](https://docs.openclaw.ai/channels): [WhatsApp](https://docs.openclaw.ai/channels/whatsapp) (Baileys), [Telegram](https://docs.openclaw.ai/channels/telegram) (grammY), [Slack](https://docs.openclaw.ai/channels/slack) (Bolt), [Discord](https://docs.openclaw.ai/channels/discord) (discord.js), [Google Chat](https://docs.openclaw.ai/channels/googlechat) (Chat API), [Signal](https://docs.openclaw.ai/channels/signal) (signal-cli), [iMessage](https://docs.openclaw.ai/channels/imessage) (imsg), [BlueBubbles](https://docs.openclaw.ai/channels/bluebubbles) (extension), [Microsoft Teams](https://docs.openclaw.ai/channels/msteams) (extension), [Matrix](https://docs.openclaw.ai/channels/matrix) (extension), [Zalo](https://docs.openclaw.ai/channels/zalo) (extension), [Zalo Personal](https://docs.openclaw.ai/channels/zalouser) (extension), [WebChat](https://docs.openclaw.ai/web/webchat).
- [Group routing](https://docs.openclaw.ai/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.openclaw.ai/channels).
### Apps + nodes
- [macOS app](https://docs.openclaw.ai/platforms/macos): menu bar control plane, [Voice Wake](https://docs.openclaw.ai/nodes/voicewake)/PTT, [Talk Mode](https://docs.openclaw.ai/nodes/talk) overlay, [WebChat](https://docs.openclaw.ai/web/webchat), debug tools, [remote gateway](https://docs.openclaw.ai/gateway/remote) control.
- [iOS node](https://docs.openclaw.ai/platforms/ios): [Canvas](https://docs.openclaw.ai/platforms/mac/canvas), [Voice Wake](https://docs.openclaw.ai/nodes/voicewake), [Talk Mode](https://docs.openclaw.ai/nodes/talk), camera, screen recording, Bonjour pairing.
- [Android node](https://docs.openclaw.ai/platforms/android): [Canvas](https://docs.openclaw.ai/platforms/mac/canvas), [Talk Mode](https://docs.openclaw.ai/nodes/talk), camera, screen recording, optional SMS.
- [macOS node mode](https://docs.openclaw.ai/nodes): system.run/notify + canvas/camera exposure.
### Tools + automation
- [Browser control](https://docs.openclaw.ai/tools/browser): dedicated openclaw Chrome/Chromium, snapshots, actions, uploads, profiles.
- [Canvas](https://docs.openclaw.ai/platforms/mac/canvas): [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui) push/reset, eval, snapshot.
- [Nodes](https://docs.openclaw.ai/nodes): camera snap/clip, screen record, [location.get](https://docs.openclaw.ai/nodes/location-command), notifications.
- [Cron + wakeups](https://docs.openclaw.ai/automation/cron-jobs); [webhooks](https://docs.openclaw.ai/automation/webhook); [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub).
- [Skills platform](https://docs.openclaw.ai/tools/skills): bundled, managed, and workspace skills with install gating + UI.
### Runtime + safety
- [Channel routing](https://docs.openclaw.ai/concepts/channel-routing), [retry policy](https://docs.openclaw.ai/concepts/retry), and [streaming/chunking](https://docs.openclaw.ai/concepts/streaming).
- [Presence](https://docs.openclaw.ai/concepts/presence), [typing indicators](https://docs.openclaw.ai/concepts/typing-indicators), and [usage tracking](https://docs.openclaw.ai/concepts/usage-tracking).
- [Models](https://docs.openclaw.ai/concepts/models), [model failover](https://docs.openclaw.ai/concepts/model-failover), and [session pruning](https://docs.openclaw.ai/concepts/session-pruning).
- [Security](https://docs.openclaw.ai/gateway/security) and [troubleshooting](https://docs.openclaw.ai/channels/troubleshooting).
### Ops + packaging
- [Control UI](https://docs.openclaw.ai/web) + [WebChat](https://docs.openclaw.ai/web/webchat) served directly from the Gateway.
- [Tailscale Serve/Funnel](https://docs.openclaw.ai/gateway/tailscale) or [SSH tunnels](https://docs.openclaw.ai/gateway/remote) with token/password auth.
- [Nix mode](https://docs.openclaw.ai/install/nix) for declarative config; [Docker](https://docs.openclaw.ai/install/docker)-based installs.
- [Doctor](https://docs.openclaw.ai/gateway/doctor) migrations, [logging](https://docs.openclaw.ai/logging).
## How it works (short)
```
WhatsApp / Telegram / Slack / Discord / Google Chat / Signal / iMessage / BlueBubbles / Microsoft Teams / Matrix / Zalo / Zalo Personal / WebChat
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ ws://127.0.0.1:18789 │
└──────────────┬────────────────┘
├─ Pi agent (RPC)
├─ CLI (openclaw …)
├─ WebChat UI
├─ macOS app
└─ iOS / Android nodes
```bash
docker build -t assurebot -f secure/Dockerfile .
docker run -d \
-e TELEGRAM_BOT_TOKEN=... \
-e ALLOWED_USERS=... \
-e ANTHROPIC_API_KEY=... \
assurebot
```
## Key subsystems
## Security Model
- **[Gateway WebSocket network](https://docs.openclaw.ai/concepts/architecture)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://docs.openclaw.ai/gateway)).
- **[Tailscale exposure](https://docs.openclaw.ai/gateway/tailscale)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://docs.openclaw.ai/gateway/remote)).
- **[Browser control](https://docs.openclaw.ai/tools/browser)** — openclawmanaged Chrome/Chromium with CDP control.
- **[Canvas + A2UI](https://docs.openclaw.ai/platforms/mac/canvas)** — agentdriven visual workspace (A2UI host: [Canvas/A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui)).
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — alwayson speech and continuous conversation.
- **[Nodes](https://docs.openclaw.ai/nodes)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOSonly `system.run`/`system.notify`.
- **No config files** — All secrets via environment variables
- **Allowlist only** — Only specified Telegram user IDs can interact
- **Timing-safe auth** — Webhook tokens compared safely
- **Sandbox isolation** — Code runs in Docker with no network, read-only root, dropped capabilities
- **Audit everything** — Every message, command, and action logged to JSONL
## Tailscale access (Gateway dashboard)
## Architecture
OpenClaw can auto-configure Tailscale **Serve** (tailnet-only) or **Funnel** (public) while the Gateway stays bound to loopback. Configure `gateway.tailscale.mode`:
- `off`: no Tailscale automation (default).
- `serve`: tailnet-only HTTPS via `tailscale serve` (uses Tailscale identity headers by default).
- `funnel`: public HTTPS via `tailscale funnel` (requires shared password auth).
Notes:
- `gateway.bind` must stay `loopback` when Serve/Funnel is enabled (OpenClaw enforces this).
- Serve can be forced to require a password by setting `gateway.auth.mode: "password"` or `gateway.auth.allowTailscale: false`.
- Funnel refuses to start unless `gateway.auth.mode: "password"` is set.
- Optional: `gateway.tailscale.resetOnExit` to undo Serve/Funnel on shutdown.
Details: [Tailscale guide](https://docs.openclaw.ai/gateway/tailscale) · [Web surfaces](https://docs.openclaw.ai/web)
## Remote Gateway (Linux is great)
Its perfectly fine to run the Gateway on a small Linux instance. Clients (macOS app, CLI, WebChat) can connect over **Tailscale Serve/Funnel** or **SSH tunnels**, and you can still pair device nodes (macOS/iOS/Android) to execute devicelocal actions when needed.
- **Gateway host** runs the exec tool and channel connections by default.
- **Device nodes** run devicelocal actions (`system.run`, camera, screen recording, notifications) via `node.invoke`.
In short: exec runs where the Gateway lives; device actions run where the device lives.
Details: [Remote access](https://docs.openclaw.ai/gateway/remote) · [Nodes](https://docs.openclaw.ai/nodes) · [Security](https://docs.openclaw.ai/gateway/security)
## macOS permissions via the Gateway protocol
The macOS app can run in **node mode** and advertises its capabilities + permission map over the Gateway WebSocket (`node.list` / `node.describe`). Clients can then execute local actions via `node.invoke`:
- `system.run` runs a local command and returns stdout/stderr/exit code; set `needsScreenRecording: true` to require screen-recording permission (otherwise youll get `PERMISSION_MISSING`).
- `system.notify` posts a user notification and fails if notifications are denied.
- `canvas.*`, `camera.*`, `screen.record`, and `location.get` are also routed via `node.invoke` and follow TCC permission status.
Elevated bash (host permissions) is separate from macOS TCC:
- Use `/elevated on|off` to toggle persession elevated access when enabled + allowlisted.
- Gateway persists the persession toggle via `sessions.patch` (WS method) alongside `thinkingLevel`, `verboseLevel`, `model`, `sendPolicy`, and `groupActivation`.
Details: [Nodes](https://docs.openclaw.ai/nodes) · [macOS app](https://docs.openclaw.ai/platforms/macos) · [Gateway protocol](https://docs.openclaw.ai/concepts/architecture)
## Agent to Agent (sessions_* tools)
- Use these to coordinate work across sessions without jumping between chat surfaces.
- `sessions_list` — discover active sessions (agents) and their metadata.
- `sessions_history` — fetch transcript logs for a session.
- `sessions_send` — message another session; optional replyback pingpong + announce step (`REPLY_SKIP`, `ANNOUNCE_SKIP`).
Details: [Session tools](https://docs.openclaw.ai/concepts/session-tool)
## Skills registry (ClawdHub)
ClawdHub is a minimal skill registry. With ClawdHub enabled, the agent can search for skills automatically and pull in new ones as needed.
[ClawdHub](https://ClawdHub.com)
## Chat commands
Send these in WhatsApp/Telegram/Slack/Google Chat/Microsoft Teams/WebChat (group commands are owner-only):
- `/status` — compact session status (model + tokens, cost when available)
- `/new` or `/reset` — reset the session
- `/compact` — compact session context (summary)
- `/think <level>` — off|minimal|low|medium|high|xhigh (GPT-5.2 + Codex models only)
- `/verbose on|off`
- `/usage off|tokens|full` — per-response usage footer
- `/restart` — restart the gateway (owner-only in groups)
- `/activation mention|always` — group activation toggle (groups only)
## Apps (optional)
The Gateway alone delivers a great experience. All apps are optional and add extra features.
If you plan to build/run companion apps, follow the platform runbooks below.
### macOS (OpenClaw.app) (optional)
- Menu bar control for the Gateway and health.
- Voice Wake + push-to-talk overlay.
- WebChat + debug tools.
- Remote gateway control over SSH.
Note: signed builds required for macOS permissions to stick across rebuilds (see `docs/mac/permissions.md`).
### iOS node (optional)
- Pairs as a node via the Bridge.
- Voice trigger forwarding + Canvas surface.
- Controlled via `openclaw nodes …`.
Runbook: [iOS connect](https://docs.openclaw.ai/platforms/ios).
### Android node (optional)
- Pairs via the same Bridge + pairing flow as iOS.
- Exposes Canvas, Camera, and Screen capture commands.
- Runbook: [Android connect](https://docs.openclaw.ai/platforms/android).
## Agent workspace + skills
- Workspace root: `~/.openclaw/workspace` (configurable via `agents.defaults.workspace`).
- Injected prompt files: `AGENTS.md`, `SOUL.md`, `TOOLS.md`.
- Skills: `~/.openclaw/workspace/skills/<skill>/SKILL.md`.
## Configuration
Minimal `~/.openclaw/openclaw.json` (model + defaults):
```json5
{
agent: {
model: "anthropic/claude-opus-4-5"
}
}
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Telegram │────▶│ AssureBot │────▶│ AI Agent │
│ (User) │◀────│ (Core) │◀────│ (Claude/ │
└─────────────┘ └─────────────┘ │ OpenAI) │
│ └─────────────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Webhooks │ │ Sandbox │ │ Scheduler│
└──────────┘ └──────────┘ └──────────┘
```
[Full configuration reference (all keys + examples).](https://docs.openclaw.ai/gateway/configuration)
## Commands
## Security model (important)
In Telegram, send:
- Any text message → AI responds
- Photo with caption → Image analysis
- `/sandbox <code>` → Run code in isolated container
- `/schedule <cron> <task>` → Create scheduled task
- `/tasks` → List scheduled tasks
- **Default:** tools run on the host for the **main** session, so the agent has full access when its just you.
- **Group/channel safety:** set `agents.defaults.sandbox.mode: "non-main"` to run **nonmain sessions** (groups/channels) inside persession Docker sandboxes; bash then runs in Docker for those sessions.
- **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
## Based On
Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker + sandboxing](https://docs.openclaw.ai/install/docker) · [Sandbox config](https://docs.openclaw.ai/gateway/configuration)
AssureBot is a hardened fork of [OpenClaw](https://github.com/openclaw/openclaw), stripped down for security-first self-hosting.
### [WhatsApp](https://docs.openclaw.ai/channels/whatsapp)
## License
- Link the device: `pnpm openclaw channels login` (stores creds in `~/.openclaw/credentials`).
- Allowlist who can talk to the assistant via `channels.whatsapp.allowFrom`.
- If `channels.whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [Telegram](https://docs.openclaw.ai/channels/telegram)
- Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` (env wins).
- Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` as needed.
```json5
{
channels: {
telegram: {
botToken: "123456:ABCDEF"
}
}
}
```
### [Slack](https://docs.openclaw.ai/channels/slack)
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `channels.slack.botToken` + `channels.slack.appToken`).
### [Discord](https://docs.openclaw.ai/channels/discord)
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins).
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.dm.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed.
```json5
{
channels: {
discord: {
token: "1234abcd"
}
}
}
```
### [Signal](https://docs.openclaw.ai/channels/signal)
- Requires `signal-cli` and a `channels.signal` config section.
### [iMessage](https://docs.openclaw.ai/channels/imessage)
- macOS only; Messages must be signed in.
- If `channels.imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [Microsoft Teams](https://docs.openclaw.ai/channels/msteams)
- Configure a Teams app + Bot Framework, then add a `msteams` config section.
- Allowlist who can talk via `msteams.allowFrom`; group access via `msteams.groupAllowFrom` or `msteams.groupPolicy: "open"`.
### [WebChat](https://docs.openclaw.ai/web/webchat)
- Uses the Gateway WebSocket; no separate WebChat port/config.
Browser control (optional):
```json5
{
browser: {
enabled: true,
color: "#FF4500"
}
}
```
## Docs
Use these when youre past the onboarding flow and want the deeper reference.
- [Start with the docs index for navigation and “whats where.”](https://docs.openclaw.ai)
- [Read the architecture overview for the gateway + protocol model.](https://docs.openclaw.ai/concepts/architecture)
- [Use the full configuration reference when you need every key and example.](https://docs.openclaw.ai/gateway/configuration)
- [Run the Gateway by the book with the operational runbook.](https://docs.openclaw.ai/gateway)
- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.openclaw.ai/web)
- [Understand remote access over SSH tunnels or tailnets.](https://docs.openclaw.ai/gateway/remote)
- [Follow the onboarding wizard flow for a guided setup.](https://docs.openclaw.ai/start/wizard)
- [Wire external triggers via the webhook surface.](https://docs.openclaw.ai/automation/webhook)
- [Set up Gmail Pub/Sub triggers.](https://docs.openclaw.ai/automation/gmail-pubsub)
- [Learn the macOS menu bar companion details.](https://docs.openclaw.ai/platforms/mac/menu-bar)
- [Platform guides: Windows (WSL2)](https://docs.openclaw.ai/platforms/windows), [Linux](https://docs.openclaw.ai/platforms/linux), [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android)
- [Debug common failures with the troubleshooting guide.](https://docs.openclaw.ai/channels/troubleshooting)
- [Review security guidance before exposing anything.](https://docs.openclaw.ai/gateway/security)
## Advanced docs (discovery + control)
- [Discovery + transports](https://docs.openclaw.ai/gateway/discovery)
- [Bonjour/mDNS](https://docs.openclaw.ai/gateway/bonjour)
- [Gateway pairing](https://docs.openclaw.ai/gateway/pairing)
- [Remote gateway README](https://docs.openclaw.ai/gateway/remote-gateway-readme)
- [Control UI](https://docs.openclaw.ai/web/control-ui)
- [Dashboard](https://docs.openclaw.ai/web/dashboard)
## Operations & troubleshooting
- [Health checks](https://docs.openclaw.ai/gateway/health)
- [Gateway lock](https://docs.openclaw.ai/gateway/gateway-lock)
- [Background process](https://docs.openclaw.ai/gateway/background-process)
- [Browser troubleshooting (Linux)](https://docs.openclaw.ai/tools/browser-linux-troubleshooting)
- [Logging](https://docs.openclaw.ai/logging)
## Deep dives
- [Agent loop](https://docs.openclaw.ai/concepts/agent-loop)
- [Presence](https://docs.openclaw.ai/concepts/presence)
- [TypeBox schemas](https://docs.openclaw.ai/concepts/typebox)
- [RPC adapters](https://docs.openclaw.ai/reference/rpc)
- [Queue](https://docs.openclaw.ai/concepts/queue)
## Workspace & skills
- [Skills config](https://docs.openclaw.ai/tools/skills-config)
- [Default AGENTS](https://docs.openclaw.ai/reference/AGENTS.default)
- [Templates: AGENTS](https://docs.openclaw.ai/reference/templates/AGENTS)
- [Templates: BOOTSTRAP](https://docs.openclaw.ai/reference/templates/BOOTSTRAP)
- [Templates: IDENTITY](https://docs.openclaw.ai/reference/templates/IDENTITY)
- [Templates: SOUL](https://docs.openclaw.ai/reference/templates/SOUL)
- [Templates: TOOLS](https://docs.openclaw.ai/reference/templates/TOOLS)
- [Templates: USER](https://docs.openclaw.ai/reference/templates/USER)
## Platform internals
- [macOS dev setup](https://docs.openclaw.ai/platforms/mac/dev-setup)
- [macOS menu bar](https://docs.openclaw.ai/platforms/mac/menu-bar)
- [macOS voice wake](https://docs.openclaw.ai/platforms/mac/voicewake)
- [iOS node](https://docs.openclaw.ai/platforms/ios)
- [Android node](https://docs.openclaw.ai/platforms/android)
- [Windows (WSL2)](https://docs.openclaw.ai/platforms/windows)
- [Linux app](https://docs.openclaw.ai/platforms/linux)
## Email hooks (Gmail)
- [docs.openclaw.ai/gmail-pubsub](https://docs.openclaw.ai/automation/gmail-pubsub)
## Molty
OpenClaw was built for **Molty**, a space lobster AI assistant. 🦞
by Peter Steinberger and the community.
- [openclaw.ai](https://openclaw.ai)
- [soul.md](https://soul.md)
- [steipete.me](https://steipete.me)
- [@openclaw](https://x.com/openclaw)
## Community
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
AI/vibe-coded PRs welcome! 🤖
Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for
[pi-mono](https://github.com/badlogic/pi-mono).
Special thanks to Adam Doppelt for lobster.bot.
Thanks to all clawtributors:
<p align="left">
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/plum-dawg"><img src="https://avatars.githubusercontent.com/u/5909950?v=4&s=48" width="48" height="48" alt="plum-dawg" title="plum-dawg"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/jaydenfyi"><img src="https://avatars.githubusercontent.com/u/213395523?v=4&s=48" width="48" height="48" alt="jaydenfyi" title="jaydenfyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a>
<a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a>
<a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
<a href="https://github.com/jonisjongithub"><img src="https://avatars.githubusercontent.com/u/86072337?v=4&s=48" width="48" height="48" alt="jonisjongithub" title="jonisjongithub"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a>
<a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/mousberg"><img src="https://avatars.githubusercontent.com/u/57605064?v=4&s=48" width="48" height="48" alt="mousberg" title="mousberg"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/hougangdev"><img src="https://avatars.githubusercontent.com/u/105773686?v=4&s=48" width="48" height="48" alt="hougangdev" title="hougangdev"/></a> <a href="https://github.com/shakkernerd"><img src="https://avatars.githubusercontent.com/u/165377636?v=4&s=48" width="48" height="48" alt="shakkernerd" title="shakkernerd"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/hirefrank"><img src="https://avatars.githubusercontent.com/u/183158?v=4&s=48" width="48" height="48" alt="hirefrank" title="hirefrank"/></a> <a href="https://github.com/joeynyc"><img src="https://avatars.githubusercontent.com/u/17919866?v=4&s=48" width="48" height="48" alt="joeynyc" title="joeynyc"/></a>
<a href="https://github.com/orlyjamie"><img src="https://avatars.githubusercontent.com/u/6668807?v=4&s=48" width="48" height="48" alt="orlyjamie" title="orlyjamie"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/rohannagpal"><img src="https://avatars.githubusercontent.com/u/4009239?v=4&s=48" width="48" height="48" alt="rohannagpal" title="rohannagpal"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/f-trycua"><img src="https://avatars.githubusercontent.com/u/195596869?v=4&s=48" width="48" height="48" alt="f-trycua" title="f-trycua"/></a>
<a href="https://github.com/benostein"><img src="https://avatars.githubusercontent.com/u/31802821?v=4&s=48" width="48" height="48" alt="benostein" title="benostein"/></a> <a href="https://github.com/elliotsecops"><img src="https://avatars.githubusercontent.com/u/141947839?v=4&s=48" width="48" height="48" alt="elliotsecops" title="elliotsecops"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a>
<a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a>
<a href="https://github.com/nonggialiang"><img src="https://avatars.githubusercontent.com/u/14367839?v=4&s=48" width="48" height="48" alt="nonggialiang" title="nonggialiang"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/dominicnunez"><img src="https://avatars.githubusercontent.com/u/43616264?v=4&s=48" width="48" height="48" alt="dominicnunez" title="dominicnunez"/></a> <a href="https://github.com/lploc94"><img src="https://avatars.githubusercontent.com/u/28453843?v=4&s=48" width="48" height="48" alt="lploc94" title="lploc94"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/AdeboyeDN"><img src="https://avatars.githubusercontent.com/u/65312338?v=4&s=48" width="48" height="48" alt="AdeboyeDN" title="AdeboyeDN"/></a> <a href="https://github.com/Alg0rix"><img src="https://avatars.githubusercontent.com/u/53804949?v=4&s=48" width="48" height="48" alt="Alg0rix" title="Alg0rix"/></a> <a href="https://github.com/papago2355"><img src="https://avatars.githubusercontent.com/u/68721273?v=4&s=48" width="48" height="48" alt="papago2355" title="papago2355"/></a>
<a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/rhuanssauro"><img src="https://avatars.githubusercontent.com/u/164682191?v=4&s=48" width="48" height="48" alt="rhuanssauro" title="rhuanssauro"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a>
<a href="https://github.com/ryancontent"><img src="https://avatars.githubusercontent.com/u/39743613?v=4&s=48" width="48" height="48" alt="ryancontent" title="ryancontent"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a>
<a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/uos-status"><img src="https://avatars.githubusercontent.com/u/255712580?v=4&s=48" width="48" height="48" alt="uos-status" title="uos-status"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a>
<a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/JonUleis"><img src="https://avatars.githubusercontent.com/u/7644941?v=4&s=48" width="48" height="48" alt="JonUleis" title="JonUleis"/></a> <a href="https://github.com/shivamraut101"><img src="https://avatars.githubusercontent.com/u/110457469?v=4&s=48" width="48" height="48" alt="shivamraut101" title="shivamraut101"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a>
<a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/kennyklee"><img src="https://avatars.githubusercontent.com/u/1432489?v=4&s=48" width="48" height="48" alt="kennyklee" title="kennyklee"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a>
<a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a>
<a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a>
<a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a>
<a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Joshua%20Mitchell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Joshua Mitchell" title="Joshua Mitchell"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a>
<a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/dguido"><img src="https://avatars.githubusercontent.com/u/294844?v=4&s=48" width="48" height="48" alt="dguido" title="dguido"/></a>
<a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/Lukavyi"><img src="https://avatars.githubusercontent.com/u/1013690?v=4&s=48" width="48" height="48" alt="Lukavyi" title="Lukavyi"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/pi0"><img src="https://avatars.githubusercontent.com/u/5158436?v=4&s=48" width="48" height="48" alt="pi0" title="pi0"/></a>
<a href="https://github.com/rmorse"><img src="https://avatars.githubusercontent.com/u/853547?v=4&s=48" width="48" height="48" alt="rmorse" title="rmorse"/></a> <a href="https://github.com/search?q=Roopak%20Nijhara"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Roopak Nijhara" title="Roopak Nijhara"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a>
<a href="https://github.com/EnzeD"><img src="https://avatars.githubusercontent.com/u/9866900?v=4&s=48" width="48" height="48" alt="EnzeD" title="EnzeD"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/itsjaydesu"><img src="https://avatars.githubusercontent.com/u/220390?v=4&s=48" width="48" height="48" alt="itsjaydesu" title="itsjaydesu"/></a> <a href="https://github.com/ivancasco"><img src="https://avatars.githubusercontent.com/u/2452858?v=4&s=48" width="48" height="48" alt="ivancasco" title="ivancasco"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
<a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/MarvinCui"><img src="https://avatars.githubusercontent.com/u/130876763?v=4&s=48" width="48" height="48" alt="MarvinCui" title="MarvinCui"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/search?q=Pocket%20Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pocket Clawd" title="Pocket Clawd"/></a>
<a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/Suksham-sharma"><img src="https://avatars.githubusercontent.com/u/94667656?v=4&s=48" width="48" height="48" alt="Suksham-sharma" title="Suksham-sharma"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/tewatia"><img src="https://avatars.githubusercontent.com/u/22875334?v=4&s=48" width="48" height="48" alt="tewatia" title="tewatia"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/0oAstro"><img src="https://avatars.githubusercontent.com/u/79555780?v=4&s=48" width="48" height="48" alt="0oAstro" title="0oAstro"/></a>
<a href="https://github.com/abhaymundhara"><img src="https://avatars.githubusercontent.com/u/62872231?v=4&s=48" width="48" height="48" alt="abhaymundhara" title="abhaymundhara"/></a> <a href="https://github.com/aduk059"><img src="https://avatars.githubusercontent.com/u/257603478?v=4&s=48" width="48" height="48" alt="aduk059" title="aduk059"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/Alex-Alaniz"><img src="https://avatars.githubusercontent.com/u/88956822?v=4&s=48" width="48" height="48" alt="Alex-Alaniz" title="Alex-Alaniz"/></a> <a href="https://github.com/alexstyl"><img src="https://avatars.githubusercontent.com/u/1665273?v=4&s=48" width="48" height="48" alt="alexstyl" title="alexstyl"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/araa47"><img src="https://avatars.githubusercontent.com/u/22760261?v=4&s=48" width="48" height="48" alt="araa47" title="araa47"/></a> <a href="https://github.com/arthyn"><img src="https://avatars.githubusercontent.com/u/5466421?v=4&s=48" width="48" height="48" alt="arthyn" title="arthyn"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a>
<a href="https://github.com/bguidolim"><img src="https://avatars.githubusercontent.com/u/987360?v=4&s=48" width="48" height="48" alt="bguidolim" title="bguidolim"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/chenyuan99"><img src="https://avatars.githubusercontent.com/u/25518100?v=4&s=48" width="48" height="48" alt="chenyuan99" title="chenyuan99"/></a> <a href="https://github.com/search?q=OpenClaw%20Maintainers"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="OpenClaw Maintainers" title="OpenClaw Maintainers"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/dasilva333"><img src="https://avatars.githubusercontent.com/u/947827?v=4&s=48" width="48" height="48" alt="dasilva333" title="dasilva333"/></a> <a href="https://github.com/David-Marsh-Photo"><img src="https://avatars.githubusercontent.com/u/228404527?v=4&s=48" width="48" height="48" alt="David-Marsh-Photo" title="David-Marsh-Photo"/></a> <a href="https://github.com/search?q=Developer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Developer" title="Developer"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a>
<a href="https://github.com/dylanneve1"><img src="https://avatars.githubusercontent.com/u/31746704?v=4&s=48" width="48" height="48" alt="dylanneve1" title="dylanneve1"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/foeken"><img src="https://avatars.githubusercontent.com/u/13864?v=4&s=48" width="48" height="48" alt="foeken" title="foeken"/></a> <a href="https://github.com/frankekn"><img src="https://avatars.githubusercontent.com/u/4488090?v=4&s=48" width="48" height="48" alt="frankekn" title="frankekn"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/grrowl"><img src="https://avatars.githubusercontent.com/u/907140?v=4&s=48" width="48" height="48" alt="grrowl" title="grrowl"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a>
<a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jane"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jane" title="Jane"/></a> <a href="https://github.com/search?q=Jarvis%20Deploy"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis Deploy" title="Jarvis Deploy"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/jogi47"><img src="https://avatars.githubusercontent.com/u/1710139?v=4&s=48" width="48" height="48" alt="jogi47" title="jogi47"/></a> <a href="https://github.com/kentaro"><img src="https://avatars.githubusercontent.com/u/3458?v=4&s=48" width="48" height="48" alt="kentaro" title="kentaro"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kira-ariaki"><img src="https://avatars.githubusercontent.com/u/257352493?v=4&s=48" width="48" height="48" alt="kira-ariaki" title="kira-ariaki"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/Kiwitwitter"><img src="https://avatars.githubusercontent.com/u/25277769?v=4&s=48" width="48" height="48" alt="Kiwitwitter" title="Kiwitwitter"/></a>
<a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/longjos"><img src="https://avatars.githubusercontent.com/u/740160?v=4&s=48" width="48" height="48" alt="longjos" title="longjos"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/louzhixian"><img src="https://avatars.githubusercontent.com/u/7994361?v=4&s=48" width="48" height="48" alt="louzhixian" title="louzhixian"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/mertcicekci0"><img src="https://avatars.githubusercontent.com/u/179321902?v=4&s=48" width="48" height="48" alt="mertcicekci0" title="mertcicekci0"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a>
<a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/Noctivoro"><img src="https://avatars.githubusercontent.com/u/183974570?v=4&s=48" width="48" height="48" alt="Noctivoro" title="Noctivoro"/></a> <a href="https://github.com/ppamment"><img src="https://avatars.githubusercontent.com/u/2122919?v=4&s=48" width="48" height="48" alt="ppamment" title="ppamment"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a>
<a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/senoldogann"><img src="https://avatars.githubusercontent.com/u/45736551?v=4&s=48" width="48" height="48" alt="senoldogann" title="senoldogann"/></a> <a href="https://github.com/sergical"><img src="https://avatars.githubusercontent.com/u/3760543?v=4&s=48" width="48" height="48" alt="sergical" title="sergical"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/shiyuanhai"><img src="https://avatars.githubusercontent.com/u/1187370?v=4&s=48" width="48" height="48" alt="shiyuanhai" title="shiyuanhai"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/search?q=techboss"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="techboss" title="techboss"/></a>
<a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/search?q=wolfred"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="wolfred" title="wolfred"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/YangHuang2280"><img src="https://avatars.githubusercontent.com/u/201681634?v=4&s=48" width="48" height="48" alt="YangHuang2280" title="YangHuang2280"/></a>
<a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/YiWang24"><img src="https://avatars.githubusercontent.com/u/176262341?v=4&s=48" width="48" height="48" alt="YiWang24" title="YiWang24"/></a> <a href="https://github.com/search?q=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/zackerthescar"><img src="https://avatars.githubusercontent.com/u/38077284?v=4&s=48" width="48" height="48" alt="zackerthescar" title="zackerthescar"/></a> <a href="https://github.com/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a>
<a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a>
<a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
</p>
MIT

View File

@ -1,4 +1,4 @@
# Moltbot Secure Edition
# AssureBot Edition
A lean, secure, self-hosted AI assistant for Railway deployment.
@ -25,7 +25,7 @@ A lean, secure, self-hosted AI assistant for Railway deployment.
```
┌────────────────────────────────────────────────────────────┐
MOLTBOT SECURE
ASSUREBOT
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Telegram │ │ Webhooks │ │ Scheduler │ │

174
pnpm-lock.yaml generated
View File

@ -314,7 +314,7 @@ importers:
specifier: ^10.5.0
version: 10.5.0
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -322,7 +322,7 @@ importers:
extensions/line:
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -348,7 +348,7 @@ importers:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -356,7 +356,7 @@ importers:
extensions/memory-core:
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -386,7 +386,7 @@ importers:
express:
specifier: ^5.2.1
version: 5.2.1
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
proper-lockfile:
@ -397,12 +397,12 @@ importers:
extensions/nostr:
dependencies:
moltbot:
specifier: workspace:*
version: link:../..
nostr-tools:
specifier: ^2.20.0
version: 2.20.0(typescript@5.9.3)
openclaw:
specifier: workspace:*
version: link:../..
zod:
specifier: ^4.3.6
version: 4.3.6
@ -439,7 +439,7 @@ importers:
specifier: ^4.3.5
version: 4.3.6
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -459,7 +459,7 @@ importers:
extensions/zalo:
dependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
undici:
@ -471,21 +471,40 @@ importers:
'@sinclair/typebox':
specifier: 0.34.47
version: 0.34.47
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
packages/clawdbot:
dependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
packages/moltbot:
secure:
dependencies:
openclaw:
specifier: workspace:*
version: link:../..
'@anthropic-ai/sdk':
specifier: ^0.39.0
version: 0.39.0
cron:
specifier: ^3.1.7
version: 3.5.0
grammy:
specifier: ^1.21.1
version: 1.39.3
openai:
specifier: ^4.77.0
version: 4.104.0(ws@8.19.0)(zod@3.25.76)
devDependencies:
'@types/node':
specifier: ^22.10.2
version: 22.19.7
tsx:
specifier: ^4.7.0
version: 4.21.0
typescript:
specifier: ^5.3.3
version: 5.9.3
ui:
dependencies:
@ -525,6 +544,9 @@ packages:
peerDependencies:
zod: ^3.25.0 || ^4.0.0
'@anthropic-ai/sdk@0.39.0':
resolution: {integrity: sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==}
'@anthropic-ai/sdk@0.71.2':
resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==}
hasBin: true
@ -2725,6 +2747,9 @@ packages:
'@types/long@4.0.2':
resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==}
'@types/luxon@3.4.2':
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
@ -2740,12 +2765,21 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node-fetch@2.6.13':
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
'@types/node@10.17.60':
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@20.19.30':
resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==}
'@types/node@22.19.7':
resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
'@types/node@24.10.9':
resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==}
@ -2958,6 +2992,10 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
agentkeepalive@4.6.0:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'}
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
@ -3325,6 +3363,9 @@ packages:
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
cron@3.5.0:
resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==}
croner@9.1.0:
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
engines: {node: '>=18.0'}
@ -3633,6 +3674,9 @@ packages:
forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
form-data@2.3.3:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
engines: {node: '>= 0.12'}
@ -3645,6 +3689,10 @@ packages:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
formdata-node@4.4.1:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
@ -3839,6 +3887,9 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@ -4232,6 +4283,10 @@ packages:
lucide@0.563.0:
resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==}
luxon@3.5.0:
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
engines: {node: '>=12'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@ -4520,6 +4575,18 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
openai@4.104.0:
resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
peerDependenciesMeta:
ws:
optional: true
zod:
optional: true
openai@6.10.0:
resolution: {integrity: sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A==}
hasBin: true
@ -5313,6 +5380,9 @@ packages:
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
engines: {node: '>=18'}
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@ -5462,6 +5532,10 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -5583,6 +5657,18 @@ snapshots:
dependencies:
zod: 4.3.6
'@anthropic-ai/sdk@0.39.0':
dependencies:
'@types/node': 18.19.130
'@types/node-fetch': 2.6.13
abort-controller: 3.0.0
agentkeepalive: 4.6.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
'@anthropic-ai/sdk@0.71.2(zod@4.3.6)':
dependencies:
json-schema-to-ts: 3.1.1
@ -8502,6 +8588,8 @@ snapshots:
'@types/long@4.0.2': {}
'@types/luxon@3.4.2': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
@ -8515,12 +8603,25 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 22.19.7
form-data: 4.0.5
'@types/node@10.17.60': {}
'@types/node@18.19.130':
dependencies:
undici-types: 5.26.5
'@types/node@20.19.30':
dependencies:
undici-types: 6.21.0
'@types/node@22.19.7':
dependencies:
undici-types: 6.21.0
'@types/node@24.10.9':
dependencies:
undici-types: 7.16.0
@ -8808,6 +8909,10 @@ snapshots:
agent-base@7.1.4: {}
agentkeepalive@4.6.0:
dependencies:
humanize-ms: 1.2.1
ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
@ -9210,6 +9315,11 @@ snapshots:
core-util-is@1.0.3: {}
cron@3.5.0:
dependencies:
'@types/luxon': 3.4.2
luxon: 3.5.0
croner@9.1.0: {}
cross-fetch@4.1.0:
@ -9573,6 +9683,8 @@ snapshots:
forever-agent@0.6.1: {}
form-data-encoder@1.7.2: {}
form-data@2.3.3:
dependencies:
asynckit: 0.4.0
@ -9596,6 +9708,11 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
formdata-node@4.4.1:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
@ -9836,6 +9953,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@ -10236,6 +10357,8 @@ snapshots:
lucide@0.563.0: {}
luxon@3.5.0: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -10553,6 +10676,21 @@ snapshots:
mimic-function: 5.0.1
optional: true
openai@4.104.0(ws@8.19.0)(zod@3.25.76):
dependencies:
'@types/node': 18.19.130
'@types/node-fetch': 2.6.13
abort-controller: 3.0.0
agentkeepalive: 4.6.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0
optionalDependencies:
ws: 8.19.0
zod: 3.25.76
transitivePeerDependencies:
- encoding
openai@6.10.0(ws@8.19.0)(zod@4.3.6):
optionalDependencies:
ws: 8.19.0
@ -11500,6 +11638,8 @@ snapshots:
uint8array-extras@1.5.0: {}
undici-types@5.26.5: {}
undici-types@6.21.0: {}
undici-types@7.16.0: {}
@ -11614,6 +11754,8 @@ snapshots:
web-streams-polyfill@3.3.3: {}
web-streams-polyfill@4.0.0-beta.3: {}
webidl-conversions@3.0.1: {}
whatwg-fetch@3.6.20: {}

View File

@ -1,6 +1,7 @@
packages:
- .
- ui
- secure
- packages/*
- extensions/*

58
railway-template.json Normal file
View File

@ -0,0 +1,58 @@
{
"$schema": "https://railway.app/railway.schema.json",
"name": "AssureBot",
"description": "Lean, secure, self-hosted AI assistant with Telegram, document analysis, and scheduled tasks",
"services": [
{
"name": "assurebot",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "secure/Dockerfile",
"watchPatterns": ["secure/**"]
},
"deploy": {
"startCommand": "node dist/index.js",
"healthcheckPath": "/health",
"healthcheckTimeout": 60,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
},
"variables": {
"DATABASE_URL": "${{Postgres.DATABASE_URL}}",
"REDIS_URL": "${{Redis.REDIS_URL}}",
"TELEGRAM_BOT_TOKEN": {
"description": "Telegram bot token from @BotFather",
"required": true
},
"ALLOWED_USERS": {
"description": "Comma-separated Telegram user IDs",
"required": true
},
"ANTHROPIC_API_KEY": {
"description": "Anthropic API key (or use OPENAI_API_KEY or OPENROUTER_API_KEY)",
"required": false
},
"OPENAI_API_KEY": {
"description": "OpenAI API key",
"required": false
},
"OPENROUTER_API_KEY": {
"description": "OpenRouter API key (100+ models)",
"required": false
},
"AI_MODEL": {
"description": "Model override (e.g., claude-3-5-sonnet-20241022)",
"required": false
}
}
},
{
"name": "Postgres",
"plugin": "postgresql"
},
{
"name": "Redis",
"plugin": "redis"
}
]
}

View File

@ -1,43 +1,42 @@
# Moltbot Secure - Minimal Docker Image
# AssureBot - Standalone Docker Image
# Lean, secure, self-hosted AI assistant for Railway
#
# Build from repo root: docker build -f secure/Dockerfile .
# Or set Railway root directory to: secure/
FROM node:22-slim AS builder
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY secure/package.json ./secure/
# Copy package files (handles both root and secure/ as context)
COPY package*.json ./
COPY tsconfig.json* ./
COPY *.ts ./
COPY *.d.ts ./
# Install dependencies
RUN pnpm install --frozen-lockfile --prod=false
# Copy source
COPY secure/ ./secure/
COPY tsconfig.json ./
RUN npm install --omit=dev=false
# Build TypeScript
RUN pnpm exec tsc --project secure/tsconfig.json
RUN npm run build
# Production image
FROM node:22-slim AS runner
# Security: Run as non-root user
RUN useradd -m -u 1000 moltbot
USER moltbot
# Security: Run as non-root user (use different UID since 1000 exists)
RUN useradd -m -u 1001 -s /bin/bash assurebot
WORKDIR /app
# Copy built files and production deps
COPY --from=builder --chown=moltbot:moltbot /app/node_modules ./node_modules
COPY --from=builder --chown=moltbot:moltbot /app/secure/dist ./dist
COPY --from=builder --chown=moltbot:moltbot /app/package.json ./
COPY --from=builder --chown=assurebot:assurebot /app/node_modules ./node_modules
COPY --from=builder --chown=assurebot:assurebot /app/dist ./dist
COPY --from=builder --chown=assurebot:assurebot /app/package.json ./
# Create data directory for audit logs
RUN mkdir -p /app/data
# Create data directory for audit logs (before switching user)
RUN mkdir -p /app/data && chown assurebot:assurebot /app/data
USER assurebot
ENV NODE_ENV=production
ENV PORT=8080
@ -45,7 +44,7 @@ ENV PORT=8080
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD node -e "fetch('http://localhost:8080/health').then(r => process.exit(r.ok ? 0 : 1))" || exit 1
CMD ["node", "dist/index.js"]

View File

@ -1,12 +1,12 @@
# Moltbot Secure
# AssureBot
**Lean, secure, self-hosted AI assistant for Railway.**
Your AI agent that runs on your infrastructure, answers only to you, and you can actually audit.
## Why Secure Edition?
## Why AssureBot?
| Full Moltbot | Secure Edition |
| Full Moltbot | AssureBot |
|--------------|----------------|
| 12+ channels | Telegram only |
| File-based config | Env vars only |
@ -21,10 +21,17 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
```
┌─────────────────────────────────────────────────────┐
│ TELEGRAM (your secure UI) │
│ ├── Chat with AI (text, voice, images) │
│ ├── Chat with AI (text, images, documents) │
│ ├── Code execution (15+ languages) │
│ ├── Forward anything → get analysis │
│ └── /commands for actions │
├─────────────────────────────────────────────────────┤
│ CODE EXECUTION │
│ ├── /js, /python, /ts, /bash - Quick execute │
│ ├── /run <lang> <code> - Any language │
│ ├── Docker (local) or Piston API (cloud) │
│ └── Isolated, no network, resource limits │
├─────────────────────────────────────────────────────┤
│ WEBHOOKS IN (authenticated) │
│ ├── GitHub → "PR merged, here's the summary" │
│ ├── Uptime → "Site down, checking why..." │
@ -35,26 +42,46 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
│ ├── Monitor RSS/sites │
│ └── Recurring research │
├─────────────────────────────────────────────────────┤
SANDBOX (isolated execution)
│ ├── Docker container
│ ├── No network by default
│ └── Resource limits
PERSISTENCE (optional)
│ ├── PostgreSQL - Tasks, user profiles
│ ├── Redis - Conversations, cache
│ └── Personality learning per user
└─────────────────────────────────────────────────────┘
```
## Commands
| Command | Description |
|---------|-------------|
| `/js <code>` | Run JavaScript |
| `/python <code>` | Run Python |
| `/ts <code>` | Run TypeScript |
| `/bash <code>` | Run shell commands |
| `/run <lang> <code>` | Run any language |
| `/status` | Bot & sandbox status |
| `/clear` | Clear conversation |
| `/schedule` | Schedule AI tasks |
| `/tasks` | List scheduled tasks |
| `/help` | Full command list |
**Supported Languages**: python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php
## Deploy to Railway
### One-Click
### One-Click (Recommended)
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/moltbot-secure)
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template?template=https://github.com/TNovs1/moltbot/tree/main&envs=TELEGRAM_BOT_TOKEN,ALLOWED_USERS,ANTHROPIC_API_KEY)
This auto-provisions PostgreSQL and Redis for persistence.
### Manual
1. Fork this repo
2. Create Railway project from GitHub
3. Set environment variables (see below)
4. Add volume at `/data`
5. Deploy
3. **Set Root Directory to `secure`**
4. Set environment variables (see below)
5. Optionally add PostgreSQL and Redis services
6. Deploy
## Configuration
@ -65,23 +92,34 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
```bash
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # From @BotFather
ALLOWED_USERS=123456789,987654321 # Telegram user IDs
ANTHROPIC_API_KEY=sk-ant-... # Or OPENAI_API_KEY
# Pick ONE AI provider:
ANTHROPIC_API_KEY=sk-ant-... # Claude
OPENAI_API_KEY=sk-... # GPT-4
OPENROUTER_API_KEY=sk-or-... # 100+ models
```
### Optional
```bash
# Webhooks
WEBHOOK_SECRET=random-32-chars # Auto-generated if missing
WEBHOOK_BASE_PATH=/hooks # Default: /hooks
# AI Model (optional - uses sensible defaults)
AI_MODEL=claude-sonnet-4-20250514 # or gpt-4o, etc.
# Sandbox
SANDBOX_ENABLED=true # Default: true
# Storage (auto-wired on Railway template)
DATABASE_URL=postgres://... # PostgreSQL
REDIS_URL=redis://... # Redis
# Sandbox (enabled by default)
SANDBOX_ENABLED=true # Auto-detects Docker or Piston API
SANDBOX_NETWORK=none # none | bridge
SANDBOX_MEMORY=512m
SANDBOX_CPUS=1
SANDBOX_TIMEOUT_MS=60000
# Webhooks
WEBHOOK_SECRET=random-32-chars # Auto-generated if missing
WEBHOOK_BASE_PATH=/hooks # Default: /hooks
# Scheduler
SCHEDULER_ENABLED=true # Default: true
@ -102,10 +140,18 @@ HOST=0.0.0.0
|---------|----------------|
| **Access** | Telegram user ID allowlist |
| **Auth** | Timing-safe token comparison |
| **Sandbox** | Docker: no network, read-only root, caps dropped |
| **Sandbox** | Docker (local) or Piston API (cloud), isolated |
| **Secrets** | Env-only, auto-redacted in logs |
| **Audit** | Every interaction logged |
### Sandbox Backends
AssureBot auto-detects the best available backend:
1. **Docker** - Full isolation, no network, caps dropped (requires Docker socket)
2. **Piston API** - Free cloud execution, 15+ languages (works on Railway/Render/Fly)
3. **None** - Sandbox disabled if neither available
### What's NOT Included
Intentionally removed:
@ -121,17 +167,17 @@ Intentionally removed:
```bash
cd secure
pnpm install
npm install
# Dev mode
TELEGRAM_BOT_TOKEN=xxx \
ANTHROPIC_API_KEY=xxx \
ALLOWED_USERS=123456789 \
pnpm dev
npm run dev
# Production
pnpm build
pnpm start
npm run build
npm start
```
## Endpoints
@ -162,24 +208,27 @@ All webhooks are:
```jsonl
{"ts":"2024-01-15T10:30:00Z","type":"message","userId":123,"text":"Hello","response":"Hi!"}
{"ts":"2024-01-15T10:30:05Z","type":"webhook","path":"/hooks/github","status":200}
{"ts":"2024-01-15T10:30:10Z","type":"sandbox","command":"python -c 'print(1)'","exitCode":0}
{"ts":"2024-01-15T10:30:10Z","type":"sandbox","command":"[python] print(1)","exitCode":0}
```
## Architecture
```
┌────────────────────┐ ┌────────────────────┐
moltbot-secure │────▶│ sandbox
│ (main container) │ │ (Docker sidecar)
AssureBot │────▶│ Sandbox
│ (main container) │ │ (Docker/Piston)
│ │ │ │
│ • Telegram bot │ │ • Isolated exec
│ • Webhook recv │ │ • No network
│ • Scheduler │ │ • Resource limits
│ • Allowlist auth │ │ • Ephemeral
│ • Telegram bot │ │ • Code execution
│ • Webhook recv │ │ • 15+ languages
│ • Scheduler │ │ • Isolated
│ • Personality │ │ • No network
└────────────────────┘ └────────────────────┘
├────▶ [PostgreSQL] - Tasks, profiles
├────▶ [Redis] - Conversations, cache
[Anthropic/OpenAI]
[Anthropic/OpenAI/OpenRouter]
(Direct API calls)
```

View File

@ -1,7 +1,7 @@
/**
* Moltbot Secure - Agent Core
* AssureBot - Agent Core
*
* Minimal AI agent that handles conversations.
* Minimal AI agent that handles conversations with image support.
* Direct API calls to Anthropic or OpenAI - no intermediaries.
*/
@ -10,9 +10,22 @@ import OpenAI from "openai";
import type { SecureConfig } from "./config.js";
import type { AuditLogger } from "./audit.js";
export type ImageContent = {
type: "image";
data: string; // base64
mediaType: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
};
export type TextContent = {
type: "text";
text: string;
};
export type MessageContent = string | (TextContent | ImageContent)[];
export type Message = {
role: "user" | "assistant";
content: string;
content: MessageContent;
};
export type AgentResponse = {
@ -25,13 +38,15 @@ export type AgentResponse = {
export type AgentCore = {
chat: (messages: Message[], systemPrompt?: string) => Promise<AgentResponse>;
provider: "anthropic" | "openai";
analyzeImage: (imageData: string, mediaType: ImageContent["mediaType"], prompt?: string) => Promise<AgentResponse>;
provider: "anthropic" | "openai" | "openrouter";
};
const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
const DEFAULT_OPENAI_MODEL = "gpt-4o";
const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-3.5-sonnet";
const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant running as a secure, self-hosted bot.
const DEFAULT_SYSTEM_PROMPT = `You are AssureBot, a helpful AI assistant running as a secure Telegram bot.
You are direct, concise, and helpful. You can:
- Answer questions and have conversations
@ -39,7 +54,17 @@ You are direct, concise, and helpful. You can:
- Help with coding and technical tasks
- Summarize content and extract information
When you receive webhook notifications, summarize them helpfully for the user.
## Available Commands (tell users about these when relevant)
- /js <code> - Run JavaScript
- /python <code> - Run Python
- /ts <code> - Run TypeScript
- /bash <code> - Run shell commands
- /run <lang> <code> - Run code in any language (python, js, ts, bash, rust, go, c, cpp, java, ruby, php)
- /status - Check bot status
- /clear - Clear conversation history
When users ask to run or test code, guide them to use the appropriate command.
Example: "Use /js console.log('hello')" or "Try /python print('hello')"
Be security-conscious:
- Never reveal API keys, tokens, or secrets
@ -53,8 +78,28 @@ function createAnthropicAgent(config: SecureConfig, audit: AuditLogger): AgentCo
const model = config.ai.model || DEFAULT_ANTHROPIC_MODEL;
function convertContent(content: MessageContent): Anthropic.MessageParam["content"] {
if (typeof content === "string") {
return content;
}
return content.map((part) => {
if (part.type === "text") {
return { type: "text" as const, text: part.text };
}
return {
type: "image" as const,
source: {
type: "base64" as const,
media_type: part.mediaType,
data: part.data,
},
};
});
}
return {
provider: "anthropic",
async chat(messages: Message[], systemPrompt?: string): Promise<AgentResponse> {
try {
const response = await client.messages.create({
@ -63,7 +108,7 @@ function createAnthropicAgent(config: SecureConfig, audit: AuditLogger): AgentCo
system: systemPrompt || DEFAULT_SYSTEM_PROMPT,
messages: messages.map((m) => ({
role: m.role,
content: m.content,
content: convertContent(m.content),
})),
});
@ -86,6 +131,23 @@ function createAnthropicAgent(config: SecureConfig, audit: AuditLogger): AgentCo
throw err;
}
},
async analyzeImage(
imageData: string,
mediaType: ImageContent["mediaType"],
prompt = "What's in this image? Describe it in detail."
): Promise<AgentResponse> {
const messages: Message[] = [
{
role: "user",
content: [
{ type: "image", data: imageData, mediaType },
{ type: "text", text: prompt },
],
},
];
return this.chat(messages);
},
};
}
@ -96,20 +158,53 @@ function createOpenAIAgent(config: SecureConfig, audit: AuditLogger): AgentCore
const model = config.ai.model || DEFAULT_OPENAI_MODEL;
type OpenAIContent = OpenAI.ChatCompletionContentPart[];
function convertContent(content: MessageContent): string | OpenAIContent {
if (typeof content === "string") {
return content;
}
return content.map((part) => {
if (part.type === "text") {
return { type: "text" as const, text: part.text };
}
return {
type: "image_url" as const,
image_url: {
url: `data:${part.mediaType};base64,${part.data}`,
},
};
});
}
return {
provider: "openai",
async chat(messages: Message[], systemPrompt?: string): Promise<AgentResponse> {
try {
const openaiMessages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt || DEFAULT_SYSTEM_PROMPT },
];
for (const m of messages) {
if (m.role === "user") {
openaiMessages.push({
role: "user",
content: convertContent(m.content),
});
} else {
// Assistant messages are always text
openaiMessages.push({
role: "assistant",
content: typeof m.content === "string" ? m.content : "",
});
}
}
const response = await client.chat.completions.create({
model,
max_tokens: 4096,
messages: [
{ role: "system", content: systemPrompt || DEFAULT_SYSTEM_PROMPT },
...messages.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
],
messages: openaiMessages,
});
const text = response.choices[0]?.message?.content || "";
@ -130,6 +225,122 @@ function createOpenAIAgent(config: SecureConfig, audit: AuditLogger): AgentCore
throw err;
}
},
async analyzeImage(
imageData: string,
mediaType: ImageContent["mediaType"],
prompt = "What's in this image? Describe it in detail."
): Promise<AgentResponse> {
const messages: Message[] = [
{
role: "user",
content: [
{ type: "image", data: imageData, mediaType },
{ type: "text", text: prompt },
],
},
];
return this.chat(messages);
},
};
}
function createOpenRouterAgent(config: SecureConfig, audit: AuditLogger): AgentCore {
// OpenRouter uses OpenAI-compatible API
const client = new OpenAI({
apiKey: config.ai.apiKey,
baseURL: "https://openrouter.ai/api/v1",
defaultHeaders: {
"HTTP-Referer": "https://github.com/TNovs1/moltbot",
"X-Title": "AssureBot",
},
});
const model = config.ai.model || DEFAULT_OPENROUTER_MODEL;
type OpenAIContent = OpenAI.ChatCompletionContentPart[];
function convertContent(content: MessageContent): string | OpenAIContent {
if (typeof content === "string") {
return content;
}
return content.map((part) => {
if (part.type === "text") {
return { type: "text" as const, text: part.text };
}
return {
type: "image_url" as const,
image_url: {
url: `data:${part.mediaType};base64,${part.data}`,
},
};
});
}
return {
provider: "openrouter",
async chat(messages: Message[], systemPrompt?: string): Promise<AgentResponse> {
try {
const openaiMessages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt || DEFAULT_SYSTEM_PROMPT },
];
for (const m of messages) {
if (m.role === "user") {
openaiMessages.push({
role: "user",
content: convertContent(m.content),
});
} else {
openaiMessages.push({
role: "assistant",
content: typeof m.content === "string" ? m.content : "",
});
}
}
const response = await client.chat.completions.create({
model,
max_tokens: 4096,
messages: openaiMessages,
});
const text = response.choices[0]?.message?.content || "";
return {
text,
usage: response.usage
? {
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
}
: undefined,
};
} catch (err) {
audit.error({
error: `OpenRouter API error: ${err instanceof Error ? err.message : String(err)}`,
});
throw err;
}
},
async analyzeImage(
imageData: string,
mediaType: ImageContent["mediaType"],
prompt = "What's in this image? Describe it in detail."
): Promise<AgentResponse> {
const messages: Message[] = [
{
role: "user",
content: [
{ type: "image", data: imageData, mediaType },
{ type: "text", text: prompt },
],
},
];
return this.chat(messages);
},
};
}
@ -137,6 +348,9 @@ export function createAgent(config: SecureConfig, audit: AuditLogger): AgentCore
if (config.ai.provider === "anthropic") {
return createAnthropicAgent(config, audit);
}
if (config.ai.provider === "openrouter") {
return createOpenRouterAgent(config, audit);
}
return createOpenAIAgent(config, audit);
}

View File

@ -1,5 +1,5 @@
/**
* Moltbot Secure - Audit Logger
* AssureBot - Audit Logger
*
* Every interaction is logged for transparency and debugging.
* Logs are append-only JSONL format.

View File

@ -1,5 +1,5 @@
/**
* Moltbot Secure - Environment-only Configuration
* AssureBot - Environment-only Configuration
*
* All configuration via environment variables.
* No config files, no filesystem secrets.
@ -14,7 +14,7 @@ export type SecureConfig = {
// AI Provider
ai: {
provider: "anthropic" | "openai";
provider: "anthropic" | "openai" | "openrouter";
apiKey: string;
model?: string;
};
@ -53,6 +53,12 @@ export type SecureConfig = {
host: string;
gatewayToken: string;
};
// Storage (optional)
storage: {
postgresUrl?: string;
redisUrl?: string;
};
};
function required(name: string): string {
@ -89,9 +95,10 @@ function parseAllowedUsers(value: string): number[] {
.filter((n) => Number.isFinite(n) && n > 0);
}
function detectAiProvider(): { provider: "anthropic" | "openai"; apiKey: string } {
function detectAiProvider(): { provider: "anthropic" | "openai" | "openrouter"; apiKey: string } {
const anthropicKey = process.env.ANTHROPIC_API_KEY;
const openaiKey = process.env.OPENAI_API_KEY;
const openrouterKey = process.env.OPENROUTER_API_KEY;
if (anthropicKey) {
return { provider: "anthropic", apiKey: anthropicKey };
@ -99,8 +106,11 @@ function detectAiProvider(): { provider: "anthropic" | "openai"; apiKey: string
if (openaiKey) {
return { provider: "openai", apiKey: openaiKey };
}
if (openrouterKey) {
return { provider: "openrouter", apiKey: openrouterKey };
}
throw new Error("Missing AI provider key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY");
throw new Error("Missing AI provider key. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or OPENROUTER_API_KEY");
}
function generateSecureToken(): string {
@ -132,7 +142,7 @@ export function loadSecureConfig(): SecureConfig {
const webhooksEnabled = optionalBool("WEBHOOKS_ENABLED", true);
const webhookSecret = optional("WEBHOOK_SECRET", generateSecureToken());
// Optional: Sandbox
// Optional: Sandbox (enabled by default - auto-detects Docker or Piston API fallback)
const sandboxEnabled = optionalBool("SANDBOX_ENABLED", true);
// Optional: Scheduler
@ -161,7 +171,7 @@ export function loadSecureConfig(): SecureConfig {
},
sandbox: {
enabled: sandboxEnabled,
image: optional("SANDBOX_IMAGE", "moltbot/sandbox:latest"),
image: optional("SANDBOX_IMAGE", "node:22-slim"),
network: (optional("SANDBOX_NETWORK", "none") as "none" | "bridge"),
memory: optional("SANDBOX_MEMORY", "512m"),
cpus: optional("SANDBOX_CPUS", "1"),
@ -177,7 +187,11 @@ export function loadSecureConfig(): SecureConfig {
server: {
port,
host: optional("HOST", "0.0.0.0"),
gatewayToken: optional("MOLTBOT_GATEWAY_TOKEN", generateSecureToken()),
gatewayToken: optional("ASSUREBOT_GATEWAY_TOKEN", generateSecureToken()),
},
storage: {
postgresUrl: process.env.DATABASE_URL || process.env.POSTGRES_URL,
redisUrl: process.env.REDIS_URL,
},
};
}
@ -231,5 +245,9 @@ export function redactConfig(config: SecureConfig): Record<string, unknown> {
host: config.server.host,
gatewayToken: "[REDACTED]",
},
storage: {
postgresUrl: config.storage.postgresUrl ? "[CONFIGURED]" : undefined,
redisUrl: config.storage.redisUrl ? "[CONFIGURED]" : undefined,
},
};
}

120
secure/documents.ts Normal file
View File

@ -0,0 +1,120 @@
/**
* AssureBot - Document Analysis
*
* Extract text from various document formats for AI analysis.
*/
export type DocumentResult = {
text: string;
pageCount?: number;
format: string;
truncated: boolean;
};
const MAX_TEXT_LENGTH = 50000; // ~12k tokens
/**
* Extract text from a buffer based on mime type
*/
export async function extractText(
buffer: Buffer,
mimeType: string,
filename?: string
): Promise<DocumentResult> {
const ext = filename?.split(".").pop()?.toLowerCase();
// Plain text files
if (
mimeType.startsWith("text/") ||
ext === "txt" ||
ext === "md" ||
ext === "json" ||
ext === "xml" ||
ext === "csv" ||
ext === "log"
) {
return extractPlainText(buffer);
}
// PDF
if (mimeType === "application/pdf" || ext === "pdf") {
return extractPdf(buffer);
}
// Code files (treat as text)
const codeExtensions = [
"js", "ts", "jsx", "tsx", "py", "rb", "go", "rs", "java",
"c", "cpp", "h", "hpp", "cs", "php", "swift", "kt", "scala",
"sh", "bash", "zsh", "yaml", "yml", "toml", "ini", "env",
"sql", "graphql", "html", "css", "scss", "less"
];
if (ext && codeExtensions.includes(ext)) {
return extractPlainText(buffer, ext);
}
// Unsupported format
return {
text: `[Unsupported document format: ${mimeType}${ext ? ` (.${ext})` : ""}]`,
format: "unsupported",
truncated: false,
};
}
/**
* Extract plain text
*/
function extractPlainText(buffer: Buffer, format = "text"): DocumentResult {
let text = buffer.toString("utf-8");
let truncated = false;
if (text.length > MAX_TEXT_LENGTH) {
text = text.slice(0, MAX_TEXT_LENGTH) + "\n\n[... truncated ...]";
truncated = true;
}
return { text, format, truncated };
}
/**
* Extract text from PDF using pdf-parse
*/
async function extractPdf(buffer: Buffer): Promise<DocumentResult> {
try {
// Dynamic import to avoid bundling issues
const pdfParse = await import("pdf-parse").then(m => m.default);
const data = await pdfParse(buffer);
let text = data.text;
let truncated = false;
if (text.length > MAX_TEXT_LENGTH) {
text = text.slice(0, MAX_TEXT_LENGTH) + "\n\n[... truncated ...]";
truncated = true;
}
return {
text,
pageCount: data.numpages,
format: "pdf",
truncated,
};
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
text: `[Failed to parse PDF: ${msg}]`,
format: "pdf-error",
truncated: false,
};
}
}
/**
* Summarize document metadata for logging
*/
export function summarizeDocument(result: DocumentResult): string {
const parts = [result.format.toUpperCase()];
if (result.pageCount) parts.push(`${result.pageCount} pages`);
parts.push(`${result.text.length} chars`);
if (result.truncated) parts.push("truncated");
return parts.join(", ");
}

View File

@ -1,10 +1,10 @@
/**
* Moltbot Secure - Entry Point
* AssureBot - Entry Point
*
* Lean, secure, self-hosted AI assistant for Railway.
*
* Usage:
* TELEGRAM_BOT_TOKEN=xxx ANTHROPIC_API_KEY=xxx ALLOWED_USERS=123 npx ts-node secure/index.ts
* TELEGRAM_BOT_TOKEN=xxx ANTHROPIC_API_KEY=xxx ALLOWED_USERS=123 npx tsx secure/index.ts
*/
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
@ -15,10 +15,12 @@ import { createTelegramBot } from "./telegram.js";
import { createWebhookHandler } from "./webhooks.js";
import { createSandboxRunner } from "./sandbox.js";
import { createScheduler } from "./scheduler.js";
import { createStorage, type Storage } from "./storage.js";
import { createPersonality } from "./personality.js";
async function main() {
console.log("=".repeat(50));
console.log(" MOLTBOT SECURE");
console.log(" ASSUREBOT");
console.log(" Lean, secure, self-hosted AI assistant");
console.log("=".repeat(50));
console.log();
@ -49,6 +51,15 @@ async function main() {
});
audit.startup();
// Create storage (PostgreSQL + Redis)
console.log("[init] Creating storage layer...");
const storage = await createStorage({
postgres: config.storage.postgresUrl ? { url: config.storage.postgresUrl } : undefined,
redis: config.storage.redisUrl ? { url: config.storage.redisUrl } : undefined,
});
const storageHealthy = await storage.isHealthy();
console.log(`[init] Storage healthy: ${storageHealthy}`);
// Create AI agent
console.log(`[init] Creating AI agent (${config.ai.provider})...`);
const agent = createAgent(config, audit);
@ -56,33 +67,46 @@ async function main() {
// Create conversation store
const conversations = createConversationStore();
// Create Telegram bot
console.log("[init] Creating Telegram bot...");
const telegram = createTelegramBot({
config,
audit,
agent,
conversations,
});
// Create webhook handler
console.log("[init] Creating webhook handler...");
const webhooks = createWebhookHandler({
config,
audit,
agent,
telegramBot: telegram.bot,
});
// Create sandbox runner
console.log("[init] Creating sandbox runner...");
const sandbox = createSandboxRunner(config, audit);
const sandboxAvailable = await sandbox.isAvailable();
console.log(`[init] Sandbox available: ${sandboxAvailable}`);
// Create scheduler
// Create a placeholder bot for circular deps
// We'll create telegram, scheduler, and webhooks together
const { Bot } = await import("grammy");
const bot = new Bot(config.telegram.botToken);
// Create scheduler (needs bot for notifications, storage for persistence)
console.log("[init] Creating scheduler...");
const scheduler = createScheduler({
config,
audit,
agent,
telegramBot: bot,
storage,
});
// Create personality engine (learning + personalization)
console.log("[init] Creating personality engine...");
const personality = await createPersonality(storage);
// Create Telegram bot handler (with sandbox, scheduler, personality)
console.log("[init] Creating Telegram bot...");
const telegram = createTelegramBot({
config,
audit,
agent,
conversations,
sandbox,
scheduler,
personality,
});
// Create webhook handler
console.log("[init] Creating webhook handler...");
const webhooks = createWebhookHandler({
config,
audit,
agent,
@ -96,6 +120,7 @@ async function main() {
// Health check
if (url.pathname === "/health" || url.pathname === "/healthz") {
const isStorageHealthy = await storage.isHealthy();
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({
@ -104,6 +129,9 @@ async function main() {
uptime: process.uptime(),
telegram: "connected",
sandbox: sandboxAvailable ? "available" : "unavailable",
storage: isStorageHealthy ? "healthy" : "degraded",
postgres: config.storage.postgresUrl ? "configured" : "none",
redis: config.storage.redisUrl ? "configured" : "none",
}));
return;
}
@ -141,6 +169,7 @@ async function main() {
try {
scheduler.stop();
await telegram.stop();
await storage.close();
await new Promise<void>((resolve, reject) => {
server.close((err) => {
@ -168,19 +197,20 @@ async function main() {
console.log(`[start] HTTP server listening on ${config.server.host}:${config.server.port}`);
});
// Start scheduler
scheduler.start();
// Start scheduler (loads tasks from storage)
await scheduler.start();
// Start Telegram bot (polling mode for simplicity)
await telegram.start();
console.log();
console.log("=".repeat(50));
console.log(" MOLTBOT SECURE IS RUNNING");
console.log(" ASSUREBOT IS RUNNING");
console.log();
console.log(` Telegram: Polling mode`);
console.log(` Webhooks: http://localhost:${config.server.port}${config.webhooks.basePath}/*`);
console.log(` Health: http://localhost:${config.server.port}/health`);
console.log(` Storage: ${config.storage.postgresUrl ? "PostgreSQL" : "memory"}${config.storage.redisUrl ? " + Redis" : ""}`);
console.log(` Allowed: ${config.telegram.allowedUsers.length} users`);
console.log();
console.log(" Press Ctrl+C to stop");

View File

@ -1,7 +1,7 @@
{
"name": "moltbot-secure",
"name": "assurebot",
"version": "1.0.0",
"description": "Lean, secure, self-hosted AI assistant for Railway",
"description": "AssureBot - Lean, secure, self-hosted AI assistant for Railway",
"type": "module",
"main": "dist/index.js",
"scripts": {
@ -13,10 +13,14 @@
"@anthropic-ai/sdk": "^0.39.0",
"cron": "^3.1.7",
"grammy": "^1.21.1",
"openai": "^4.77.0"
"openai": "^4.77.0",
"pdf-parse": "^1.1.1",
"pg": "^8.11.3",
"redis": "^4.6.12"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/pg": "^8.10.9",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
},

10
secure/pdf-parse.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare module "pdf-parse" {
function pdfParse(dataBuffer: Buffer): Promise<{
numpages: number;
numrender: number;
info: Record<string, unknown>;
metadata: Record<string, unknown>;
text: string;
}>;
export default pdfParse;
}

265
secure/personality.ts Normal file
View File

@ -0,0 +1,265 @@
/**
* AssureBot - Personality Engine
*
* Persistent, evolving AI personality that learns from conversations.
* - Stores traits and preferences in Redis (fast access)
* - Syncs to PostgreSQL (durability)
* - Learns user preferences, tone, and topics over time
*/
import type { Storage, UserProfile, PersonalityTraits } from "./storage.js";
// Re-export types for convenience
export type { UserProfile, PersonalityTraits };
export type Personality = {
getSystemPrompt: (userId: number) => Promise<string>;
getUserProfile: (userId: number) => Promise<UserProfile>;
updateUserProfile: (userId: number, updates: Partial<UserProfile>) => Promise<void>;
learnFromConversation: (userId: number, userMessage: string, botResponse: string) => Promise<void>;
getTraits: () => Promise<PersonalityTraits>;
updateTraits: (updates: Partial<PersonalityTraits>) => Promise<void>;
};
const DEFAULT_TRAITS: PersonalityTraits = {
name: "AssureBot",
greeting: "Hey",
signOff: "",
humor: "subtle",
verbosity: "balanced",
commonPhrases: [],
avoidPhrases: [],
expertiseAreas: ["coding", "analysis", "automation"],
lastUpdated: new Date(),
version: 1,
};
const DEFAULT_USER_PROFILE: Omit<UserProfile, "userId"> = {
preferredTone: "friendly",
interests: [],
recentTopics: [],
interactionCount: 0,
lastSeen: new Date(),
notes: [],
};
export async function createPersonality(storage: Storage): Promise<Personality> {
// Load or initialize traits from storage
let traits: PersonalityTraits = await storage.getPersonalityTraits() ?? { ...DEFAULT_TRAITS };
// Save default traits if none exist
if (!(await storage.getPersonalityTraits())) {
await storage.savePersonalityTraits(traits);
console.log("[personality] Initialized default traits");
}
// In-memory cache for hot profiles (reduces Redis calls during conversation)
const profileCache = new Map<number, UserProfile>();
async function loadUserProfile(userId: number): Promise<UserProfile> {
// Check in-memory cache first
if (profileCache.has(userId)) {
return profileCache.get(userId)!;
}
// Try loading from storage (Redis -> PostgreSQL -> memory)
const stored = await storage.getUserProfile(userId);
if (stored) {
profileCache.set(userId, stored);
return stored;
}
// Create new profile for this user
const profile: UserProfile = {
userId,
...DEFAULT_USER_PROFILE,
lastSeen: new Date(),
};
// Persist new profile
await storage.saveUserProfile(profile);
profileCache.set(userId, profile);
console.log(`[personality] Created new profile for user ${userId}`);
return profile;
}
async function saveUserProfile(profile: UserProfile): Promise<void> {
// Update cache
profileCache.set(profile.userId, profile);
// Persist to storage (Redis + PostgreSQL)
await storage.saveUserProfile(profile);
}
return {
async getSystemPrompt(userId: number): Promise<string> {
const profile = await loadUserProfile(userId);
let prompt = `You are ${traits.name}, a helpful AI assistant running as a Telegram bot.
## Personality
- Tone: ${profile.preferredTone}
- Verbosity: ${traits.verbosity}
- Humor: ${traits.humor === "none" ? "Stay professional" : traits.humor === "subtle" ? "Occasional light humor is fine" : "Be playful and fun"}
## Your Expertise
${traits.expertiseAreas.map(e => `- ${e}`).join("\n")}
## About This User
- Interactions: ${profile.interactionCount}
- Interests: ${profile.interests.length > 0 ? profile.interests.join(", ") : "Not yet known"}
- Recent topics: ${profile.recentTopics.length > 0 ? profile.recentTopics.slice(-3).join(", ") : "None yet"}
${profile.notes.length > 0 ? `- Notes: ${profile.notes.slice(-3).join("; ")}` : ""}
## Available Commands (you can tell users about these)
- /js <code> - Run JavaScript code
- /python <code> or /py <code> - Run Python code
- /ts <code> - Run TypeScript code
- /bash <code> or /sh <code> - Run shell commands
- /run <language> <code> - Run code in any supported language (python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php)
- /status - Check bot and sandbox status
- /clear - Clear conversation history
- /schedule "<cron>" "<name>" <prompt> - Schedule recurring AI tasks
- /tasks - List scheduled tasks
- /deltask <id> - Delete a task
When a user asks to run code, you can either:
1. Tell them to use the appropriate command (e.g., "Use /js console.log('hello')")
2. Just answer their question directly if they don't need to execute code
## Guidelines
- Be helpful, accurate, and security-conscious
- Never reveal API keys, tokens, or secrets
- Adapt to the user's communication style
- Remember context from this conversation
- When users want to run code, guide them to use the right command
${traits.commonPhrases.length > 0 ? `- Phrases you like: ${traits.commonPhrases.join(", ")}` : ""}
${traits.avoidPhrases.length > 0 ? `- Avoid saying: ${traits.avoidPhrases.join(", ")}` : ""}`;
return prompt;
},
async getUserProfile(userId: number): Promise<UserProfile> {
return loadUserProfile(userId);
},
async updateUserProfile(userId: number, updates: Partial<UserProfile>): Promise<void> {
const profile = await loadUserProfile(userId);
Object.assign(profile, updates);
await saveUserProfile(profile);
},
async learnFromConversation(
userId: number,
userMessage: string,
botResponse: string
): Promise<void> {
const profile = await loadUserProfile(userId);
// Update interaction count
profile.interactionCount++;
profile.lastSeen = new Date();
// Extract topics (simple keyword extraction)
const topics = extractTopics(userMessage);
if (topics.length > 0) {
// Add to recent topics, keep last 10
profile.recentTopics = [...profile.recentTopics, ...topics].slice(-10);
// Add unique topics to interests
for (const topic of topics) {
if (!profile.interests.includes(topic)) {
profile.interests.push(topic);
// Keep interests manageable
if (profile.interests.length > 20) {
profile.interests = profile.interests.slice(-20);
}
}
}
}
// Detect user preferences from message style
if (userMessage.length < 50 && !userMessage.includes("?")) {
// User prefers concise communication
profile.preferredTone = "concise";
} else if (userMessage.includes("please") || userMessage.includes("thank")) {
profile.preferredTone = "friendly";
}
await saveUserProfile(profile);
},
async getTraits(): Promise<PersonalityTraits> {
return { ...traits };
},
async updateTraits(updates: Partial<PersonalityTraits>): Promise<void> {
traits = {
...traits,
...updates,
lastUpdated: new Date(),
version: traits.version + 1,
};
// Persist to storage
await storage.savePersonalityTraits(traits);
console.log(`[personality] Updated traits (v${traits.version})`);
},
};
}
/**
* Simple topic extraction from text
*/
function extractTopics(text: string): string[] {
const topics: string[] = [];
const lowerText = text.toLowerCase();
// Tech topics
const techKeywords = [
"python", "javascript", "typescript", "rust", "go", "java",
"docker", "kubernetes", "aws", "api", "database", "sql",
"react", "vue", "node", "linux", "git", "ci/cd",
"machine learning", "ai", "llm", "chatgpt", "claude",
];
for (const keyword of techKeywords) {
if (lowerText.includes(keyword)) {
topics.push(keyword);
}
}
// Task types
if (lowerText.includes("debug") || lowerText.includes("fix") || lowerText.includes("error")) {
topics.push("debugging");
}
if (lowerText.includes("write") || lowerText.includes("create") || lowerText.includes("build")) {
topics.push("development");
}
if (lowerText.includes("explain") || lowerText.includes("how does") || lowerText.includes("what is")) {
topics.push("learning");
}
return topics.slice(0, 3); // Max 3 topics per message
}
/**
* Generate a personalized greeting
*/
export function generateGreeting(traits: PersonalityTraits, profile: UserProfile): string {
const greetings = {
casual: ["Hey!", "Hi there!", "What's up?"],
professional: ["Hello.", "Good day.", "Greetings."],
friendly: ["Hey there! 👋", "Hi! Good to see you!", "Hello friend!"],
concise: ["Hi.", "Hey.", ""],
};
const options = greetings[profile.preferredTone];
const greeting = options[Math.floor(Math.random() * options.length)];
if (profile.interactionCount > 10 && profile.name) {
return `${greeting} ${profile.name}!`;
}
return greeting;
}

View File

@ -2,12 +2,13 @@
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "secure/Dockerfile"
"dockerfilePath": "Dockerfile"
},
"deploy": {
"startCommand": "node dist/index.js",
"healthcheckPath": "/health",
"healthcheckTimeout": 30,
"healthcheckTimeout": 60,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 5
"restartPolicyMaxRetries": 3
}
}

10
secure/railway.toml Normal file
View File

@ -0,0 +1,10 @@
[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile"
[deploy]
startCommand = "node dist/index.js"
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 3

View File

@ -1,7 +1,10 @@
/**
* Moltbot Secure - Sandbox Execution
* AssureBot - Sandbox Execution
*
* Isolated code execution with multiple backends:
* 1. Docker (local) - if Docker socket available
* 2. Piston API (cloud) - free code execution API fallback
*
* Isolated Docker container for code/script execution.
* Security-first: no network, read-only root, resource limits.
*/
@ -19,7 +22,34 @@ export type SandboxResult = {
export type SandboxRunner = {
run: (command: string, stdin?: string) => Promise<SandboxResult>;
runCode: (language: string, code: string) => Promise<SandboxResult>;
isAvailable: () => Promise<boolean>;
backend: "docker" | "piston" | "none";
};
// Piston API - free cloud-based code execution
const PISTON_API = "https://emkc.org/api/v2/piston";
// Supported languages for Piston
const PISTON_LANGUAGES: Record<string, { language: string; version: string }> = {
python: { language: "python", version: "3.10" },
python3: { language: "python", version: "3.10" },
py: { language: "python", version: "3.10" },
javascript: { language: "javascript", version: "18.15.0" },
js: { language: "javascript", version: "18.15.0" },
node: { language: "javascript", version: "18.15.0" },
typescript: { language: "typescript", version: "5.0.3" },
ts: { language: "typescript", version: "5.0.3" },
bash: { language: "bash", version: "5.2.0" },
sh: { language: "bash", version: "5.2.0" },
shell: { language: "bash", version: "5.2.0" },
rust: { language: "rust", version: "1.68.2" },
go: { language: "go", version: "1.16.2" },
c: { language: "c", version: "10.2.0" },
cpp: { language: "c++", version: "10.2.0" },
java: { language: "java", version: "15.0.2" },
ruby: { language: "ruby", version: "3.0.1" },
php: { language: "php", version: "8.2.3" },
};
/**
@ -35,6 +65,102 @@ async function checkDocker(): Promise<boolean> {
});
}
/**
* Check if Piston API is available
*/
async function checkPiston(): Promise<boolean> {
try {
const response = await fetch(`${PISTON_API}/runtimes`, {
method: "GET",
signal: AbortSignal.timeout(5000),
});
return response.ok;
} catch {
return false;
}
}
/**
* Execute code via Piston API
*/
async function runPiston(
language: string,
code: string,
timeoutMs: number
): Promise<SandboxResult> {
const startTime = Date.now();
const langConfig = PISTON_LANGUAGES[language.toLowerCase()];
if (!langConfig) {
return {
exitCode: 1,
stdout: "",
stderr: `Unsupported language: ${language}\n\nSupported: ${Object.keys(PISTON_LANGUAGES).join(", ")}`,
timedOut: false,
durationMs: Date.now() - startTime,
};
}
try {
const response = await fetch(`${PISTON_API}/execute`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
language: langConfig.language,
version: langConfig.version,
files: [{ content: code }],
}),
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
const text = await response.text();
return {
exitCode: 1,
stdout: "",
stderr: `Piston API error: ${response.status} ${text}`,
timedOut: false,
durationMs: Date.now() - startTime,
};
}
const result = await response.json() as {
run: { stdout: string; stderr: string; code: number; signal: string | null };
compile?: { stdout: string; stderr: string; code: number };
};
// Check for compilation errors
if (result.compile && result.compile.code !== 0) {
return {
exitCode: result.compile.code,
stdout: result.compile.stdout || "",
stderr: result.compile.stderr || "Compilation failed",
timedOut: false,
durationMs: Date.now() - startTime,
};
}
return {
exitCode: result.run.code,
stdout: (result.run.stdout || "").slice(0, 10000),
stderr: (result.run.stderr || "").slice(0, 10000),
timedOut: result.run.signal === "SIGKILL",
durationMs: Date.now() - startTime,
};
} catch (err) {
const isTimeout = err instanceof Error && err.name === "TimeoutError";
return {
exitCode: 1,
stdout: "",
stderr: isTimeout ? "Execution timed out" : `Error: ${err instanceof Error ? err.message : String(err)}`,
timedOut: isTimeout,
durationMs: Date.now() - startTime,
};
}
}
/**
* Build Docker run arguments for secure execution
*/
@ -83,101 +209,192 @@ function buildDockerArgs(config: SecureConfig["sandbox"], command: string): stri
return args;
}
/**
* Execute command via Docker
*/
async function runDocker(
config: SecureConfig["sandbox"],
command: string,
stdin?: string
): Promise<SandboxResult> {
const startTime = Date.now();
return new Promise((resolve) => {
const args = buildDockerArgs(config, command);
const proc = spawn("docker", args, {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let timedOut = false;
let resolved = false;
const finish = (exitCode: number) => {
if (resolved) return;
resolved = true;
resolve({
exitCode,
stdout: stdout.slice(0, 10000), // Limit output size
stderr: stderr.slice(0, 10000),
timedOut,
durationMs: Date.now() - startTime,
});
};
// Timeout
const timeout = setTimeout(() => {
timedOut = true;
proc.kill("SIGKILL");
}, config.timeoutMs);
proc.stdout?.on("data", (data: Buffer) => {
stdout += data.toString();
// Prevent memory exhaustion
if (stdout.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.stderr?.on("data", (data: Buffer) => {
stderr += data.toString();
if (stderr.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.on("error", (err) => {
clearTimeout(timeout);
stderr += `\nProcess error: ${err.message}`;
finish(1);
});
proc.on("close", (code) => {
clearTimeout(timeout);
finish(code ?? 1);
});
// Write stdin if provided
if (stdin && proc.stdin) {
proc.stdin.write(stdin);
proc.stdin.end();
} else {
proc.stdin?.end();
}
});
}
export function createSandboxRunner(config: SecureConfig, audit: AuditLogger): SandboxRunner {
const sandboxConfig = config.sandbox;
// Detect available backend at creation time
let detectedBackend: "docker" | "piston" | "none" = "none";
let backendChecked = false;
async function detectBackend(): Promise<"docker" | "piston" | "none"> {
if (backendChecked) return detectedBackend;
if (!sandboxConfig.enabled) {
detectedBackend = "none";
backendChecked = true;
return detectedBackend;
}
// Try Docker first
if (await checkDocker()) {
detectedBackend = "docker";
console.log("[sandbox] Using Docker backend");
} else if (await checkPiston()) {
// Fall back to Piston API
detectedBackend = "piston";
console.log("[sandbox] Using Piston API backend (Docker not available)");
} else {
detectedBackend = "none";
console.log("[sandbox] No sandbox backend available");
}
backendChecked = true;
return detectedBackend;
}
// Start detection immediately
void detectBackend();
return {
get backend() {
return detectedBackend;
},
async isAvailable(): Promise<boolean> {
if (!sandboxConfig.enabled) return false;
return checkDocker();
const backend = await detectBackend();
return backend !== "none";
},
async run(command: string, stdin?: string): Promise<SandboxResult> {
const backend = await detectBackend();
const startTime = Date.now();
if (!sandboxConfig.enabled) {
if (backend === "none") {
return {
exitCode: 1,
stdout: "",
stderr: "Sandbox is disabled",
stderr: "Sandbox is disabled or no backend available",
timedOut: false,
durationMs: 0,
};
}
return new Promise((resolve) => {
const args = buildDockerArgs(sandboxConfig, command);
let result: SandboxResult;
const proc = spawn("docker", args, {
stdio: ["pipe", "pipe", "pipe"],
});
if (backend === "docker") {
result = await runDocker(sandboxConfig, command, stdin);
} else {
// Piston: run as bash
result = await runPiston("bash", command, sandboxConfig.timeoutMs);
}
let stdout = "";
let stderr = "";
let timedOut = false;
let resolved = false;
const finish = (exitCode: number) => {
if (resolved) return;
resolved = true;
const durationMs = Date.now() - startTime;
audit.sandbox({
command,
exitCode,
durationMs,
});
resolve({
exitCode,
stdout: stdout.slice(0, 10000), // Limit output size
stderr: stderr.slice(0, 10000),
timedOut,
durationMs,
});
};
// Timeout
const timeout = setTimeout(() => {
timedOut = true;
proc.kill("SIGKILL");
}, sandboxConfig.timeoutMs);
proc.stdout?.on("data", (data: Buffer) => {
stdout += data.toString();
// Prevent memory exhaustion
if (stdout.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.stderr?.on("data", (data: Buffer) => {
stderr += data.toString();
if (stderr.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.on("error", (err) => {
clearTimeout(timeout);
stderr += `\nProcess error: ${err.message}`;
finish(1);
});
proc.on("close", (code) => {
clearTimeout(timeout);
finish(code ?? 1);
});
// Write stdin if provided
if (stdin && proc.stdin) {
proc.stdin.write(stdin);
proc.stdin.end();
} else {
proc.stdin?.end();
}
audit.sandbox({
command,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
return result;
},
async runCode(language: string, code: string): Promise<SandboxResult> {
const backend = await detectBackend();
if (backend === "none") {
return {
exitCode: 1,
stdout: "",
stderr: "Sandbox is disabled or no backend available",
timedOut: false,
durationMs: 0,
};
}
let result: SandboxResult;
if (backend === "piston") {
// Use Piston directly for language support
result = await runPiston(language, code, sandboxConfig.timeoutMs);
} else {
// Docker: build command for the language
const command = buildCommand(language, code);
result = await runDocker(sandboxConfig, command);
}
audit.sandbox({
command: `[${language}] ${code.slice(0, 100)}...`,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
return result;
},
};
}
@ -214,13 +431,12 @@ export function parseSandboxRequest(text: string): {
}
/**
* Build execution command for language
* Build execution command for language (Docker only)
*/
export function buildCommand(language: string, code: string): string {
switch (language.toLowerCase()) {
case "python":
case "py":
// Write code to temp file and execute
return `python3 -c ${JSON.stringify(code)}`;
case "javascript":
@ -234,7 +450,6 @@ export function buildCommand(language: string, code: string): string {
return code;
default:
// Default to shell
return code;
}
}

View File

@ -10,6 +10,7 @@ import type { SecureConfig } from "./config.js";
import type { AuditLogger } from "./audit.js";
import type { AgentCore } from "./agent.js";
import type { Bot } from "grammy";
import type { Storage } from "./storage.js";
import { sendToUser } from "./telegram.js";
export type ScheduledTask = {
@ -29,7 +30,7 @@ export type Scheduler = {
enableTask: (id: string, enabled: boolean) => boolean;
listTasks: () => ScheduledTask[];
runTask: (id: string) => Promise<void>;
start: () => void;
start: () => Promise<void>;
stop: () => void;
};
@ -38,6 +39,7 @@ export type SchedulerDeps = {
audit: AuditLogger;
agent: AgentCore;
telegramBot: Bot;
storage?: Storage;
};
function generateId(): string {
@ -45,9 +47,9 @@ function generateId(): string {
}
export function createScheduler(deps: SchedulerDeps): Scheduler {
const { config, audit, agent, telegramBot } = deps;
const { config, audit, agent, telegramBot, storage } = deps;
const tasks = new Map<string, ScheduledTask>();
const cronJobs = new Map<string, CronJob>();
const cronJobs = new Map<string, CronJob<null, unknown>>();
async function executeTask(task: ScheduledTask): Promise<void> {
const startTime = Date.now();
@ -68,6 +70,11 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
task.lastStatus = "ok";
task.lastError = undefined;
// Save updated task status
if (storage) {
void storage.saveTask(task);
}
audit.cron({
jobId: task.id,
jobName: task.name,
@ -81,6 +88,11 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
task.lastStatus = "error";
task.lastError = errorMsg;
// Save updated task status
if (storage) {
void storage.saveTask(task);
}
audit.cron({
jobId: task.id,
jobName: task.name,
@ -133,6 +145,10 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
const task: ScheduledTask = { ...taskInput, id };
tasks.set(id, task);
scheduleTask(task);
// Persist to storage
if (storage) {
void storage.saveTask(task);
}
return id;
},
@ -147,6 +163,10 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
}
tasks.delete(id);
// Remove from storage
if (storage) {
void storage.deleteTask(id);
}
return true;
},
@ -171,13 +191,23 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
await executeTask(task);
},
start(): void {
async start(): Promise<void> {
if (!config.scheduler.enabled) {
console.log("[scheduler] Scheduler is disabled");
return;
}
console.log("[scheduler] Starting scheduler...");
// Load tasks from storage
if (storage) {
const storedTasks = await storage.getAllTasks();
for (const task of storedTasks) {
tasks.set(task.id, task);
}
console.log(`[scheduler] Loaded ${storedTasks.length} tasks from storage`);
}
for (const task of tasks.values()) {
scheduleTask(task);
}

584
secure/storage.ts Normal file
View File

@ -0,0 +1,584 @@
/**
* AssureBot - Storage Layer
*
* PostgreSQL for persistent data (tasks, profiles, traits)
* Redis for caching and sessions
*/
import type { ScheduledTask } from "./scheduler.js";
export type StorageConfig = {
postgres?: {
url: string;
};
redis?: {
url: string;
};
};
export type Storage = {
// Tasks
saveTask: (task: ScheduledTask) => Promise<void>;
getTask: (id: string) => Promise<ScheduledTask | null>;
getAllTasks: () => Promise<ScheduledTask[]>;
deleteTask: (id: string) => Promise<boolean>;
// Conversations (Redis cache)
getConversation: (userId: number) => Promise<ConversationMessage[]>;
saveConversation: (userId: number, messages: ConversationMessage[]) => Promise<void>;
clearConversation: (userId: number) => Promise<void>;
// Personality (Redis + PostgreSQL)
getUserProfile: (userId: number) => Promise<UserProfile | null>;
saveUserProfile: (profile: UserProfile) => Promise<void>;
getPersonalityTraits: () => Promise<PersonalityTraits | null>;
savePersonalityTraits: (traits: PersonalityTraits) => Promise<void>;
// Health
isHealthy: () => Promise<boolean>;
close: () => Promise<void>;
};
export type ConversationMessage = {
role: "user" | "assistant";
content: string;
timestamp?: string;
};
export type UserProfile = {
userId: number;
name?: string;
timezone?: string;
preferredTone: "casual" | "professional" | "friendly" | "concise";
interests: string[];
recentTopics: string[];
interactionCount: number;
lastSeen: Date;
notes: string[];
};
export type PersonalityTraits = {
name: string;
greeting: string;
signOff: string;
humor: "none" | "subtle" | "playful";
verbosity: "concise" | "balanced" | "detailed";
commonPhrases: string[];
avoidPhrases: string[];
expertiseAreas: string[];
lastUpdated: Date;
version: number;
};
/**
* In-memory storage (fallback when no DB configured)
*/
function createMemoryStorage(): Storage {
const tasks = new Map<string, ScheduledTask>();
const conversations = new Map<number, ConversationMessage[]>();
const userProfiles = new Map<number, UserProfile>();
let personalityTraits: PersonalityTraits | null = null;
return {
async saveTask(task) {
tasks.set(task.id, task);
},
async getTask(id) {
return tasks.get(id) || null;
},
async getAllTasks() {
return Array.from(tasks.values());
},
async deleteTask(id) {
return tasks.delete(id);
},
async getConversation(userId) {
return conversations.get(userId) || [];
},
async saveConversation(userId, messages) {
conversations.set(userId, messages);
},
async clearConversation(userId) {
conversations.delete(userId);
},
async getUserProfile(userId) {
return userProfiles.get(userId) || null;
},
async saveUserProfile(profile) {
userProfiles.set(profile.userId, profile);
},
async getPersonalityTraits() {
return personalityTraits;
},
async savePersonalityTraits(traits) {
personalityTraits = traits;
},
async isHealthy() {
return true;
},
async close() {
// Nothing to close
},
};
}
/**
* PostgreSQL storage for tasks and personality
*/
async function createPostgresStorage(url: string): Promise<{
saveTask: Storage["saveTask"];
getTask: Storage["getTask"];
getAllTasks: Storage["getAllTasks"];
deleteTask: Storage["deleteTask"];
getUserProfile: Storage["getUserProfile"];
saveUserProfile: Storage["saveUserProfile"];
getPersonalityTraits: Storage["getPersonalityTraits"];
savePersonalityTraits: Storage["savePersonalityTraits"];
isHealthy: () => Promise<boolean>;
close: () => Promise<void>;
}> {
const { default: pg } = await import("pg");
const pool = new pg.Pool({ connectionString: url });
// Create tables if not exist
await pool.query(`
CREATE TABLE IF NOT EXISTS scheduled_tasks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
schedule TEXT NOT NULL,
prompt TEXT NOT NULL,
enabled BOOLEAN DEFAULT true,
last_run TIMESTAMPTZ,
last_status TEXT,
last_error TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
)
`);
// User profiles table
await pool.query(`
CREATE TABLE IF NOT EXISTS user_profiles (
user_id BIGINT PRIMARY KEY,
name TEXT,
timezone TEXT,
preferred_tone TEXT DEFAULT 'friendly',
interests JSONB DEFAULT '[]',
recent_topics JSONB DEFAULT '[]',
interaction_count INTEGER DEFAULT 0,
last_seen TIMESTAMPTZ DEFAULT NOW(),
notes JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
)
`);
// Personality traits table (singleton)
await pool.query(`
CREATE TABLE IF NOT EXISTS personality_traits (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
name TEXT DEFAULT 'AssureBot',
greeting TEXT DEFAULT 'Hey',
sign_off TEXT DEFAULT '',
humor TEXT DEFAULT 'subtle',
verbosity TEXT DEFAULT 'balanced',
common_phrases JSONB DEFAULT '[]',
avoid_phrases JSONB DEFAULT '[]',
expertise_areas JSONB DEFAULT '["coding", "analysis", "automation"]',
last_updated TIMESTAMPTZ DEFAULT NOW(),
version INTEGER DEFAULT 1
)
`);
console.log("[storage] PostgreSQL connected, tables ready");
return {
async saveTask(task) {
await pool.query(
`INSERT INTO scheduled_tasks (id, name, schedule, prompt, enabled, last_run, last_status, last_error, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())
ON CONFLICT (id) DO UPDATE SET
name = $2, schedule = $3, prompt = $4, enabled = $5,
last_run = $6, last_status = $7, last_error = $8, updated_at = NOW()`,
[
task.id,
task.name,
task.schedule,
task.prompt,
task.enabled,
task.lastRun || null,
task.lastStatus || null,
task.lastError || null,
]
);
},
async getTask(id) {
const result = await pool.query(
"SELECT * FROM scheduled_tasks WHERE id = $1",
[id]
);
if (result.rows.length === 0) return null;
return rowToTask(result.rows[0]);
},
async getAllTasks() {
const result = await pool.query("SELECT * FROM scheduled_tasks ORDER BY created_at");
return result.rows.map(rowToTask);
},
async deleteTask(id) {
const result = await pool.query(
"DELETE FROM scheduled_tasks WHERE id = $1",
[id]
);
return (result.rowCount ?? 0) > 0;
},
async getUserProfile(userId) {
const result = await pool.query(
"SELECT * FROM user_profiles WHERE user_id = $1",
[userId]
);
if (result.rows.length === 0) return null;
return rowToUserProfile(result.rows[0]);
},
async saveUserProfile(profile) {
await pool.query(
`INSERT INTO user_profiles (user_id, name, timezone, preferred_tone, interests, recent_topics, interaction_count, last_seen, notes, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
ON CONFLICT (user_id) DO UPDATE SET
name = $2, timezone = $3, preferred_tone = $4, interests = $5,
recent_topics = $6, interaction_count = $7, last_seen = $8, notes = $9, updated_at = NOW()`,
[
profile.userId,
profile.name || null,
profile.timezone || null,
profile.preferredTone,
JSON.stringify(profile.interests),
JSON.stringify(profile.recentTopics),
profile.interactionCount,
profile.lastSeen,
JSON.stringify(profile.notes),
]
);
},
async getPersonalityTraits() {
const result = await pool.query("SELECT * FROM personality_traits WHERE id = 1");
if (result.rows.length === 0) return null;
return rowToTraits(result.rows[0]);
},
async savePersonalityTraits(traits) {
await pool.query(
`INSERT INTO personality_traits (id, name, greeting, sign_off, humor, verbosity, common_phrases, avoid_phrases, expertise_areas, last_updated, version)
VALUES (1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (id) DO UPDATE SET
name = $1, greeting = $2, sign_off = $3, humor = $4, verbosity = $5,
common_phrases = $6, avoid_phrases = $7, expertise_areas = $8, last_updated = $9, version = $10`,
[
traits.name,
traits.greeting,
traits.signOff,
traits.humor,
traits.verbosity,
JSON.stringify(traits.commonPhrases),
JSON.stringify(traits.avoidPhrases),
JSON.stringify(traits.expertiseAreas),
traits.lastUpdated,
traits.version,
]
);
},
async isHealthy() {
try {
await pool.query("SELECT 1");
return true;
} catch {
return false;
}
},
async close() {
await pool.end();
},
};
}
function rowToTask(row: Record<string, unknown>): ScheduledTask {
return {
id: row.id as string,
name: row.name as string,
schedule: row.schedule as string,
prompt: row.prompt as string,
enabled: row.enabled as boolean,
lastRun: row.last_run ? new Date(row.last_run as string) : undefined,
lastStatus: row.last_status as "ok" | "error" | undefined,
lastError: row.last_error as string | undefined,
};
}
function rowToUserProfile(row: Record<string, unknown>): UserProfile {
return {
userId: Number(row.user_id),
name: row.name as string | undefined,
timezone: row.timezone as string | undefined,
preferredTone: row.preferred_tone as UserProfile["preferredTone"],
interests: (row.interests as string[]) || [],
recentTopics: (row.recent_topics as string[]) || [],
interactionCount: row.interaction_count as number,
lastSeen: new Date(row.last_seen as string),
notes: (row.notes as string[]) || [],
};
}
function rowToTraits(row: Record<string, unknown>): PersonalityTraits {
return {
name: row.name as string,
greeting: row.greeting as string,
signOff: row.sign_off as string,
humor: row.humor as PersonalityTraits["humor"],
verbosity: row.verbosity as PersonalityTraits["verbosity"],
commonPhrases: (row.common_phrases as string[]) || [],
avoidPhrases: (row.avoid_phrases as string[]) || [],
expertiseAreas: (row.expertise_areas as string[]) || [],
lastUpdated: new Date(row.last_updated as string),
version: row.version as number,
};
}
/**
* Redis storage for conversations/cache and personality caching
*/
async function createRedisStorage(url: string): Promise<{
getConversation: Storage["getConversation"];
saveConversation: Storage["saveConversation"];
clearConversation: Storage["clearConversation"];
getUserProfile: Storage["getUserProfile"];
saveUserProfile: Storage["saveUserProfile"];
getPersonalityTraits: Storage["getPersonalityTraits"];
savePersonalityTraits: Storage["savePersonalityTraits"];
isHealthy: () => Promise<boolean>;
close: () => Promise<void>;
}> {
const { createClient } = await import("redis");
const client = createClient({ url });
client.on("error", (err) => console.error("[redis] Error:", err));
await client.connect();
console.log("[storage] Redis connected");
const CONVERSATION_TTL = 60 * 60 * 24; // 24 hours
const PROFILE_TTL = 60 * 60 * 24 * 7; // 7 days
const TRAITS_TTL = 60 * 60 * 24 * 30; // 30 days
const MAX_MESSAGES = 50;
return {
async getConversation(userId) {
const key = `conv:${userId}`;
const data = await client.get(key);
if (!data) return [];
try {
return JSON.parse(data) as ConversationMessage[];
} catch {
return [];
}
},
async saveConversation(userId, messages) {
const key = `conv:${userId}`;
// Keep only last N messages
const trimmed = messages.slice(-MAX_MESSAGES);
await client.setEx(key, CONVERSATION_TTL, JSON.stringify(trimmed));
},
async clearConversation(userId) {
const key = `conv:${userId}`;
await client.del(key);
},
async getUserProfile(userId) {
const key = `profile:${userId}`;
const data = await client.get(key);
if (!data) return null;
try {
const parsed = JSON.parse(data);
return {
...parsed,
lastSeen: new Date(parsed.lastSeen),
} as UserProfile;
} catch {
return null;
}
},
async saveUserProfile(profile) {
const key = `profile:${profile.userId}`;
await client.setEx(key, PROFILE_TTL, JSON.stringify(profile));
},
async getPersonalityTraits() {
const key = "personality:traits";
const data = await client.get(key);
if (!data) return null;
try {
const parsed = JSON.parse(data);
return {
...parsed,
lastUpdated: new Date(parsed.lastUpdated),
} as PersonalityTraits;
} catch {
return null;
}
},
async savePersonalityTraits(traits) {
const key = "personality:traits";
await client.setEx(key, TRAITS_TTL, JSON.stringify(traits));
},
async isHealthy() {
try {
await client.ping();
return true;
} catch {
return false;
}
},
async close() {
await client.quit();
},
};
}
/**
* Create storage based on config
* Strategy:
* - Redis: fast cache for conversations, profiles, traits
* - PostgreSQL: durable backing store for profiles, traits, tasks
* - Memory: fallback when neither is available
*/
export async function createStorage(config: StorageConfig): Promise<Storage> {
const memory = createMemoryStorage();
let pgStorage: Awaited<ReturnType<typeof createPostgresStorage>> | null = null;
let redisStorage: Awaited<ReturnType<typeof createRedisStorage>> | null = null;
// Try PostgreSQL
if (config.postgres?.url) {
try {
pgStorage = await createPostgresStorage(config.postgres.url);
} catch (err) {
console.error("[storage] PostgreSQL connection failed, using memory:", err);
}
}
// Try Redis
if (config.redis?.url) {
try {
redisStorage = await createRedisStorage(config.redis.url);
} catch (err) {
console.error("[storage] Redis connection failed, using memory:", err);
}
}
// Create layered personality storage (Redis cache -> PostgreSQL backing -> memory fallback)
async function getUserProfile(userId: number): Promise<UserProfile | null> {
// Try Redis cache first
if (redisStorage) {
const cached = await redisStorage.getUserProfile(userId);
if (cached) return cached;
}
// Try PostgreSQL
if (pgStorage) {
const profile = await pgStorage.getUserProfile(userId);
// Cache in Redis if found
if (profile && redisStorage) {
await redisStorage.saveUserProfile(profile);
}
return profile;
}
// Fallback to memory
return memory.getUserProfile(userId);
}
async function saveUserProfile(profile: UserProfile): Promise<void> {
// Save to PostgreSQL (durable)
if (pgStorage) {
await pgStorage.saveUserProfile(profile);
}
// Cache in Redis
if (redisStorage) {
await redisStorage.saveUserProfile(profile);
}
// Also update memory for consistency
await memory.saveUserProfile(profile);
}
async function getPersonalityTraits(): Promise<PersonalityTraits | null> {
// Try Redis cache first
if (redisStorage) {
const cached = await redisStorage.getPersonalityTraits();
if (cached) return cached;
}
// Try PostgreSQL
if (pgStorage) {
const traits = await pgStorage.getPersonalityTraits();
// Cache in Redis if found
if (traits && redisStorage) {
await redisStorage.savePersonalityTraits(traits);
}
return traits;
}
// Fallback to memory
return memory.getPersonalityTraits();
}
async function savePersonalityTraits(traits: PersonalityTraits): Promise<void> {
// Save to PostgreSQL (durable)
if (pgStorage) {
await pgStorage.savePersonalityTraits(traits);
}
// Cache in Redis
if (redisStorage) {
await redisStorage.savePersonalityTraits(traits);
}
// Also update memory for consistency
await memory.savePersonalityTraits(traits);
}
return {
// Tasks: prefer PostgreSQL, fallback to memory
saveTask: pgStorage?.saveTask ?? memory.saveTask,
getTask: pgStorage?.getTask ?? memory.getTask,
getAllTasks: pgStorage?.getAllTasks ?? memory.getAllTasks,
deleteTask: pgStorage?.deleteTask ?? memory.deleteTask,
// Conversations: prefer Redis, fallback to memory
getConversation: redisStorage?.getConversation ?? memory.getConversation,
saveConversation: redisStorage?.saveConversation ?? memory.saveConversation,
clearConversation: redisStorage?.clearConversation ?? memory.clearConversation,
// Personality: layered (Redis cache -> PostgreSQL -> memory)
getUserProfile,
saveUserProfile,
getPersonalityTraits,
savePersonalityTraits,
async isHealthy() {
const pgOk = pgStorage ? await pgStorage.isHealthy() : true;
const redisOk = redisStorage ? await redisStorage.isHealthy() : true;
return pgOk && redisOk;
},
async close() {
await pgStorage?.close();
await redisStorage?.close();
},
};
}

View File

@ -1,20 +1,23 @@
/**
* Moltbot Secure - Telegram Channel
* AssureBot - Telegram Channel
*
* Minimal, secure Telegram bot handler.
* Minimal, secure Telegram bot handler with image analysis.
* Allowlist-only: only approved users can interact.
*/
import { Bot, Context, webhookCallback } from "grammy";
import { Bot, Context } from "grammy";
import type { SecureConfig } from "./config.js";
import type { AuditLogger } from "./audit.js";
import type { AgentCore, ConversationStore, Message } from "./agent.js";
import type { AgentCore, ConversationStore, ImageContent } from "./agent.js";
import type { SandboxRunner } from "./sandbox.js";
import type { Scheduler } from "./scheduler.js";
import type { Personality } from "./personality.js";
import { extractText, summarizeDocument } from "./documents.js";
export type TelegramBot = {
bot: Bot;
start: () => Promise<void>;
stop: () => Promise<void>;
webhookHandler: (path?: string) => ReturnType<typeof webhookCallback>;
};
export type TelegramDeps = {
@ -22,6 +25,9 @@ export type TelegramDeps = {
audit: AuditLogger;
agent: AgentCore;
conversations: ConversationStore;
sandbox?: SandboxRunner;
scheduler?: Scheduler;
personality?: Personality;
onWebhookMessage?: (userId: number, text: string) => void;
};
@ -38,7 +44,7 @@ function formatUsername(ctx: Context): string {
}
export function createTelegramBot(deps: TelegramDeps): TelegramBot {
const { config, audit, agent, conversations } = deps;
const { config, audit, agent, conversations, sandbox, scheduler, personality } = deps;
const bot = new Bot(config.telegram.botToken);
// Error handler
@ -63,17 +69,29 @@ export function createTelegramBot(deps: TelegramDeps): TelegramBot {
}
await ctx.reply(
`Welcome to Moltbot Secure.
`Welcome to AssureBot.
You are authorized to use this bot.
Commands:
/start - Show this message
/clear - Clear conversation history
/status - Check bot status
/help - Show help
Code Execution:
/js <code> - Run JavaScript
/python <code> - Run Python
/ts <code> - Run TypeScript
/bash <code> - Run shell commands
/run <lang> <code> - Run any language
Just send me a message to chat!`
Other Commands:
/status - Check bot status
/clear - Clear conversation history
/schedule - Schedule AI tasks
/tasks - List scheduled tasks
/help - Show full help
Features:
- Chat with AI
- Image analysis (send photos)
- Document analysis (send PDFs)
- Code execution (15+ languages)`
);
});
@ -96,11 +114,15 @@ Just send me a message to chat!`
}
const history = conversations.get(userId);
const sandboxStatus = sandbox
? `${sandbox.backend} (${await sandbox.isAvailable() ? "ready" : "unavailable"})`
: "not configured";
await ctx.reply(
`Status:
- AI Provider: ${agent.provider}
- Conversation: ${history.length} messages
- Sandbox: ${config.sandbox.enabled ? "enabled" : "disabled"}
- Sandbox: ${sandboxStatus}
- Webhooks: ${config.webhooks.enabled ? "enabled" : "disabled"}
- Scheduler: ${config.scheduler.enabled ? "enabled" : "disabled"}`
);
@ -114,28 +136,331 @@ Just send me a message to chat!`
}
await ctx.reply(
`Moltbot Secure Help
`AssureBot Help
This is a secure, self-hosted AI assistant.
A secure, self-hosted AI assistant.
Features:
- Chat with AI (text messages)
- Forward content for analysis
- Receive webhook notifications
CODE EXECUTION:
/js <code> - Run JavaScript
/python <code> or /py <code> - Run Python
/ts <code> - Run TypeScript
/bash <code> or /sh <code> - Run shell
/run <lang> <code> - Run any language
Commands:
/start - Welcome message
/clear - Clear conversation history
/status - Bot status
Supported: python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php
SCHEDULING:
/schedule "<cron>" "<name>" <prompt>
/tasks - List tasks
/deltask <id> - Delete task
Example: /schedule "0 9 * * *" "Morning" Good morning!
OTHER:
/status - Bot & sandbox status
/clear - Clear conversation
/help - This message
Security:
- Only authorized users can interact
- All interactions are logged
- No data is sent to third parties (except AI provider)`
FEATURES:
- Chat naturally with AI
- Send images for analysis
- Send PDFs/docs for analysis
- Code runs in isolated sandbox`
);
});
// Command: /sandbox <code>
bot.command("sandbox", async (ctx) => {
const userId = ctx.from?.id;
const username = formatUsername(ctx);
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
return;
}
if (!sandbox) {
await ctx.reply("Sandbox is not configured.");
return;
}
if (!config.sandbox.enabled) {
await ctx.reply("Sandbox is disabled.");
return;
}
const code = ctx.message?.text?.replace(/^\/sandbox\s*/, "").trim() ?? "";
if (!code) {
await ctx.reply("Usage: /sandbox <code>\n\nExample: /sandbox echo Hello World");
return;
}
const startTime = Date.now();
await ctx.replyWithChatAction("typing");
try {
const result = await sandbox.run(code);
const output = result.stdout || result.stderr || "(no output)";
const status = result.exitCode === 0 ? "✓" : `✗ (exit ${result.exitCode})`;
const timeout = result.timedOut ? " [TIMED OUT]" : "";
await ctx.reply(
`**Sandbox Result** ${status}${timeout}\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\`\nDuration: ${result.durationMs}ms`,
{ parse_mode: "Markdown" }
).catch(async () => {
await ctx.reply(`Sandbox Result ${status}${timeout}\n\n${output.slice(0, 3500)}\n\nDuration: ${result.durationMs}ms`);
});
audit.sandbox({
command: code,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
audit.error({ error: `Sandbox error: ${errorMsg}`, metadata: { userId, code } });
await ctx.reply(`Sandbox error: ${errorMsg}`);
}
});
// Helper for language-specific code execution
async function runCodeCommand(
ctx: Context,
language: string,
commandName: string
): Promise<void> {
const userId = ctx.from?.id;
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
return;
}
if (!sandbox) {
await ctx.reply("Sandbox is not configured.");
return;
}
const isAvailable = await sandbox.isAvailable();
if (!isAvailable) {
await ctx.reply(`Sandbox unavailable. Backend: ${sandbox.backend}`);
return;
}
const code = ctx.message?.text?.replace(new RegExp(`^/${commandName}\\s*`), "").trim() ?? "";
if (!code) {
await ctx.reply(`Usage: /${commandName} <code>\n\nExample: /${commandName} console.log("Hello!")`);
return;
}
await ctx.replyWithChatAction("typing");
try {
const result = await sandbox.runCode(language, code);
const output = result.stdout || result.stderr || "(no output)";
const status = result.exitCode === 0 ? "✓" : `✗ (exit ${result.exitCode})`;
const timeout = result.timedOut ? " [TIMED OUT]" : "";
const backend = sandbox.backend === "piston" ? " [Piston]" : "";
await ctx.reply(
`**${language}** ${status}${timeout}${backend}\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\`\nDuration: ${result.durationMs}ms`,
{ parse_mode: "Markdown" }
).catch(async () => {
await ctx.reply(`${language} ${status}${timeout}${backend}\n\n${output.slice(0, 3500)}\n\nDuration: ${result.durationMs}ms`);
});
audit.sandbox({
command: `[${language}] ${code.slice(0, 100)}`,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
audit.error({ error: `Code execution error: ${errorMsg}`, metadata: { userId, language, code } });
await ctx.reply(`Error: ${errorMsg}`);
}
}
// Command: /js <code> - Run JavaScript
bot.command("js", (ctx) => runCodeCommand(ctx, "javascript", "js"));
// Command: /python <code> - Run Python
bot.command("python", (ctx) => runCodeCommand(ctx, "python", "python"));
bot.command("py", (ctx) => runCodeCommand(ctx, "python", "py"));
// Command: /ts <code> - Run TypeScript
bot.command("ts", (ctx) => runCodeCommand(ctx, "typescript", "ts"));
// Command: /bash <code> - Run Bash
bot.command("bash", (ctx) => runCodeCommand(ctx, "bash", "bash"));
bot.command("sh", (ctx) => runCodeCommand(ctx, "bash", "sh"));
// Command: /run <language> <code> - Run code in any supported language
bot.command("run", async (ctx) => {
const userId = ctx.from?.id;
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
return;
}
if (!sandbox) {
await ctx.reply("Sandbox is not configured.");
return;
}
const isAvailable = await sandbox.isAvailable();
if (!isAvailable) {
await ctx.reply(`Sandbox unavailable. Backend: ${sandbox.backend}`);
return;
}
const text = ctx.message?.text?.replace(/^\/run\s*/, "").trim() ?? "";
const match = text.match(/^(\w+)\s+([\s\S]+)$/);
if (!match) {
await ctx.reply(
`Usage: /run <language> <code>
Supported languages:
- javascript, js
- typescript, ts
- python, py
- bash, sh
- rust, go, c, cpp, java, ruby, php
Example: /run python print("Hello!")`
);
return;
}
const [, language, code] = match;
await ctx.replyWithChatAction("typing");
try {
const result = await sandbox.runCode(language, code);
const output = result.stdout || result.stderr || "(no output)";
const status = result.exitCode === 0 ? "✓" : `✗ (exit ${result.exitCode})`;
const timeout = result.timedOut ? " [TIMED OUT]" : "";
const backend = sandbox.backend === "piston" ? " [Piston]" : "";
await ctx.reply(
`**${language}** ${status}${timeout}${backend}\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\`\nDuration: ${result.durationMs}ms`,
{ parse_mode: "Markdown" }
).catch(async () => {
await ctx.reply(`${language} ${status}${timeout}${backend}\n\n${output.slice(0, 3500)}\n\nDuration: ${result.durationMs}ms`);
});
audit.sandbox({
command: `[${language}] ${code.slice(0, 100)}`,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
audit.error({ error: `Code execution error: ${errorMsg}`, metadata: { userId, language, code } });
await ctx.reply(`Error: ${errorMsg}`);
}
});
// Command: /schedule <cron> <name> <prompt>
bot.command("schedule", async (ctx) => {
const userId = ctx.from?.id;
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
return;
}
if (!scheduler) {
await ctx.reply("Scheduler is not configured.");
return;
}
if (!config.scheduler.enabled) {
await ctx.reply("Scheduler is disabled.");
return;
}
// Parse: /schedule "*/5 * * * *" "Task Name" What to do
const text = ctx.message?.text?.replace(/^\/schedule\s*/, "").trim() ?? "";
const match = text.match(/^"([^"]+)"\s+"([^"]+)"\s+(.+)$/s);
if (!match) {
await ctx.reply(
`Usage: /schedule "<cron>" "<name>" <prompt>
Example:
/schedule "0 9 * * *" "Morning Brief" Give me a summary of what I should focus on today
Cron format: minute hour day month weekday
- "0 9 * * *" = 9:00 AM daily
- "*/30 * * * *" = Every 30 minutes
- "0 0 * * 1" = Midnight on Mondays`
);
return;
}
const [, cronExpr, name, prompt] = match;
try {
const taskId = scheduler.addTask({
name,
schedule: cronExpr,
prompt,
enabled: true,
});
await ctx.reply(`Task scheduled!\n\nID: ${taskId}\nName: ${name}\nSchedule: ${cronExpr}`);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
await ctx.reply(`Failed to schedule task: ${errorMsg}`);
}
});
// Command: /tasks
bot.command("tasks", async (ctx) => {
const userId = ctx.from?.id;
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
return;
}
if (!scheduler) {
await ctx.reply("Scheduler is not configured.");
return;
}
const tasks = scheduler.listTasks();
if (tasks.length === 0) {
await ctx.reply("No scheduled tasks.\n\nUse /schedule to create one.");
return;
}
const lines = tasks.map((t) => {
const status = t.enabled ? "✓" : "✗";
const lastRun = t.lastRun ? t.lastRun.toISOString().slice(0, 16) : "never";
return `${status} **${t.name}** (${t.id})\n ${t.schedule}\n Last: ${lastRun}`;
});
await ctx.reply(`**Scheduled Tasks**\n\n${lines.join("\n\n")}`, { parse_mode: "Markdown" }).catch(async () => {
await ctx.reply(`Scheduled Tasks\n\n${lines.join("\n\n").replace(/\*\*/g, "")}`);
});
});
// Command: /deltask <id>
bot.command("deltask", async (ctx) => {
const userId = ctx.from?.id;
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
return;
}
if (!scheduler) {
await ctx.reply("Scheduler is not configured.");
return;
}
const taskId = ctx.message?.text?.replace(/^\/deltask\s*/, "").trim() ?? "";
if (!taskId) {
await ctx.reply("Usage: /deltask <task_id>");
return;
}
if (scheduler.removeTask(taskId)) {
await ctx.reply(`Task ${taskId} deleted.`);
} else {
await ctx.reply(`Task ${taskId} not found.`);
}
});
// Handle all text messages
bot.on("message:text", async (ctx) => {
const userId = ctx.from?.id;
@ -170,12 +495,22 @@ Security:
// Get conversation history
const history = conversations.get(userId);
// Call AI
const response = await agent.chat(history);
// Get personalized system prompt if personality is configured
const systemPrompt = personality
? await personality.getSystemPrompt(userId)
: undefined;
// Call AI with optional personalized system prompt
const response = await agent.chat(history, systemPrompt);
// Add assistant response to history
conversations.add(userId, { role: "assistant", content: response.text });
// Learn from this conversation
if (personality) {
await personality.learnFromConversation(userId, text, response.text);
}
// Send response
await ctx.reply(response.text, { parse_mode: "Markdown" }).catch(async () => {
// Fallback without markdown if it fails
@ -256,25 +591,168 @@ Security:
// Handle photos
bot.on("message:photo", async (ctx) => {
const userId = ctx.from?.id;
const username = formatUsername(ctx);
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
audit.messageBlocked({
userId: userId || 0,
username,
reason: "User not in allowlist",
});
return;
}
await ctx.reply(
"I received your image. Image analysis is available with Claude - please describe what you'd like me to analyze."
);
const startTime = Date.now();
const caption = ctx.message.caption || "What's in this image? Describe it in detail.";
try {
await ctx.replyWithChatAction("typing");
// Get the largest photo (last in array)
const photos = ctx.message.photo;
const photo = photos[photos.length - 1];
// Get file info
const file = await ctx.api.getFile(photo.file_id);
if (!file.file_path) {
await ctx.reply("Sorry, I couldn't download the image.");
return;
}
// Download the file
const fileUrl = `https://api.telegram.org/file/bot${config.telegram.botToken}/${file.file_path}`;
const response = await fetch(fileUrl);
if (!response.ok) {
await ctx.reply("Sorry, I couldn't download the image.");
return;
}
const buffer = await response.arrayBuffer();
const base64 = Buffer.from(buffer).toString("base64");
// Determine media type from file path
const ext = file.file_path.split(".").pop()?.toLowerCase();
let mediaType: ImageContent["mediaType"] = "image/jpeg";
if (ext === "png") mediaType = "image/png";
else if (ext === "gif") mediaType = "image/gif";
else if (ext === "webp") mediaType = "image/webp";
// Analyze with AI
const result = await agent.analyzeImage(base64, mediaType, caption);
await ctx.reply(result.text, { parse_mode: "Markdown" }).catch(async () => {
await ctx.reply(result.text);
});
audit.message({
userId,
username,
text: `[IMAGE] ${caption}`,
response: result.text,
durationMs: Date.now() - startTime,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
audit.error({
error: `Failed to analyze image: ${errorMsg}`,
metadata: { userId, username },
});
await ctx.reply("Sorry, I couldn't analyze that image. Please try again.");
}
});
// Handle documents
bot.on("message:document", async (ctx) => {
const userId = ctx.from?.id;
const username = formatUsername(ctx);
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
audit.messageBlocked({
userId: userId || 0,
username,
reason: "User not in allowlist",
});
return;
}
await ctx.reply(
"I received your document. Document analysis coming soon - for now, please copy/paste the text content."
);
const doc = ctx.message?.document;
if (!doc) {
await ctx.reply("Could not process document.");
return;
}
const startTime = Date.now();
const caption = ctx.message?.caption || "Please analyze this document and summarize the key points.";
try {
await ctx.replyWithChatAction("typing");
// Check file size (max 20MB)
if (doc.file_size && doc.file_size > 20 * 1024 * 1024) {
await ctx.reply("Document too large (max 20MB).");
return;
}
// Get file info
const file = await ctx.api.getFile(doc.file_id);
if (!file.file_path) {
await ctx.reply("Could not download document.");
return;
}
// Download the file
const fileUrl = `https://api.telegram.org/file/bot${config.telegram.botToken}/${file.file_path}`;
const response = await fetch(fileUrl);
if (!response.ok) {
await ctx.reply("Failed to download document.");
return;
}
const buffer = Buffer.from(await response.arrayBuffer());
const mimeType = doc.mime_type || "application/octet-stream";
// Extract text
const extracted = await extractText(buffer, mimeType, doc.file_name);
if (extracted.format === "unsupported") {
await ctx.reply(
`Unsupported document format: ${mimeType}\n\nSupported: PDF, TXT, MD, JSON, CSV, code files`
);
return;
}
if (extracted.format === "pdf-error") {
await ctx.reply(`Could not parse PDF: ${extracted.text}`);
return;
}
// Analyze with AI
const result = await agent.chat([
{
role: "user",
content: `${caption}\n\n--- Document Content (${summarizeDocument(extracted)}) ---\n\n${extracted.text}`,
},
]);
await ctx.reply(result.text, { parse_mode: "Markdown" }).catch(async () => {
await ctx.reply(result.text);
});
audit.message({
userId,
username,
text: `[DOCUMENT: ${doc.file_name || "unnamed"}] ${caption}`,
response: result.text,
durationMs: Date.now() - startTime,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
audit.error({
error: `Failed to analyze document: ${errorMsg}`,
metadata: { userId, username, filename: doc.file_name },
});
await ctx.reply("Sorry, I couldn't analyze that document. Please try again.");
}
});
return {
@ -293,10 +771,6 @@ Security:
console.log("[telegram] Stopping bot...");
await bot.stop();
},
webhookHandler(path = "/telegram"): ReturnType<typeof webhookCallback> {
return webhookCallback(bot, "http", { path });
},
};
}

View File

@ -4,6 +4,7 @@
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"types": ["node"],
"outDir": "./dist",
"rootDir": ".",
"strict": true,

View File

@ -1,5 +1,5 @@
/**
* Moltbot Secure - Webhook Receiver
* AssureBot - Webhook Receiver
*
* Authenticated webhook endpoint for external integrations.
* Receives events from GitHub, Stripe, uptime monitors, etc.
@ -48,8 +48,8 @@ function extractToken(req: IncomingMessage, url: URL): { token: string; fromQuer
return { token: authHeader.slice(7), fromQuery: false };
}
// Check X-Moltbot-Token header
const tokenHeader = req.headers["x-moltbot-token"];
// Check X-AssureBot-Token header
const tokenHeader = req.headers["x-assurebot-token"];
if (typeof tokenHeader === "string") {
return { token: tokenHeader, fromQuery: false };
}