Channel Agent Builder System
This document describes the editable channel-agent system now implemented in bot0.
It covers how bot0 creates local file-backed agents, how those agents become channel targets, and how the builder avoids relying on the bot0 monorepo or existing package examples.
See also: Channel Agent Runtime Boundary, which documents the package-owned system prompt used when these agents run.
Summary
bot0 can now create user-owned channel agents as local packages under the bot0 data directory:
~/.bot0/agents/<agent-id>/ agent.json skills/ <entry-skill>/ SKILL.md sub-skills.json benchmarks.json <optional-sub-skill>/ SKILL.md sub-skills.json benchmarks.json state/ README.md
The daemon discovers these packages, exposes production packages as channel AgentTargets, and routes incoming Telegram/Slack/WhatsApp/SMS messages by agentId.
The core idea is:
agent_createis the canonical skeleton generator.channel-agent-builderis the protected system skill that teaches bot0 how to use the tools.- existing packages are not examples to copy.
- channel connection and route state is inspected through tools, not config files.
- secrets never live in agent packages.
Components
| Component | Purpose |
|---|---|
~/.bot0/agents | User-owned editable agent packages. In dev, this may be ~/.bot0-dev/agents or BOT0_DATA_DIR/agents. |
agent.json | Package manifest: id, label, description, entry skill, status, allowed tools, channel defaults. |
skills/* | Skill-backed runtime behavior for the agent. The entry skill orchestrates optional sub-skills. |
state/ | Agent-local durable state files. Skills should refer to runtime-provided state context instead of hardcoded absolute paths. |
channel-agent-builder | Protected system skill installed under .system; hidden from the editable Skills library. |
| Agent workspace tools | Create, read, validate, update, and deploy packages. |
| Channel route tools | List connections/routes and preflight deployments without reading config directly. |
Builder Skill
channel-agent-builder is installed at daemon startup into:
~/.bot0/skills/.system/channel-agent-builder
It is protected:
ownershipKind: "system"isProtected: trueisVisibleInLibrary: falsecanEdit: false
The builder skill is used when a user asks to create, modify, validate, preflight, or deploy a channel agent.
Critical builder rules:
- Do not inspect bot0 source code, repo docs, or daemon config files.
- Do not inspect existing bundled or user agent package files to copy structure.
- Use
agent_listonly for metadata and id collision awareness. - Use
agent_createas the source of truth for the package skeleton. - Use
agent_readonly when modifying the specific user-owned agent named by the user. - Use channel route tools instead of reading config.
- Do not claim deployment is live until
agent_deploy_channelsucceeds.
Creation Flow
For a new channel agent, bot0 should follow this flow:
- Load
channel-agent-builder. - Call
agent_listonly to check existing IDs and names. - Choose a valid lowercase
agentId. - Choose the narrowest practical
allowedTools. - Call
agent_create. - Edit only files inside the new package returned by
agent_create. - Call
agent_validate. - If deployment was requested, run route preflight and then deploy only if safe.
Example agent_create call for a multi-skill stateful agent:
{ "id": "meeting-miner", "label": "Meeting Miner", "description": "Turns messy meeting notes into concise briefs and tracks open action items.", "entrySkill": "meeting-miner", "status": "production", "allowedTools": ["execute_skill", "read", "write_file"], "subSkills": [ { "id": "meeting-miner-parser", "description": "Parse messy meeting notes into structured decisions, actions, blockers, and questions." }, { "id": "meeting-miner-action-tracker", "description": "Normalize, merge, and update tracked action items." }, { "id": "meeting-miner-response-writer", "description": "Write concise Telegram-friendly replies." } ], "stateFiles": ["meeting-log.json", "action-items.json"] }
agent_create creates:
- the manifest
- the entry skill
- each requested sub-skill
sub-skills.jsonwiring- empty benchmark files
- requested state files under
state/ README.md
This removes the need for bot0 to browse existing packages for structure.
Workspace Tools
agent_list
Lists package metadata only:
idlabeldescriptionentrySkillstatussource
It intentionally does not expose directory or skillsDirectory. This keeps new-agent creation from drifting into "copy an existing package" behavior.
agent_read
Reads one user-owned package and returns editable locations for that target package.
Use only when modifying an existing user agent.
agent_create
Creates a user-owned package under ${getChannelPaths().bot0Dir}/agents.
Important inputs:
idlabeldescriptionentrySkillstatusallowedToolssubSkillsstateFileschannelDefaultsoverwrite
Guardrails:
- validates ID and skill names
- rejects path traversal
- rejects bundled ID collisions by default
- rejects secret-looking manifest values
- clears discovery caches after writes
agent_update_manifest
Updates agent.json for a user-owned package.
It preserves the same manifest validation and secret rejection rules as creation.
agent_validate
Validates user packages.
Current checks include:
- entry skill exists
- declared sub-skills exist
- skill files do not hardcode local agent workspace paths such as
~/.bot0-dev/agents/...
Validation returns a structured checks array with valid and errors.
agent_deploy_channel
Creates or replaces a daemon channel route for an agent.
Deployment now runs the same preflight logic used by channel_route_preflight. If an enabled route already owns the target connection, deployment fails unless disableConflictingRoutes=true is explicitly passed.
disableConflictingRoutes=true should only be used after the user explicitly approves disabling an existing active route.
Channel Route Tools
channel_connection_list
Lists configured channel connections using safe metadata only.
It redacts secret values. For example, Telegram bot tokens are shown only as booleans such as:
{ "hasBotToken": true }
It does not expose raw tokens, webhook secrets, API keys, passwords, or credentials.
channel_route_list
Lists existing channel routes:
- route id
- route name
- enabled state
- channel
- connection id
- agent id
- route scope
channel_route_preflight
Checks whether a deployment can safely proceed.
It reports:
- missing channel connections
- disabled connections
- channel/connection mismatch
- broad
anyscope warnings - active route conflicts
- available connections for that channel
The builder should call this before agent_deploy_channel.
Runtime Behavior
The channel runtime path is:
- channel adapter normalizes the inbound event.
- route resolver finds a route and
agentId. - the agent registry resolves the file-backed
AgentTarget. - the target builds a lean agent-owned system prompt from
agent.jsonand the entry skillSKILL.md. - the target invokes the daemon task runner with that
systemPromptoverride and the packageallowedTools. - the entry skill, embedded in the agent prompt, handles the domain behavior.
- optional sub-skills run through
execute_skill. - the final response is sent back through the channel adapter.
File-backed agent prompts include:
You are <label>, a file-backed channel agent. Agent id: <agent-id> Agent package directory: <path> Agent skills directory: <path> Agent state directory: <path> Entry skill: <entry-skill> <entry_skill> ...SKILL.md... </entry_skill>
The packaged agent run does not use the default interactive bot0 system prompt. Normal skills still use the regular bot0 prompt plus loaded skill instructions.
Skills should use the runtime-provided state location instead of hardcoding absolute paths.
The channel gateway rebuilds the default agent registry dynamically when a fixed registry is not supplied, so newly created agents can be routed without rebuilding the app.
Tool Scope
agent.json.allowedTools narrows runtime access for the channel agent. It cannot expand daemon permissions.
The runtime does not automatically add skill. The entry skill is already embedded in the package-owned agent prompt. Add skill only when the package explicitly needs the skill tool.
For most stateful orchestrator agents, a narrow tool set is:
["execute_skill", "read", "write_file"]
Add tools only when the agent behavior requires them. Avoid edit_file unless the agent truly needs patch-style edits.
Deployment Safety
Routes are effectively exclusive per channel connection.
If one enabled route already owns a Telegram bot connection, deploying another enabled route to the same connection would cause one route to be normalized disabled. The new flow prevents silent failure:
channel_route_preflightreports the conflict.agent_deploy_channelrejects the deployment by default.- deployment can disable conflicting routes only with explicit
disableConflictingRoutes=true.
This keeps bot0 from accidentally replacing a live agent route.
Security Boundaries
Agent packages may contain:
- prompts
- skill instructions
- non-secret metadata
- state files
- route references by ID
Agent packages must not contain:
- bot tokens
- OAuth secrets
- API keys
- webhook secrets
- passwords
- credentials
Channel connections and provider credentials remain in existing config, connector, proxy, or secure storage surfaces.
Current Media Limitation
Telegram image/file/audio messages can be normalized with metadata and, when downloads are enabled, local file paths.
The current generic channel-agent path does not automatically pass image bytes into the model as multimodal input. Image-dependent agents should not claim they can inspect pixels unless an explicit OCR/image-analysis tool or parser path is available.
For now, a receipt or image-based agent should either:
- require pasted text or a useful caption, or
- use a dedicated OCR/image tool once one exists.
Example User Prompt
A good non-handholding test prompt:
Create me a Telegram channel agent named meeting-miner. It should turn messy meeting notes or transcript chunks into concise meeting briefs, keep track of open action items, and support basic commands for status, completed actions, blockers, due-today items, and recaps. Use a few focused sub-skills where it makes sense. Keep replies short and Telegram-friendly. Validate it, and deploy it to Telegram DM if that can be done safely with the configured connections.
The expected behavior is that bot0:
- uses
agent_createwithsubSkillsandstateFiles - does not inspect existing package files
- validates the package
- calls
channel_connection_list - calls
channel_route_list - calls
channel_route_preflight - deploys only if preflight passes
Verification
Focused regression coverage includes:
- protected
channel-agent-builderinstall/discovery - metadata-only
agent_list agent_createsub-skill and state-file scaffolding- hardcoded local path validation
- route conflict rejection
- explicit conflict disabling
- safe channel connection metadata redaction
- channel route preflight blockers
Useful local checks:
node --import tsx --test \ packages/daemon/src/tools/agent-workspace.test.ts \ packages/daemon/src/tools/channel-workspace.test.ts \ packages/daemon/src/skill/system-skills.test.ts pnpm --filter @bot0/daemon typecheck