Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,6 @@ SERVE_WEB_INTERFACE=false
RELOAD_AGENTS=false
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=false
ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=false
# The ADK task worker is enabled by default. Set false to opt out.
# User-scoped account tools remain root-only.
TASK_WORKER_ENABLED=true

# ---------------------------------------------------------------------------
# Telegram Bot Configuration
Expand Down Expand Up @@ -225,7 +222,7 @@ ZEPTO_MCP_ENABLED=false
# 1. Install: uv pip install opensandbox-server
# 2. Configure: uvx opensandbox-server init-config ~/.sandbox.toml --example docker
# 3. Edit port: ~/.sandbox.toml: port = 9090
# 4. Pre-pull image: docker pull opensandbox/code-interpreter:v1.0.2
# 4. Pre-pull image: docker pull ghcr.io/queryplanner/athena-sandbox:latest
# 5. Start server: uvx opensandbox-server
#
# Enable sandbox tools
Expand All @@ -240,12 +237,14 @@ ZEPTO_MCP_ENABLED=false
# Sandbox TTL in minutes (default: 30)
# SANDBOX_TIMEOUT_MINUTES=30
#
# Resource limits per sandbox
# SANDBOX_MEMORY_LIMIT=512Mi
# Resource limits per sandbox. Chromium needs headroom; the default is 1Gi.
# SANDBOX_MEMORY_LIMIT=1Gi
# SANDBOX_CPU_LIMIT=0.5
#
# Docker image for sandboxes (default: opensandbox/code-interpreter:v1.0.2)
# SANDBOX_IMAGE=opensandbox/code-interpreter:v1.0.2
# Docker image for sandboxes. The default image includes Chromium, a pinned
# agent-browser, and agent-browser's bundled usage guides. The published image
# is linux/amd64; build an arm64 equivalent for Apple Silicon sandbox hosts.
# SANDBOX_IMAGE=ghcr.io/queryplanner/athena-sandbox:latest
#
# Security boundary: Blacki never copies model, repository, search, Telegram, or
# application credentials into a general-purpose sandbox. SANDBOX_API_KEY is used
Expand Down
43 changes: 28 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,39 @@ to managed services.

### Self-Hosted Agent, Managed Tools

The agent itself runs on a $5-10/month VPS, but tools should use managed cloud services whenever possible:
The agent itself runs on a $5-10/month VPS. Expensive or stateful tools should
use a managed service or an isolated sandbox whenever possible:

- **Browser automation** → Browser Use Cloud (not local Playwright/Puppeteer)
- **Browser automation** → `agent-browser` inside the session OpenSandbox
(not host Chromium, local Playwright, or Puppeteer)
- **Search** → Brave Search API (not self-hosted SearXNG)
- **LLM** → OpenRouter/Google (not self-hosted Ollama)

This architecture keeps the server footprint minimal while giving the agent powerful capabilities. Managed services handle:
- Infrastructure maintenance
- Scaling
- Updates and security patches
- Geographic distribution (e.g., Browser Use proxies)
This architecture keeps the agent host footprint minimal. Managed services
handle maintenance, scaling, updates, and geographic distribution; the
OpenSandbox handles code execution and browser isolation.

### Browser boundary

The root agent calls `agent_browser(args)`, which runs the pinned
`agent-browser` CLI in the current session's OpenSandbox. Chromium, its profile,
cookies, and screenshots stay in that temporary sandbox; the Blacki host runs no
browser and stores no browser profile. The sandbox image must provide
`agent-browser`, Chromium, and the bundled `agent-browser` usage guides. Size
the sandbox memory and CPU for Chromium, and treat every page as untrusted
input. Do not add a host-side browser or copy browser state onto the Blacki host.

### Why This Matters

Self-hosting everything sounds appealing, but for a personal assistant:
- Browser automation requires headless Chrome, which consumes significant RAM/CPU
- Browser automation needs headless Chromium; Blacki runs it in the sandbox
rather than on the agent host
- Search indices need constant crawling and updates
- LLM inference needs GPU or is painfully slow

