diff --git a/README.md b/README.md index 1fd5e074c..36f921e36 100644 --- a/README.md +++ b/README.md @@ -1,518 +1,114 @@ -# 🦞 OpenClaw β€” Personal AI Assistant +# AssureBot -

- - - OpenClaw - -

+**Lean, secure, self-hosted AI assistant for Railway.** -

- EXFOLIATE! EXFOLIATE! -

+Your AI agent that runs on your infrastructure, answers only to you, and you can actually audit. -

- CI status - GitHub release - Discord - MIT License -

+[![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 long‑context strength and better prompt‑injection 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-`), 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 ` (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)** β€” openclaw‑managed Chrome/Chromium with CDP control. -- **[Canvas + A2UI](https://docs.openclaw.ai/platforms/mac/canvas)** β€” agent‑driven 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)** β€” always‑on speech and continuous conversation. -- **[Nodes](https://docs.openclaw.ai/nodes)** β€” Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOS‑only `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) - -It’s 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 device‑local actions when needed. - -- **Gateway host** runs the exec tool and channel connections by default. -- **Device nodes** run device‑local 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 you’ll 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 per‑session elevated access when enabled + allowlisted. -- Gateway persists the per‑session 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 reply‑back ping‑pong + 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 ` β€” 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.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 ` β†’ Run code in isolated container +- `/schedule ` β†’ Create scheduled task +- `/tasks` β†’ List scheduled tasks -- **Default:** tools run on the host for the **main** session, so the agent has full access when it’s just you. -- **Group/channel safety:** set `agents.defaults.sandbox.mode: "non-main"` to run **non‑main sessions** (groups/channels) inside per‑session 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 you’re past the onboarding flow and want the deeper reference. -- [Start with the docs index for navigation and β€œwhat’s 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: - -

- steipete plum-dawg bohdanpodvirnyi iHildy jaydenfyi joaohlisboa mneves75 MatthieuBizien MaudeBot Glucksberg - rahthakor vrknetha radek-paclt vignesh07 Tobias Bischoff joshp123 czekaj mukhtharcm sebslight maxsumrall - xadenryan rodrigouroz juanpablodlc hsrvc magimetal zerone0x tyler6204 meaningfool patelhiren NicholasSpisak - jonisjongithub abhisekbasu1 jamesgroat claude JustYannicc Mariano Belinky Hyaxia dantelex SocialNerd42069 daveonkels - google-labs-jules[bot] lc0rp mousberg adam91holt hougangdev shakkernerd gumadeiras mteam88 hirefrank joeynyc - orlyjamie dbhurley Eng. Juan Combetto TSavo julianengel bradleypriest benithors rohannagpal timolins f-trycua - benostein elliotsecops nachx639 pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino Vasanth Rao Naik Sabavat - petter-b thewilloftheshadow cpojer scald andranik-sahakyan davidguttman sleontenko denysvitali sircrumpet peschee - nonggialiang rafaelreis-r dominicnunez lploc94 ratulsarna lutr0 danielz1z AdeboyeDN Alg0rix papago2355 - emanuelst KristijanJovanovski rdev rhuanssauro joshrad-dev kiranjd osolmaz adityashaw2 CashWilliams sheeek - ryancontent artuskg Takhoffman onutc pauloportella neooriginal manuelhettich minghinmatthewlam myfunc travisirby - obviyus buddyh connorshea kyleok mcinteerj dependabot[bot] John-Rood timkrase uos-status gerardward2007 - roshanasingh4 tosh-hamburg azade-c dlauer JonUleis shivamraut101 bjesuiter cheeeee robbyczgw-cla Josh Phillips - YuriNachos pookNast Whoaa512 chriseidhof ngutman ysqander aj47 kennyklee superman32432432 Yurii Chukhlib - grp06 antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy imfing jalehman jarvis-medmatic - kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse dougvk erikpr1994 fal3 Ghost jonasjancarik - Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist sibbl chrisrodz Friederike Seiler gabriel-trigo - iamadig Jonathan D. Rhyne (DJ-D) Joshua Mitchell Kit koala73 manmal ogulcancelik pasogott petradonka rubyrunsstuff - siddhantjain suminhthanh svkozak VACInc wes-davis zats 24601 ameno- Chris Taylor dguido - Django Navarro evalexpr henrino3 humanwritten larlyssa Lukavyi odysseus0 oswalpalash pcty-nextgen-service-account pi0 - rmorse Roopak Nijhara Syhids Aaron Konyer aaronveklabs andreabadesso Andrii cash-echo-bot Clawd ClawdFx - EnzeD erik-agens Evizero fcatuhe itsjaydesu ivancasco ivanrvpereira Jarvis jayhickey jeffersonwarrior - jeffersonwarrior jverdi longmaba MarvinCui mickahouan mjrussell odnxe p6l-richard philipp-spiess Pocket Clawd - robaxelsen Sash Catanzarite Suksham-sharma T5-AndyML tewatia travisp VAC william arzt zknicker 0oAstro - abhaymundhara aduk059 alejandro maza Alex-Alaniz alexstyl andrewting19 anpoirier araa47 arthyn Asleep123 - bguidolim bolismauro chenyuan99 OpenClaw Maintainers conhecendoia dasilva333 David-Marsh-Photo Developer Dimitrios Ploutarchos Drake Thomsen - dylanneve1 Felix Krause foeken frankekn ganghyun kim grrowl gtsifrikas HazAT hrdwdmrbl hugobarauna - Jamie Openshaw Jane Jarvis Deploy Jefferson Nunn jogi47 kentaro Kevin Lin kira-ariaki kitze Kiwitwitter - levifig Lloyd longjos loukotal louzhixian martinpucik Matt mini mertcicekci0 Miles mrdbstn - MSch Mustafa Tag Eldeen ndraiman nexty5870 Noctivoro ppamment prathamdby ptn1411 reeltimeapps RLTCmpe - Rolf Fredheim Rony Kelner Samrat Jha senoldogann sergical shiv19 shiyuanhai siraht snopoke techboss - testingabc321 The Admiral thesash Ubuntu voidserf Vultr-Clawd Admin Wimmie wolfred wstock YangHuang2280 - yazinsai YiWang24 ymat19 Zach Knickerbocker zackerthescar 0xJonHoldsCrypto aaronn Alphonse-arianee atalovesyou Azade - carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres - rhjoh ronak-guliani William Stock -

+MIT diff --git a/SECURE-BOT.md b/SECURE-BOT.md index 0e270583f..9ee0b7574 100644 --- a/SECURE-BOT.md +++ b/SECURE-BOT.md @@ -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 β”‚ β”‚ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95b940c97..df5dfdd73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index acf898add..3b66f3dcb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - . - ui + - secure - packages/* - extensions/* diff --git a/railway-template.json b/railway-template.json new file mode 100644 index 000000000..b0a0a4868 --- /dev/null +++ b/railway-template.json @@ -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" + } + ] +} diff --git a/secure/Dockerfile b/secure/Dockerfile index 29d9097e0..f8971bab8 100644 --- a/secure/Dockerfile +++ b/secure/Dockerfile @@ -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"] diff --git a/secure/README.md b/secure/README.md index f8005e120..8cb2726da 100644 --- a/secure/README.md +++ b/secure/README.md @@ -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 - 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 ` | Run JavaScript | +| `/python ` | Run Python | +| `/ts ` | Run TypeScript | +| `/bash ` | Run shell commands | +| `/run ` | 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) ``` diff --git a/secure/agent.ts b/secure/agent.ts index 8a98c029a..15a00f88a 100644 --- a/secure/agent.ts +++ b/secure/agent.ts @@ -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; - provider: "anthropic" | "openai"; + analyzeImage: (imageData: string, mediaType: ImageContent["mediaType"], prompt?: string) => Promise; + 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 - Run JavaScript +- /python - Run Python +- /ts - Run TypeScript +- /bash - Run shell commands +- /run - 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 { 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 { + 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 { 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 { + 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 { + 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 { + 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); } diff --git a/secure/audit.ts b/secure/audit.ts index 6351f1673..e869ae6cd 100644 --- a/secure/audit.ts +++ b/secure/audit.ts @@ -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. diff --git a/secure/config.ts b/secure/config.ts index 9411cabdd..530de8a95 100644 --- a/secure/config.ts +++ b/secure/config.ts @@ -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 { host: config.server.host, gatewayToken: "[REDACTED]", }, + storage: { + postgresUrl: config.storage.postgresUrl ? "[CONFIGURED]" : undefined, + redisUrl: config.storage.redisUrl ? "[CONFIGURED]" : undefined, + }, }; } diff --git a/secure/documents.ts b/secure/documents.ts new file mode 100644 index 000000000..4d690816a --- /dev/null +++ b/secure/documents.ts @@ -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 { + 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 { + 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(", "); +} diff --git a/secure/index.ts b/secure/index.ts index f0f9104c3..202a2b9bc 100644 --- a/secure/index.ts +++ b/secure/index.ts @@ -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((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"); diff --git a/secure/package.json b/secure/package.json index 90ccb130b..9c4b5bf03 100644 --- a/secure/package.json +++ b/secure/package.json @@ -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" }, diff --git a/secure/pdf-parse.d.ts b/secure/pdf-parse.d.ts new file mode 100644 index 000000000..225937866 --- /dev/null +++ b/secure/pdf-parse.d.ts @@ -0,0 +1,10 @@ +declare module "pdf-parse" { + function pdfParse(dataBuffer: Buffer): Promise<{ + numpages: number; + numrender: number; + info: Record; + metadata: Record; + text: string; + }>; + export default pdfParse; +} diff --git a/secure/personality.ts b/secure/personality.ts new file mode 100644 index 000000000..16dbd81b1 --- /dev/null +++ b/secure/personality.ts @@ -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; + getUserProfile: (userId: number) => Promise; + updateUserProfile: (userId: number, updates: Partial) => Promise; + learnFromConversation: (userId: number, userMessage: string, botResponse: string) => Promise; + getTraits: () => Promise; + updateTraits: (updates: Partial) => Promise; +}; + +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 = { + preferredTone: "friendly", + interests: [], + recentTopics: [], + interactionCount: 0, + lastSeen: new Date(), + notes: [], +}; + +export async function createPersonality(storage: Storage): Promise { + // 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(); + + async function loadUserProfile(userId: number): Promise { + // 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 { + // Update cache + profileCache.set(profile.userId, profile); + // Persist to storage (Redis + PostgreSQL) + await storage.saveUserProfile(profile); + } + + return { + async getSystemPrompt(userId: number): Promise { + 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 - Run JavaScript code +- /python or /py - Run Python code +- /ts - Run TypeScript code +- /bash or /sh - Run shell commands +- /run - 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 "" "" - Schedule recurring AI tasks +- /tasks - List scheduled tasks +- /deltask - 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 { + return loadUserProfile(userId); + }, + + async updateUserProfile(userId: number, updates: Partial): Promise { + const profile = await loadUserProfile(userId); + Object.assign(profile, updates); + await saveUserProfile(profile); + }, + + async learnFromConversation( + userId: number, + userMessage: string, + botResponse: string + ): Promise { + 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 { + return { ...traits }; + }, + + async updateTraits(updates: Partial): Promise { + 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; +} diff --git a/secure/railway.json b/secure/railway.json index 39c1c8402..00de9201e 100644 --- a/secure/railway.json +++ b/secure/railway.json @@ -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 } } diff --git a/secure/railway.toml b/secure/railway.toml new file mode 100644 index 000000000..31a65137a --- /dev/null +++ b/secure/railway.toml @@ -0,0 +1,10 @@ +[build] +builder = "dockerfile" +dockerfilePath = "Dockerfile" + +[deploy] +startCommand = "node dist/index.js" +healthcheckPath = "/health" +healthcheckTimeout = 30 +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 3 diff --git a/secure/sandbox.ts b/secure/sandbox.ts index f7c087dd8..d44aa8208 100644 --- a/secure/sandbox.ts +++ b/secure/sandbox.ts @@ -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; + runCode: (language: string, code: string) => Promise; isAvailable: () => Promise; + 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 = { + 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 { }); } +/** + * Check if Piston API is available + */ +async function checkPiston(): Promise { + 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 { + 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 { + 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 { - if (!sandboxConfig.enabled) return false; - return checkDocker(); + const backend = await detectBackend(); + return backend !== "none"; }, async run(command: string, stdin?: string): Promise { + 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 { + 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; } } diff --git a/secure/scheduler.ts b/secure/scheduler.ts index 976a107ee..240428e79 100644 --- a/secure/scheduler.ts +++ b/secure/scheduler.ts @@ -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; - start: () => void; + start: () => Promise; 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(); - const cronJobs = new Map(); + const cronJobs = new Map>(); async function executeTask(task: ScheduledTask): Promise { 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 { 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); } diff --git a/secure/storage.ts b/secure/storage.ts new file mode 100644 index 000000000..391e6c9fa --- /dev/null +++ b/secure/storage.ts @@ -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; + getTask: (id: string) => Promise; + getAllTasks: () => Promise; + deleteTask: (id: string) => Promise; + + // Conversations (Redis cache) + getConversation: (userId: number) => Promise; + saveConversation: (userId: number, messages: ConversationMessage[]) => Promise; + clearConversation: (userId: number) => Promise; + + // Personality (Redis + PostgreSQL) + getUserProfile: (userId: number) => Promise; + saveUserProfile: (profile: UserProfile) => Promise; + getPersonalityTraits: () => Promise; + savePersonalityTraits: (traits: PersonalityTraits) => Promise; + + // Health + isHealthy: () => Promise; + close: () => Promise; +}; + +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(); + const conversations = new Map(); + const userProfiles = new Map(); + 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; + close: () => Promise; +}> { + 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): 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): 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): 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; + close: () => Promise; +}> { + 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 { + const memory = createMemoryStorage(); + + let pgStorage: Awaited> | null = null; + let redisStorage: Awaited> | 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 { + // 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 { + // 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 { + // 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 { + // 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(); + }, + }; +} diff --git a/secure/telegram.ts b/secure/telegram.ts index af3b676c8..33286c259 100644 --- a/secure/telegram.ts +++ b/secure/telegram.ts @@ -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; stop: () => Promise; - webhookHandler: (path?: string) => ReturnType; }; 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 - Run JavaScript +/python - Run Python +/ts - Run TypeScript +/bash - Run shell commands +/run - 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 - Run JavaScript +/python or /py - Run Python +/ts - Run TypeScript +/bash or /sh - Run shell +/run - 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 "" "" +/tasks - List tasks +/deltask - 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 + 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 \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 { + 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} \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 - Run JavaScript + bot.command("js", (ctx) => runCodeCommand(ctx, "javascript", "js")); + + // Command: /python - Run Python + bot.command("python", (ctx) => runCodeCommand(ctx, "python", "python")); + bot.command("py", (ctx) => runCodeCommand(ctx, "python", "py")); + + // Command: /ts - Run TypeScript + bot.command("ts", (ctx) => runCodeCommand(ctx, "typescript", "ts")); + + // Command: /bash - Run Bash + bot.command("bash", (ctx) => runCodeCommand(ctx, "bash", "bash")); + bot.command("sh", (ctx) => runCodeCommand(ctx, "bash", "sh")); + + // Command: /run - 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 + +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 + 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 "" "" + +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 + 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 "); + 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 { - return webhookCallback(bot, "http", { path }); - }, }; } diff --git a/secure/tsconfig.json b/secure/tsconfig.json index ed701170b..704e636cb 100644 --- a/secure/tsconfig.json +++ b/secure/tsconfig.json @@ -4,6 +4,7 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "lib": ["ES2022"], + "types": ["node"], "outDir": "./dist", "rootDir": ".", "strict": true, diff --git a/secure/webhooks.ts b/secure/webhooks.ts index 430d0e50d..707891612 100644 --- a/secure/webhooks.ts +++ b/secure/webhooks.ts @@ -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 }; }