Capability System
This chapter covers the capability system of Ameba-Claw — that is, how the individual tools an Agent can call are organized and integrated. Start with “Capability Overview” to see how capabilities are exposed to the LLM by group and how visibility is controlled; then read “Capability Runtime and Self-Registration” to understand the C-layer descriptor model and the staged self-registration mechanism; then read “Capability Management and Visibility Control” to see how compile-time pruning, runtime enable/disable, and LLM visibility form a three-layer control mechanism tied to the Web dashboard; finally, “Implementing a Capability” walks through the full flow — from descriptor definition, to the execute callback, to self-registration — so you can write your own capability by following along.
Capability Overview
What Is a Capability
A Capability is a callable tool registered in the C layer; the LLM invokes it directly as a function call through the tool-use API. Each Capability has a unique string id, a JSON input schema, and an execute function. Besides LLM invocation, it can also be triggered via:
AT command:
AT+CLAW=cap,<id>[,<json_args>]Lua script:
cap.call("id", json_string)via thecapLua moduleInternal C code: the
claw_cap_call()C API
Capability Groups and Gating
All Capabilities are organized into their corresponding cap_group. Most groups are always visible to the LLM; some groups are hidden by default and only unlock once the corresponding Skill has been activated. This mechanism ensures the LLM has acquired the necessary context before calling hardware-related or resource-intensive tools.
Group |
Gate Skill |
Capabilities Included |
|---|---|---|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
(always visible) |
|
|
board_hardware_info (must be activated first) |
|
|
camera_capture (must be activated first) |
|
Capability Quick Reference
File Management
Capability |
Description |
|---|---|
|
Read file contents from |
|
Write or append file contents |
|
Delete a file (stops any Lua job using that path first) |
|
List files and subdirectories at a given path |
|
Move or rename a file |
|
Copy a file to a new path |
|
Get file metadata (size, type, modification time) |
Skill Management
Capability |
Description |
|---|---|
|
List all available Skills (built-in + user) |
|
Activate a Skill for the current session; injects its SKILL.md context and unlocks the corresponding cap_groups |
|
Save a new user Skill (SKILL.md + optional scripts/main.lua) |
|
Delete a user-created Skill directory |
|
Deactivate a Skill from the session’s activation list |
Lua Execution
Capability |
Description |
|---|---|
|
Execute a Lua script synchronously; returns the |
|
Start a Lua script as a background job; returns |
|
Get a background job’s status and incremental log output (via the since_seq parameter) |
|
List all background Lua jobs and their status |
|
Cooperatively stop a running background job |
Scheduler
Capability |
Description |
|---|---|
|
Add or update a scheduled job (upsert by id); trigger |
|
List all registered scheduled jobs |
|
Get full details of a single scheduled job by ID |
|
Remove a scheduled job by ID |
|
Enable a previously disabled job |
|
Disable a job without removing it (keeps the definition, stops triggering) |
|
Temporarily pause a job without disabling it (e.g. “skip today’s alarm”), resume with |
|
Resume a paused job |
|
Immediately execute a job’s action once (for testing), without changing its schedule |
Board Hardware (requires activating board_hardware_info first)
Capability |
Description |
|---|---|
|
List all registered hardware devices on the board (sensors, displays, etc.) |
|
Get full device information: chip, interface pin assignment, driver parameters |
|
Query whether a peripheral type is available and which pins/instances are free |
|
Get the board-level hardware description schema (chip definitions, constraints) |
|
Reload the board.json configuration from the VFS |
System Information
Capability |
Description |
|---|---|
|
Get free heap memory and its historical minimum |
|
List the status, priority, and stack watermark of all FreeRTOS tasks |
|
Get chip model, firmware version, and uptime |
|
Get Wi-Fi SSID, channel, RSSI, and IP address |
|
Get the current IP address |
|
Soft-reset the device after a short delay |
Time
Capability |
Description |
|---|---|
|
Get the current wall-clock time (UTC+8) and Unix timestamp |
|
Trigger SNTP time sync and write the result to the system clock and RTC |
Network
Capability |
Description |
|---|---|
|
Make an HTTP/HTTPS request (GET/POST/PUT/PATCH/DELETE/HEAD); returns status code and response body |
|
Start persistent UDP broadcast/listen peer discovery; fires a callback when peers appear/disappear |
|
Stop the peer discovery background service |
|
One-shot peer discovery (blocks until found or timeout) |
Audio Streaming (requires activating camera_capture first)
Capability |
Description |
|---|---|
|
Start both RX (UDP→speaker) and TX (DMIC→UDP) streams at once |
|
Start the DMIC-to-UDP send stream (push-to-talk switch controlled by the C layer) |
|
Start the UDP-to-speaker receive stream; should be called before TX |
|
Stop both TX and RX stream tasks |
|
Pause the audio stream |
|
Get TX/RX running status and UDP packet counters |
Vision
Capability |
Description |
|---|---|
|
Generate a visual description of a camera image |
Web Search
Capability |
Description |
|---|---|
|
Perform a web search and return summarized results |
Long-Term Memory
Capability |
Description |
|---|---|
|
Store a long-term memory entry (content is required; source and tags are optional, tags comma-separated) |
|
Search long-term memory by keyword (keyword, max_results optional) |
|
Update the content of a long-term memory entry by id (id, content required) |
|
Delete a long-term memory entry by id |
|
List all long-term memory entries (max_results optional, default 20) |
The internal mechanisms behind these five tools (structured memory, summary/tag-based retrieval, automatic extraction) are covered in Memory Internals.
Instant Messaging
Capability |
Description |
|---|---|
|
Send a text message to the local WebUI |
|
Send a text message via Telegram Bot |
|
Send a message via Feishu |
|
Send a text message via WeChat |
|
Send a message via QQ |
|
Send a media file to an IM platform |
AT Command Interface
List all registered Capabilities:
AT+CLAW=cap
List all installed Skills (calls the skill_list Capability):
AT+CLAW=cap,skill_list
Call a Capability with JSON arguments:
AT+CLAW=cap,<capability_id>,<json_args>
Examples:
AT+CLAW=cap,get_info
AT+CLAW=cap,file_list,{"path":"vfs:/skills/"}
AT+CLAW=cap,lua_run,{"path":"vfs:/tmp/test.lua","args":{}}
AT+CLAW=cap,skill_activate,{"name":"board_hardware_info"}
Specify a session ID (to control per-session cap_groups visibility):
AT+CLAW=cap,<capability_id>,<json_args>,sid,<session_id>
Calling Capabilities from Lua Scripts
Use the cap Lua module inside Skill scripts. cap.call always returns two values:
local cap = require("cap")
local cjson = require("cjson")
-- Correct: capture both ok (boolean) and result_json (string)
local ok, result_json = cap.call("get_current_time", "{}")
if not ok then
return '{"error":' .. (result_json or '"cap call failed"') .. '}'
end
local t = cjson.decode(result_json)
-- use fields such as t.timestamp, t.datetime
-- Wrong: capturing only one return value gets the boolean ok, discarding the JSON payload
-- local result = cap.call("get_current_time", "{}") -- do not do this
Capability Runtime and Self-Registration
This document is for developers and covers the C-layer runtime model and self-registration mechanism for Capabilities. For “how to call a Capability”, see Capability Overview; for “how to write a new Capability from scratch”, see Implementing a Capability.
Two Different Kinds of “Registration”
Ameba-Claw has two similarly-named modules with different responsibilities — be sure to distinguish them:
claw_cap— the tool runtime. Every “invocable piece of capability” (used by the LLM as a tool, by the AT console as acapcommand, or called directly by the event router) is first registered as a descriptor with metadata, then executed through a unified entry point.claw_cap_registry— the boot lifecycle registry. It manages the three-stage hooks each capability plugin runs during boot assembly. Each cap self-registers in its own.cfile; there is no central table listing all caps.
Capability Descriptor
claw_cap_descriptor_t describes a single capability. Its core fields:
Field |
Description |
|---|---|
|
Identifier and display name (used when calling) |
|
Logical grouping tag (included in list output) |
|
Human- and model-facing description, plus the JSON Schema parameter definition |
|
|
|
Flag bits such as |
|
Optional lifecycle hooks |
|
When actually executed, receives JSON string input and the call context |
A batch of related descriptors is placed into a claw_cap_group_t and registered as one group via claw_cap_register_group(), making it easy to enable/disable and manage visibility per group.
Call Paths
The same claw_cap_call entry point serves three kinds of callers (distinguished by claw_cap_call_context_t.caller):
Agent: the LLM issues a tool call → bridged via
call_cap→claw_cap_call, withcaller = LLM.AT console:
AT+CLAW=cap,<id>[,<json>], withcaller = MANUAL.Event router: the rule action
rtk_cap, withcaller = INTERNAL(see Event Routing and Automation).
LLM Tool Visibility
claw_cap_build_llm_tools_json() generates the tool list based on the current context, which the tools provider hands to the Agent. Registered does not mean visible to the LLM:
All registered groups can be called directly by the AT console and the event router;
but whether a group’s tools are “natively” seen by the LLM is controlled by a visibility whitelist. This avoids stuffing all tools into the context at once, which would bloat the prompt and degrade model performance.
Each session also has its own independent visibility scope (
claw_cap_set_session_llm_visible_groups): activating a Skill adds its bound groups to the current session’s whitelist, implementing “disclosure on demand”.
Operational APIs: claw_cap_list / claw_cap_list_groups enumerate capabilities and groups; claw_cap_enable_group / claw_cap_disable_group enable/disable by group; claw_cap_unregister_group unloads a group (waiting with a timeout for active calls to finish).
Self-Registration Mechanism: CLAW_CAP_REGISTER
Each cap declares and self-registers a lifecycle descriptor with a single macro line in its own .c file, with no need to register in any central file:
CLAW_CAP_REGISTER(time, {
.group = "time",
.order = 10,
.on_init = time_on_init,
.on_agent = time_on_agent,
.on_io = time_on_io,
});
The macro expands into a static const descriptor (placed in rodata) plus a C constructor function; the constructor runs before the RTOS/heap/serial port are ready, so it can only register — it must not call malloc, touch the RTOS, or read configuration. Configuration is passed in through the const claw_config_t * parameter of each stage’s hook.
The Three Lifecycle Stages
ameba_claw_main() drives the hooks of all registered caps stage by stage (see Boot and Runtime Assembly):
Stage |
Hook |
Timing and Purpose |
|---|---|---|
INIT |
|
Register capability groups; before |
AGENT |
|
Append context providers / completion observers / set visibility; between |
IO |
|
Channels / background services / Wi-Fi callbacks / HTTP routes; after event routing and http_init, before http_server_start |
Execution Order Is Determined by the order Field
Within the same stage, hooks execute in ascending, stable order by order — never relying on link order or registration order. The values assigned to order are centrally documented in a comment table in claw_cap_registry.h, serving as the single point of coordination; do not scatter conflicting values across individual cap files.
Only relationships that are order-sensitive need to hold, for example: the relative order of providers during the AGENT stage (cap_time 10 < cap_skill_mgr 20 < cap_board_mgr 30) determines the assembly order of the system prompt; cap_router_mgr (200) comes last, so its group is registered after the AGENT-stage visibility snapshot, making it hidden from the LLM by default. Other caps that only register groups, where order doesn’t matter, can share a common value.
The CLAW_CAP_FLAG_CORE flag marks a cap as always enabled, ignoring the runtime enable/disable list (e.g. cap_lua, cap_webui).
Capability Management and Visibility Control
This document describes the three-layer Capability control mechanism in Ameba-Claw — from compile-time pruning to runtime enable/disable, and then to LLM tool visibility — as well as the persistence and runtime semantics behind the Web dashboard “CAP Management” panel.
Three-Layer Control Model
Ameba-Claw manages the activation state and callable scope of each Capability group at three levels:
Layer |
Control Mechanism |
When It Takes Effect |
Description |
|---|---|---|---|
Compile-Time Pruning |
Kconfig |
After flashing firmware |
When disabled, the module is not compiled into the firmware, takes no code space, and does not exist at runtime |
Runtime Enable/Disable |
Web dashboard “CAP Management” / C API |
After next reboot |
Persisted to claw_config; affects all call paths including LLM, AT commands, and Lua scripts |
LLM Visibility |
Web dashboard “CAP Management” / C API |
Takes effect immediately |
Persisted to claw_config; only affects the LLM tool list, does not affect AT/Lua calls |
The three layers have an “AND” relationship: Kconfig determines whether the module exists, runtime enable determines whether the module can be called, and LLM visibility determines whether the LLM can actively discover and use the module’s tools.
Compile-Time Pruning (Kconfig)
The project Kconfig provides a CONFIG_CLAW_CAP_* option for each capability module (e.g., CONFIG_CLAW_CAP_MCP_CLIENT, CONFIG_CLAW_CAP_MCP_SERVER). When an option is disabled, the corresponding module’s source files are excluded from compilation, reducing firmware size, and no registration records for that cap group will appear at runtime. The Web management interface can only view and operate modules that have been compiled into the firmware.
Runtime Enable/Disable
Cap groups that have been compiled into the firmware can be enabled or disabled per group from the Web dashboard or the C API layer. The disabled state is persisted via claw_config_set_disabled_cap_groups(). On the next boot, ameba_claw_main() reads this list and calls claw_cap_disable_group() on the corresponding groups to put them into the disabled state. Tools belonging to a disabled group remain registered in the system, but calls to them will return an error directly.
To operate directly in C code (not persisted, takes effect only for the current runtime):
claw_cap_disable_group("my_group");
claw_cap_enable_group("my_group");
LLM Visibility
For enabled cap groups, whether their tools are exposed to the LLM (appearing in the tool list within the tool-use context) is controlled separately by a visibility switch. When turned off, the group’s tools are not visible to the LLM, but they can still be called via AT commands (AT+CLAW=cap,<id>) and Lua scripts (cap.call()).
A visibility change synchronously calls cap_skill_mgr_apply_base_visibility(), which immediately refreshes the base tool whitelist for all sessions without requiring a reboot. The setting is persisted via claw_config_set_cap_hidden_groups() and remains effective after a reboot.
Note
The Web dashboard manages “base visibility” — the default state when no Skill is activated. Activating a Skill additionally unlocks the corresponding cap groups on top of the current session (session-level visibility), which is not constrained by the base visibility switch. For the relationship between Skills and visibility, see Capability Overview.
Core Capability Groups (CLAW_CAP_FLAG_CORE)
Cap groups registered with the CLAW_CAP_FLAG_CORE flag are not subject to runtime enable/disable or the LLM visibility blacklist; they are always enabled and visible to the LLM. In the Web dashboard, these groups are displayed as “locked” and no toggle is provided.
Groups currently marked as core include cap_lua (Lua script execution capability) and cap_webui (the cap group used by the Web dashboard itself). If a group is indispensable for normal device operation, CLAW_CAP_FLAG_CORE should be added at registration time to prevent it from being accidentally disabled.
Web Dashboard HTTP Interface
The “CAP Management” panel interacts with the firmware through the following three HTTP interfaces, which can also be called directly by third-party tools or scripts:
GET /api/cap/groups— Returns a status snapshot of all registered cap groups. Each record containsgroup_id,tools(number of tools),runtime_enabled,llm_visible, andis_core.POST /api/cap/groups/runtime— Toggles the runtime enable state, persisted, takes effect after reboot. Request body:{"group_id": "my_group", "enabled": true}.POST /api/cap/groups/visibility— Toggles LLM visibility, persisted and takes effect immediately. Request body:{"group_id": "my_group", "visible": true}.
MCP Dynamic Cap Groups
After the MCP client connects to an external MCP Server, it dynamically registers the remote tools as a local cap group using mcp_<server_name> as the group_id. These groups appear in the “CAP Management” panel just like built-in groups and are subject to the same three-layer control constraints. For the complete mechanism of MCP integration, see MCP Integration.
Implementing a Capability
This document demonstrates how to write a new C-layer Capability for Ameba-Claw, covering the full flow from descriptor definition through the execute callback to self-registration. For background on the runtime model, see Capability Runtime and Self-Registration.
Note
This document covers C capabilities compiled into the firmware. If you only want to give the Agent new functionality at runtime without recompiling, use a Lua Skill instead (see Skill System) — no C code changes required.
Directory Structure
Each capability is a component directory under claw_capabilities/:
claw_capabilities/cap_my_feature/
├── CMakeLists.txt
├── include/
│ └── cap_my_feature.h (optional, only needed if exposing an interface across modules)
└── src/
└── cap_my_feature.c Descriptor + execute callback + self-registration
Implementing the Execute Callback
execute is the core of the capability: it receives a JSON string as input and mallocs the result, writing it into *output (the caller is responsible for free). The signature is fixed:
#include "claw_cap.h"
#include <cJSON.h>
static int my_feature_execute(const char *input_json,
const claw_cap_call_context_t *ctx,
char **output)
{
cJSON *root = cJSON_Parse(input_json ? input_json : "{}");
if (!root) {
return claw_cap_set_output(output, "{\"error\":\"invalid JSON\"}");
}
/* Read the call context from ctx when needed: session_id / channel / chat_id / caller */
cJSON *param = cJSON_GetObjectItem(root, "param");
if (!cJSON_IsString(param)) {
cJSON_Delete(root);
return claw_cap_set_output(output, "{\"error\":\"param is required\"}");
}
int rc = claw_cap_set_output(output, "{\"ok\":true,\"echo\":\"%s\"}",
param->valuestring);
cJSON_Delete(root);
return rc;
}
Key conventions:
Write the result into
*output— do not print directly to the serial port. Theclaw_cap_set_output(output, fmt, ...)helper automatically allocates a large enough buffer.Return
RTK_SUCCESSon success; on allocation failure, leave*outputasNULLand returnRTK_ERR_NOMEM.Any temporary memory allocated inside
executemust be freed before returning.If the output could be large, consider returning a file path for the LLM to query further, rather than stuffing in a large block of text.
Defining the Descriptor and Group
static const claw_cap_descriptor_t s_caps[] = {
{
.id = "my_action",
.name = "my_action",
.family = "my_feature",
.description = "Perform my custom action with the given param.",
.kind = CLAW_CAP_KIND_INVOKE,
.cap_flags = CLAW_CAP_FLAG_LLM_ACCESS, /* allow LLM invocation */
.input_schema_json =
"{\"type\":\"object\","
"\"properties\":{\"param\":{\"type\":\"string\"}},"
"\"required\":[\"param\"]}",
.execute = my_feature_execute,
},
};
static const claw_cap_group_t s_group = {
.group_id = "my_feature",
.plugin_name = "cap_my_feature",
.version = "1",
.descriptors = s_caps,
.descriptor_count = sizeof(s_caps) / sizeof(s_caps[0]),
};
description and input_schema_json directly determine how the LLM understands and calls this tool, so make sure they are written clearly.
Self-Registration
Declare the lifecycle hooks with CLAW_CAP_REGISTER and self-register, with no need to register in any central file. A cap only needs to implement the stages it requires:
#include "claw_cap_registry.h"
/* INIT stage: register the tool group */
static void my_feature_on_init(const claw_config_t *cfg)
{
(void)cfg;
claw_cap_register_group(&s_group);
}
CLAW_CAP_REGISTER(my_feature, {
.group = "my_feature",
.order = 90, /* see the order allocation table in claw_cap_registry.h */
.on_init = my_feature_on_init,
});
Warning
The constructor function expanded by CLAW_CAP_REGISTER runs before the RTOS / heap / serial port are ready, so the constructor itself can only register the descriptor — it must not call malloc, touch the RTOS, or read configuration. Real initialization belongs in on_init (which receives a const claw_config_t *).
The meaning of the three stage hooks (on_init / on_agent / on_io) and the rules for choosing order values are covered in Capability Runtime and Self-Registration. If your cap also needs to inject into the system prompt or hook a completion observer, implement on_agent; if it needs to run a background task, register a Wi-Fi callback, or register an HTTP route, implement on_io.
CMakeLists.txt
ameba_add_library(cap_my_feature SRCS src/cap_my_feature.c)
# Depends at minimum on the claw_cap runtime and cJSON; self-registration also needs claw_cap_registry.
# For the exact form, refer to other cap_*/CMakeLists.txt in the same directory (this project uses the SDK's unified build macros).
Finally, add the component directory to the declaration list in claw_capabilities/CMakeLists.txt (following the pattern of existing caps). Since registration is automatic, there is no need to manually call the registration function in ameba_claw_main.c — once compiled into the firmware, the constructor registers it automatically, and the assembly stage drives its hooks according to order.
Capabilities That Produce Events
If a capability needs to proactively produce events (e.g. receiving an IM message, a peripheral interrupt), set kind to CLAW_CAP_KIND_EMITTER (or BOTH), and call the event publishing interface from a background task:
#include "claw_event_publisher.h"
/* Produce a text message event */
claw_event_dispatcher_publish_message(
"my_gateway", /* source_cap */
"my_channel", /* channel */
chat_id,
text,
sender_id,
message_id);
/* Produce a custom trigger event */
claw_event_dispatcher_publish_trigger(
"my_gateway", /* source_cap */
"my_custom_event", /* event_type */
event_key,
payload_json);
Once an event is produced, the event router decides — based on its rules — whether to trigger automation or hand it to the Agent for reasoning. See Event Routing and Automation for details.
Reference Implementations
The existing caps in the project are the best templates: cap_time (uses all three stages, including a context provider and Wi-Fi callback), cap_system (pure query tool), cap_files / cap_lua (filesystem + path validation), cap_honesty (only hooks a completion observer, registers no tool group).