Tools extend Chalie’s capabilities by allowing sandboxed execution of external code. Tools are containerized, versioned, and can be triggered on-demand or on a schedule.
The tools system provides:
Tool Registry Service
backend/tools/ directoryTool Container Service
Tool Config Service
*** instead of actual value)Tool Relevance Service
REST API (backend/api/tools.py)
Each tool is a subdirectory in backend/tools/ with three required files:
All tools implement a unified contract: base64-encoded JSON in → JSON out.
The framework sends this to the tool container as a base64-encoded JSON string (CMD arg):
{
"params": {
"query": "user's search query",
"location": "optional param value"
},
"settings": {
"api_key": "abc123",
"endpoint": "https://api.example.com"
},
"telemetry": {
"lat": 35.8762,
"lon": 14.5366,
"city": "Valletta",
"country": "Malta",
"time": "2026-02-20T16:54:01Z",
"locale": "en-MT",
"language": "en-US"
}
}
params: LLM-extracted parameters from the user’s intent (matches manifest parameters schema)settings: Tool-specific config from the database (API keys, endpoints, etc.)telemetry: Flattened client context (always present, fields may be null)The tool must write this JSON to stdout (and only stdout):
{
"text": "Human-readable result text. Optional.",
"html": "<div style=\"...\">Inline HTML card. Optional.</div>",
"title": "Dynamic card title (optional, overrides manifest title)",
"error": "Error message string (if operation failed)"
}
text: Plain text result. If manifest output.synthesize: true, Chalie rewrites this in its own voice.html: HTML fragment for UI card display. Rules:
style="..." attributes, no <style> blocks or external CSS<script> tags, no event handlers (onclick, onerror, etc.), no javascript: URIs<html>, <head>, <body> tags. Must be self-contained.<iframe>, <form>, <input>, <object>, <embed>, <base>title: Optional dynamic title to override the manifest card titleerror: If present, triggers fallback behavior and skips text/html processingRequired fields:
{
"name": "tool_name",
"description": "Human-readable description for Chalie to understand what this tool does",
"version": "1.0.0",
"category": "search|calculation|memory|integration|utility|context|research|communication",
"trigger": {
"type": "on_demand|cron|webhook"
},
"parameters": {
"param_name": {
"type": "string|integer|float|boolean",
"description": "What this parameter does",
"required": true,
"default": null
}
},
"returns": {
"text": { "type": "string" },
"html": { "type": "string" }
},
"output": {
"synthesize": true,
"ephemeral": false,
"card": {
"enabled": true,
"title": "Card Title",
"accent_color": "#4a90d4",
"background_color": "rgba(74, 144, 212, 0.10)"
}
}
}
Trigger Types:
"on_demand" — Called when relevant during ACT mode (default)"cron" — Runs on schedule, results enqueued as prompts
"schedule" (simple cron: */30 = every 30 minutes)"prompt" (template string, tool output appended)"webhook" — Not currently implementedOutput Configuration:
The output section controls how the tool’s result is displayed:
{
"output": {
"synthesize": true,
"ephemeral": false,
"card": {
"enabled": true,
"title": "Weather in ",
"accent_color": "#4a90d4",
"background_color": "rgba(74, 144, 212, 0.10)"
}
}
}
synthesize: If true, the framework rewrites text in Chalie’s voice. If false, text is hidden.ephemeral: If true, the tool’s output is never assimilated into episodic memory and is excluded from action-completion verification. Use for tools whose output is transient by nature (e.g., current weather). Defaults to false.card.enabled: If true, the framework renders the html field as a UI card.card.title: Default card title (can be overridden by tool’s title in output JSON)card.accent_color: Accent color for the card (CSS color string)card.background_color: Background color for the card (CSS color string)Optional Fields:
{
"icon": "fa-star",
"config_schema": {
"api_key": {
"description": "Your API key",
"secret": true,
"required": true
},
"endpoint": {
"description": "API endpoint URL",
"secret": false,
"default": "https://api.example.com"
}
},
"constraints": {
"timeout_seconds": 9,
"cost_budget": 1000
},
"sandbox": {
"memory": "512m",
"network": "bridge|none|host",
"writable": false
},
"notification": {
"default_enabled": false
}
}
Must be a valid Dockerfile that:
text, html, title, and/or error fields)Example (Python):
FROM python:3.9-slim
WORKDIR /app
COPY . .
RUN pip install -q requests
ENTRYPOINT ["python", "-u", "runner.py"]
Example (Bash):
FROM alpine:3.19
RUN apk add --no-cache bash jq
WORKDIR /tool
COPY runner.sh .
RUN chmod +x runner.sh
ENTRYPOINT ["bash", "runner.sh"]
The tool script receives the formalized payload and must return the formalized output.
Python example:
#!/usr/bin/env python3
import json
import base64
import sys
# Decode base64 payload from command arg
payload = json.loads(base64.b64decode(sys.argv[1]).decode())
params = payload.get("params", {}) # user-provided parameters
settings = payload.get("settings", {}) # stored tool config (API keys, etc.)
telemetry = payload.get("telemetry", {}) # client context (lat, lon, city, etc.)
try:
# Your tool logic here
result_data = fetch_data(params, settings, telemetry)
# Format output with text and optional HTML
output = {
"text": f"Weather: {result_data['temp']}°C and {result_data['condition']}",
"html": f'<div style="padding:16px"><div style="font-size:2rem">{result_data["temp"]}°C</div></div>'
}
print(json.dumps(output))
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
Bash example (canonical reference):
#!/usr/bin/env bash
set -euo pipefail
# Decode base64 payload
PAYLOAD=$(echo "$1" | base64 -d)
# Extract fields using jq
NAME=$(echo "$PAYLOAD" | jq -r '.params.name // "World"')
CITY=$(echo "$PAYLOAD" | jq -r '.telemetry.city // ""')
# Compose text and HTML
TEXT="Hello, $NAME!"
HTML="<div style=\"padding:16px;font-family:sans-serif\"><div style=\"font-size:1.4rem\">Hello, $NAME!</div></div>"
# Output formalized contract JSON
jq -n \
--arg text "$TEXT" \
--arg html "$HTML" \
'{"text": $text, "html": $html}'
List available tools:
curl http://localhost:8080/tools \
-H "Authorization: Bearer YOUR_API_KEY"
Set configuration (API keys, endpoints):
curl -X PUT http://localhost:8080/tools/my_tool/config \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"api_key": "sk-...", "endpoint": "https://..."}'
Test configuration:
curl -X POST http://localhost:8080/tools/my_tool/test \
-H "Authorization: Bearer YOUR_API_KEY"
Returns {"ok": true, "message": "Configuration looks complete"} if all required keys are set.
Get configuration (secrets masked):
curl http://localhost:8080/tools/my_tool/config \
-H "Authorization: Bearer YOUR_API_KEY"
Delete a config key:
curl -X DELETE http://localhost:8080/tools/my_tool/config/api_key \
-H "Authorization: Bearer YOUR_API_KEY"
When user sends a message that matches ACT mode:
[TOOL:name]...[/TOOL] markers and included in LLM contextTools have three status values (from API /tools endpoint):
Every tool container runs with:
sandbox.memory)none)sandbox.writable: true)constraints.timeout_seconds-0.2 reward in procedural memoryOptional per-tool budget tracking (if tool returns budget_remaining field):
Tool output is sanitized before integration:
{...}, function calls, ACTION: keywordsWhen creating a new tool, ensure:
backend/tools/tool_name/name, description, version, trigger, parameters, returnsoutput section with synthesize and card configtrigger.type is one of: on_demand, cron, webhookpython -m json.tool manifest.json to validate JSON syntaxdocker build -t test-tool . succeeds["bash", "runner.sh"] or ["python", "runner.py"])sys.argv[1]params, settings, telemetry from payloadtext, html, title, and/or error fieldsjq or JSON library to avoid shell injection<style> blocks, no <script> tags)PAYLOAD='{"params":{"name":"Test"},"settings":{},"telemetry":{"city":"Malta","country":"Malta"}}'
ENCODED=$(echo $PAYLOAD | base64)
docker run --rm <image> "$ENCODED"
text and/or html fields{"error": "reason"} on failureAll tools follow the formalized contract defined above:
{
"text": "Human-readable plain text result (optional)",
"html": "<div style=\"...\">Inline HTML fragment (optional)</div>",
"title": "Card title override (optional)"
}
{
"error": "Human-readable error message"
}
The framework then:
synthesize: true: Rewrites text in Chalie’s voice and includes it in chatsynthesize: false: Hides text and shows only the card (if card.enabled: true)card.enabled: true: Renders html as a UI card with metadata from card configComplete example demonstrating the formalized contract.
Directory structure:
backend/tools/tool_example/
├── manifest.json
├── Dockerfile
└── runner.sh
manifest.json:
{
"name": "tool_example",
"description": "Example tool demonstrating the formalized tool contract",
"version": "1.0",
"category": "utility",
"icon": "fa-star",
"trigger": { "type": "on_demand" },
"parameters": {
"name": {
"type": "string",
"required": true,
"description": "Name to greet"
}
},
"returns": {
"text": { "type": "string" },
"html": { "type": "string" }
},
"output": {
"synthesize": false,
"card": {
"enabled": true,
"title": "Hello",
"accent_color": "#6c63ff",
"background_color": "rgba(108, 99, 255, 0.10)"
}
},
"constraints": { "timeout_seconds": 10 },
"sandbox": { "network": "none", "memory": "64m" }
}
Dockerfile:
FROM alpine:3.19
RUN apk add --no-cache bash jq
WORKDIR /tool
COPY runner.sh .
RUN chmod +x runner.sh
ENTRYPOINT ["bash", "runner.sh"]
#!/usr/bin/env bash
set -euo pipefail
# Decode base64 payload from command arg
PAYLOAD=$(echo "$1" | base64 -d)
# Extract fields using jq
NAME=$(echo "$PAYLOAD" | jq -r '.params.name // "World"')
CITY=$(echo "$PAYLOAD" | jq -r '.telemetry.city // ""')
TIME=$(echo "$PAYLOAD" | jq -r '.telemetry.time // ""')
# Compose text output
TEXT="Hello, $NAME!"
[ -n "$CITY" ] && TEXT="$TEXT You're in $CITY."
# Compose HTML card — inline CSS only, no scripts, fragment only
HTML=$(jq -rn \
--arg name "$NAME" \
--arg city "$CITY" \
--arg time "$TIME" \
'"<div style=\"padding:16px;font-family:sans-serif\">
<div style=\"font-size:1.4rem;font-weight:600;margin-bottom:8px\">Hello, \($name)!</div>
" + (if $city != "" then "<div style=\"color:#666;margin-bottom:4px\">📍 \($city)</div>" else "" end) + "
" + (if $time != "" then "<div style=\"color:#999;font-size:0.85rem\">\($time)</div>" else "" end) + "
</div>"'
)
# Output formalized contract JSON to stdout
jq -n \
--arg text "$TEXT" \
--arg html "$HTML" \
'{"text": $text, "html": $html}'
Test:
PAYLOAD='{"params":{"name":"Dylan"},"settings":{},"telemetry":{"city":"Valletta","country":"Malta","time":"2026-02-20T17:00:00Z","lat":35.8762,"lon":14.5366}}'
ENCODED=$(echo "$PAYLOAD" | base64)
docker run --rm chalie-tool-tool_example:1.0 "$ENCODED"
# Output: {"text": "Hello, Dylan! You're in Valletta.", "html": "<div style=\"...\">...</div>"}
docker psbackend/tools/tool_name/python -m json.tool manifest.jsonls backend/tools/tool_name/Dockerfiledocker-compose logs -f backend | grep TOOLTool name in manifest must match directory name exactly (case-sensitive).
curl http://localhost:8080/tools/my_tool/configcurl -X POST http://localhost:8080/tools/my_tool/test"required": true)"constraints": {"timeout_seconds": 30}Container exceeded timeout. Options:
timeout_seconds in manifestIn manifest.json:
{
"sandbox": {
"memory": "1g",
"network": "none",
"writable": true
}
}
memory: Docker memory limit (e.g., 256m, 1g)network: bridge (default), none (isolated), host (access host network)writable: false (read-only) or true (can write to /tmp)For scheduled tools (e.g., reminder checks, daily digests):
manifest.json:
{
"trigger": {
"type": "cron",
"schedule": "*/30",
"prompt": "Check for any overdue reminders and summarize them:"
}
}
schedule: Simple cron expression. */30 = every 30 minutesprompt: Template string. Tool output is appended before enqueueing on prompt-queuePlaceholder for future external event triggers (e.g., email received, calendar event).
tools_enabled: false in config to disable all tools