By delegating to managed services, the agent stays fast, cheap, and reliable. The server can be a tiny VPS that costs less than a coffee per month.
By delegating to managed services and isolating Chromium in the sandbox, the
agent host stays fast, cheap, and reliable. The server can be a tiny VPS that
costs less than a coffee per month.

### Tool Design Principles

Expand All @@ -45,9 +58,9 @@ capability boundaries:
- `src/blacki/server.py` starts the FastAPI and ADK runtime, health endpoints,
storage, and optional Telegram polling.
- `src/blacki/agent.py` builds the root agent, model configuration, plugins, and
the public/delegated agent variants.
the public and private Telegram agent variants.
- `src/blacki/registry.py` is the tool factory. It decides which tools are
exposed for the public runner, the Telegram root agent, and delegated workers.
exposed for the public runner and the Telegram root agent.
- `src/blacki/prompts/` contains instruction text and ADK policy plugins.
Feature-specific operating and safety instructions belong in a loaded skill
when possible.
Expand All @@ -58,8 +71,8 @@ capability boundaries:
a sandbox.

The root agent may receive private, user-scoped integrations such as Google
Health, Gmail, Zepto, TTS, and durable user files. Public ADK requests and
delegated workers receive only the tools explicitly allowed for their boundary.
Health, Gmail, Zepto, TTS, and durable user files. Public ADK requests receive
only the tools explicitly allowed for their boundary.
Shared session-sandbox access does not grant access to another user's accounts.

### File lifecycle and storage boundaries
Expand Down Expand Up @@ -96,7 +109,7 @@ deduplication, failure cleanup, and user-facing consent.
Gmail is a direct Gmail REST API connector under `src/blacki/gmail/`, not a
general-purpose shared service. OAuth credentials are stored per private
Telegram user, Gmail tools are registered only for the private root-agent
flow, and delegated workers remain unable to call Gmail. The Gmail skill in
flow; public agents cannot call Gmail. The Gmail skill in
`src/blacki/skills/gmail/SKILL.md` supplies the agent-facing usage and safety
rules; loading the skill is not itself an authorization grant.

Expand Down Expand Up @@ -155,7 +168,7 @@ features and data paths that it may touch. The implementation agent must
actively consider at least:

- user identity and authorization boundaries;
- Telegram, ADK session, and delegated-worker behavior;
- Telegram and ADK session behavior;
- sandbox lifetime and whether a file is temporary or durable;
- Cloudflare R2 storage, catalog, retention, restore, and deletion behavior;
- OAuth scopes, token ownership, external APIs, and privacy implications;
Expand Down
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,17 @@ 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;
- browser automation uses the dedicated `agent_browser(args)` tool inside the
current session's OpenSandbox instance. Its argument array is shell-quoted,
and commands time out after 90 seconds. The default
`ghcr.io/queryplanner/athena-sandbox:latest` image provides Chromium, a pinned
agent-browser, and agent-browser's bundled usage guides; the tool itself does
not install dependencies. Run `agent_browser(args=["skills", "get", "core"])`
for the usage guide, or `agent_browser(args=["--help"])` for the command list.
The tool is available to the root agent when sandbox tools are enabled.
Blacki uses one root agent without a delegated task worker. Chromium profiles
and screenshots remain in the temporary sandbox, and this tool adds no
saved-login service or durable file retention;
- 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;
Expand All @@ -161,7 +171,7 @@ 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
The public ADK runner 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
Expand All @@ -170,7 +180,7 @@ 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.
ADK HTTP runner never receives 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
Expand Down
27 changes: 8 additions & 19 deletions docs/base-infra/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,23 +197,6 @@ MEM0_QDRANT_PATH=/app/data/qdrant

If Qdrant Cloud values are present, the provider stores the vectors remotely.

## Delegated task worker

| Variable | Default | Purpose |
| --- | --- | --- |
| `TASK_WORKER_ENABLED` | `true` | Register a same-privilege ADK task worker |

