Skip to content

Architecture

Blacki keeps the agent process small and delegates expensive capabilities to managed APIs when they are enabled.

Runtime

The container runs:

python -m blacki.server

src/blacki/server.py creates a FastAPI application with Google ADK's get_fast_api_app, initializes SQLite-backed storage, optionally starts Telegram long polling, and exposes /live, /ready, and /health.

src/blacki/agent.py creates the LlmAgent, selects a native Gemini model or a LiteLLM/OpenRouter model from the environment, registers tools, and assembles the ADK application plugins.

Request paths

Telegram

  1. The Telegram bot polls Telegram's HTTPS API for updates.
  2. A Telegram chat is mapped to the ADK runtime.
  3. Text is sent as an ADK text part. Telegram photos are downloaded, validated, and sent as caption/default text plus an inline JPEG part.
  4. The agent calls the configured model and tools.
  5. The response is sent back through Telegram's API. When the private Kokoro tool is enabled and selected, it synthesizes a bounded MP3 in memory and sends it directly to the same chat or topic through Telegram sendAudio.

Long polling is outbound. It does not require a public webhook, domain, TLS certificate, or inbound application port.

HTTP and ADK web interface

FastAPI listens on port 8080 inside the container. Docker Compose maps that port to 127.0.0.1:8080 on the host by default. The web interface is disabled in the VPS samples and can be reached securely through an SSH tunnel when enabled.

Persistence boundaries

Blacki uses different stores for different responsibilities:

Responsibility Store Persists across restart?
ADK HTTP/web conversation sessions In-memory ADK session service No
Reminders, calories, workouts, preferences, declarative data SQLite Yes
Telegram runtime session metadata SQLite-backed application state Yes
Telegram photo parts in ADK history SQLite-backed session events Yes
Optional Mem0 memory with local Qdrant /app/data Yes with the Compose volume
Optional Mem0 memory with Qdrant Cloud Managed Qdrant Provider-managed
Zepto OAuth credentials /app/data/credentials/zepto-mcp-remote/ Yes with the Compose volume
Application logs and traces JSON files under /app/logs Yes with the Compose volume

Compose maps .adk_state/, data/, and logs/ from the host. Back up the first two as application state.

Native Telegram photos are capped at 10 MB because ADK retains inline user parts in session history for later conversation turns. This bounds session database growth and image replay costs while preserving multimodal context.

Managed integrations

Optional tools follow the project's cloud-first principle:

  • models use OpenRouter or Google;
  • search can use Exa with Brave as a fallback;
  • browser automation can use Browser Use Cloud;
  • speech synthesis can use a private Kokoro API reachable over Tailscale;
  • grocery shopping can use Zepto's hosted MCP server for one allowlisted, shared account;
  • vector memory can use Qdrant Cloud; and
  • code execution can use an OpenSandbox server.

Each integration degrades independently when its credentials are absent. Startup still requires at least one model API key.

Kokoro speech delivery is registered only on the Telegram-specific root agent. The public ADK runner and delegated task worker cannot call it. Blacki sends only the requested speech text to the configured Kokoro endpoint, accepts a bounded MP3 response, keeps it in memory, and serializes synthesis through Telegram upload so concurrent chats cannot retain multiple audio payloads. It does not retry Telegram delivery automatically. Configuring the tool disables content-rich ADK and OpenInference logging, although the local ADK session database still retains tool calls and arguments as normal conversation history.

Zepto is registered only on a Telegram-specific root-agent runner. The public ADK HTTP runner and delegated task worker never receive the Zepto toolset. Unauthorized Telegram identities are rejected before Blacki opens an MCP connection. ADK confirmation is required only when an order or payment tool uses confirmOrder=true; other individual Zepto tools run directly. Its OAuth files are plaintext protected by a 0700 directory and 0600 file permissions; they are not encrypted. Shopping prompts, tool calls, and results remain in the local ADK session database and are sent to the configured model as part of normal agent execution.

Sandbox credential threat model

Sandbox commands may process untrusted uploaded files, fetched web content, and nested-agent instructions. Any of those inputs can attempt prompt injection or run shell commands that inspect the process environment. Blacki therefore treats every general-purpose sandbox as untrusted:

  • model, repository, search, OpenRouter, Google, Telegram, and application credentials are never copied into the sandbox environment;
  • SANDBOX_API_KEY authenticates the host-side OpenSandbox connection only;
  • sandbox SDK exception details are not returned to tools or written to logs, because provider errors can echo credential material; and
  • the Gemini CLI sandbox skill is not registered while no least-privilege credential broker exists.

An authenticated capability must be implemented as a separately authorized, short-lived broker operation. Adding a standing environment variable is not an acceptable opt-in path.

Health semantics

/live is side-effect-free and process-only. /ready checks the already initialized SQLite connection and returns HTTP 503 during startup or database failure. /health delegates to the same implementation for compatibility. Optional Mem0 memory is not readiness-critical and probes never lazily initialize it.

Security boundary

The default deployment is not a public web application:

  • the host port binds to loopback;
  • the production Compose overlay defeats public-bind and development-feature overrides;
  • Telegram needs outbound HTTPS only;
  • secrets live in an ignored .env file; and
  • .dockerignore excludes secrets and runtime state from image builds.

Public web access, authentication, TLS termination, and reverse-proxy configuration are intentionally separate architectural decisions.