By default, the root agent can delegate one complex task at a time to a
registered ADK task-mode child. Set `TASK_WORKER_ENABLED=false` to opt out. The
worker receives an independently built copy of the root agent's user-facing
toolset and shares the same ADK session state, so sandbox tools reconnect to the
same sandbox ID.

This is not a security boundary or a background worker pool: the worker has the
root agent's privileges, runs within the request, and cannot delegate
recursively. Delegation can add another model and tool turn, increasing latency
and model usage.

## OpenSandbox

| Variable | Default | Purpose |
Expand All @@ -222,9 +205,15 @@ and model usage.
| `SANDBOX_DOMAIN` | `localhost:9090` | OpenSandbox server address |
| `SANDBOX_API_KEY` | unset | Optional server credential |
| `SANDBOX_TIMEOUT_MINUTES` | `30` | Sandbox lifetime |
| `SANDBOX_MEMORY_LIMIT` | `512Mi` | Per-sandbox memory setting |
| `SANDBOX_MEMORY_LIMIT` | `1Gi` | Per-sandbox memory; Chromium needs headroom |
| `SANDBOX_CPU_LIMIT` | `0.5` | Per-sandbox CPU setting |
| `SANDBOX_IMAGE` | project default | Code-interpreter image |
| `SANDBOX_IMAGE` | `ghcr.io/queryplanner/athena-sandbox:latest` | Chromium + agent-browser sandbox image |

The default sandbox image provides Chromium, a pinned agent-browser, and the
bundled agent-browser usage guides. The `agent_browser` tool runs in this
sandbox only; it never runs on the Blacki host. The published image is
`linux/amd64`; on arm64 hosts, build an equivalent image or point
`SANDBOX_IMAGE` at an arm64 build.

Running a local OpenSandbox server adds Docker and resource requirements beyond
the Blacki golden path.
Expand Down
2 changes: 1 addition & 1 deletion docs/gmail.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ and can be consumed only once.
4. Send `/disconnect_gmail` and confirm when access should be revoked.

Each private Telegram chat has its own connection. Group chats, public agents,
delegated workers, local test identities, and unconnected users receive no
local test identities, and unconnected users receive no
Gmail tools.

## Supported operations
Expand Down
2 changes: 1 addition & 1 deletion docs/telegram-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ server on another machine. Blacki calls Kokoro's OpenAI-compatible
`/v1/audio/speech` endpoint, keeps the bounded MP3 in memory, and uploads it to
the current Telegram chat or topic. Synthesis and upload are serialized so only
one audio payload is retained at a time. The tool is not registered on the
public ADK HTTP runner or delegated task worker.
public ADK HTTP runner.

When this private tool is configured, Blacki disables content-rich ADK and
OpenInference logging. Tool notifications may show that speech synthesis is
Expand Down
2 changes: 1 addition & 1 deletion docs/zepto-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ clients.

- One private bridge directory represents one Zepto account and shared cart.
- Only explicitly allowlisted positive private Telegram chat IDs can connect.
Groups, topics, task workers, and arbitrary HTTP user IDs are rejected.
Groups, topics, and arbitrary HTTP user IDs are rejected.
- Zepto is loaded into a Telegram-only ADK runner. The unauthenticated ADK HTTP
runner never receives the Zepto skill or toolset, even if a caller spoofs an
allowlisted Telegram user ID.
Expand Down
39 changes: 1 addition & 38 deletions src/blacki/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import TYPE_CHECKING, Any

from dotenv import load_dotenv
from google.adk.agents import BaseAgent, LlmAgent
from google.adk.agents import LlmAgent
from google.adk.apps import App
from google.adk.apps.app import EventsCompactionConfig
from google.adk.plugins.base_plugin import BasePlugin
Expand All @@ -28,7 +28,6 @@
return_description_root,
return_global_instruction,
return_instruction_root,
return_instruction_task_worker,
)
from .prompts.policies import (
DomainPolicyPlugin,
Expand Down Expand Up @@ -56,8 +55,6 @@
logger = logging.getLogger(__name__)

logging_callbacks = LoggingCallbacks()
TASK_WORKER_NAME = "task_worker"
TASK_WORKER_ENABLED_VALUES = frozenset({"1", "true", "yes"})
AUTO_COMPACTION_TOKEN_THRESHOLD = 200_000
AUTO_COMPACTION_EVENT_RETENTION_SIZE = 8
# ADK 2.5.0 requires the sliding-window fields even for token-only
Expand Down Expand Up @@ -140,14 +137,6 @@ def _find_and_load_dotenv() -> None:
break


def _task_worker_enabled() -> bool:
"""Return whether the default-on delegated task worker is enabled."""
return (
os.getenv("TASK_WORKER_ENABLED", "true").strip().lower()
in TASK_WORKER_ENABLED_VALUES
)


def create_agent(*, include_user_scoped_tools: bool = False) -> LlmAgent:
"""Create and configure the root agent.

Expand Down Expand Up @@ -185,31 +174,6 @@ def create_agent(*, include_user_scoped_tools: bool = False) -> LlmAgent:
recover_telegram_tool_error if include_user_scoped_tools else None
)

sub_agents: list[BaseAgent] = []
if _task_worker_enabled():
worker_tools = build_tools(tool_config, include_user_scoped_tools=False)
worker_tools.append(PreloadMemoryTool())
sub_agents.append(
LlmAgent(
name=TASK_WORKER_NAME,
description=(
"Complete one complex delegated task with Blacki's "
"non-private tools and shared session sandbox"
),
mode="task",
before_agent_callback=logging_callbacks.before_agent,
after_agent_callback=after_agent_callbacks.copy(),
model=build_model(),
instruction=return_instruction_task_worker(),
tools=worker_tools,
before_model_callback=logging_callbacks.before_model,
after_model_callback=after_model_callbacks.copy(),
before_tool_callback=before_tool_callbacks.copy(),
after_tool_callback=logging_callbacks.after_tool,
on_tool_error_callback=telegram_tool_error_callback,
)
)

return LlmAgent(
name="blacki",
description=return_description_root(),
Expand All @@ -223,7 +187,6 @@ def create_agent(*, include_user_scoped_tools: bool = False) -> LlmAgent:
before_tool_callback=before_tool_callbacks,
after_tool_callback=logging_callbacks.after_tool,
on_tool_error_callback=telegram_tool_error_callback,
sub_agents=sub_agents,
)


Expand Down
20 changes: 0 additions & 20 deletions src/blacki/prompts/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,21 +151,6 @@
{NATURAL_CHAT_STYLE}"""


TASK_WORKER_BEHAVIOR = f"""\
{CORE_ASSISTANT_BEHAVIOR}

<delegated_task_worker>
Complete only the task delegated by the root agent. You have the same non-private
tools as the root agent, including access to the same session sandbox when sandbox
tools are enabled. User-scoped account tools such as Zepto and Gmail remain root-only.

Do not create or delegate to another worker. Do not broaden the task, repeat a
state-changing tool call, or retry a non-idempotent operation without evidence that
it is safe. Respect the original user's authorization and report a concise result to
the root agent when the delegated task is complete.
</delegated_task_worker>"""


def return_description_root() -> str:
"""Return the root agent's short capability description."""
return "A privacy-conscious personal assistant for questions and explicit tracking"
Expand All @@ -176,11 +161,6 @@ def return_instruction_root() -> str:
return ROOT_ASSISTANT_BEHAVIOR


def return_instruction_task_worker() -> str:
"""Return behavior for the delegated task worker."""
return TASK_WORKER_BEHAVIOR


def return_global_instruction(ctx: ReadonlyContext) -> str:
"""Return leading safety, privacy, and temporal context for each request."""
_ = ctx
Expand Down
1 change: 1 addition & 0 deletions src/blacki/prompts/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@

SEARCH_CONFLICTING_TOOL_NAMES = frozenset(
{
"agent_browser",
"sandbox_run_command",
"sandbox_write_file",
"sandbox_read_file",
Expand Down
Loading
Loading