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 the cap Lua module

  • Internal 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

files

(always visible)

file_read, file_write, file_delete, file_list, file_move, file_copy, file_stat

skill_mgr

(always visible)

skill_list, skill_activate, skill_save, skill_delete, skill_deactivate

lua

(always visible)

lua_run, lua_run_async, lua_job_get, lua_job_list, lua_job_stop

scheduler

(always visible)

scheduler_add_job, scheduler_list_jobs, scheduler_get_job, scheduler_remove_job, scheduler_enable_job, scheduler_disable_job, scheduler_pause_job, scheduler_resume_job, scheduler_trigger_now

system

(always visible)

get_heap_info, get_task_list, get_info, get_wifi, get_ip, restart

time

(always visible)

get_current_time, sync_time

http_request

(always visible)

http_request

net_discover

(always visible)

net_discover_start, net_discover_stop, net_discover_peer

vision

(always visible)

vision_describe

web_search

(always visible)

web_search

memory

(always visible)

memory_store, memory_recall, memory_update, memory_forget, memory_list

im_local / im_telegram / etc.

(always visible)

local_send_text, telegram_send_text, feishu_send_message, wechat_send_text, qq_send_message, im_send_media

board

board_hardware_info (must be activated first)

board_list_devices, board_get_device, board_query_peripheral, board_schema, board_reload

audio_stream

camera_capture (must be activated first)

audio_stream_start, audio_stream_tx_start, audio_stream_rx_start, audio_stream_stop, audio_stream_pause, audio_stream_status

Capability Quick Reference

File Management

Capability

Description

file_read

Read file contents from vfs:/ or rolfs:/

file_write

Write or append file contents

file_delete

Delete a file (stops any Lua job using that path first)

file_list

List files and subdirectories at a given path

file_move

Move or rename a file

file_copy

Copy a file to a new path

file_stat

Get file metadata (size, type, modification time)

Skill Management

Capability

Description

skill_list

List all available Skills (built-in + user)

skill_activate

Activate a Skill for the current session; injects its SKILL.md context and unlocks the corresponding cap_groups

skill_save

Save a new user Skill (SKILL.md + optional scripts/main.lua)

skill_delete

Delete a user-created Skill directory

skill_deactivate

Deactivate a Skill from the session’s activation list

Lua Execution

Capability

Description

lua_run

Execute a Lua script synchronously; returns the run() return value and print() output

lua_run_async

Start a Lua script as a background job; returns job_id immediately

lua_job_get

Get a background job’s status and incremental log output (via the since_seq parameter)

lua_job_list

List all background Lua jobs and their status

lua_job_stop

Cooperatively stop a running background job

Scheduler

Capability

Description

scheduler_add_job

Add or update a scheduled job (upsert by id); trigger kind supports once / interval / cron / on_event, action action supports agent / cap / emit

scheduler_list_jobs

List all registered scheduled jobs

scheduler_get_job

Get full details of a single scheduled job by ID

scheduler_remove_job

Remove a scheduled job by ID

scheduler_enable_job

Enable a previously disabled job

scheduler_disable_job

Disable a job without removing it (keeps the definition, stops triggering)

scheduler_pause_job

Temporarily pause a job without disabling it (e.g. “skip today’s alarm”), resume with scheduler_resume_job

scheduler_resume_job

Resume a paused job

scheduler_trigger_now

Immediately execute a job’s action once (for testing), without changing its schedule

Board Hardware (requires activating board_hardware_info first)

Capability

Description

board_list_devices

List all registered hardware devices on the board (sensors, displays, etc.)

board_get_device

Get full device information: chip, interface pin assignment, driver parameters

board_query_peripheral

Query whether a peripheral type is available and which pins/instances are free

board_schema

Get the board-level hardware description schema (chip definitions, constraints)

board_reload

Reload the board.json configuration from the VFS

System Information

Capability

Description

get_heap_info

Get free heap memory and its historical minimum

get_task_list

List the status, priority, and stack watermark of all FreeRTOS tasks

get_info

Get chip model, firmware version, and uptime

get_wifi

Get Wi-Fi SSID, channel, RSSI, and IP address

get_ip

Get the current IP address

restart

Soft-reset the device after a short delay

Time

Capability

Description

get_current_time

Get the current wall-clock time (UTC+8) and Unix timestamp

sync_time

Trigger SNTP time sync and write the result to the system clock and RTC

Network

Capability

Description

http_request

Make an HTTP/HTTPS request (GET/POST/PUT/PATCH/DELETE/HEAD); returns status code and response body

net_discover_start

Start persistent UDP broadcast/listen peer discovery; fires a callback when peers appear/disappear

net_discover_stop

Stop the peer discovery background service

net_discover_peer

One-shot peer discovery (blocks until found or timeout)

Audio Streaming (requires activating camera_capture first)

Capability

Description

audio_stream_start

Start both RX (UDP→speaker) and TX (DMIC→UDP) streams at once

audio_stream_tx_start

Start the DMIC-to-UDP send stream (push-to-talk switch controlled by the C layer)

audio_stream_rx_start

Start the UDP-to-speaker receive stream; should be called before TX

audio_stream_stop

Stop both TX and RX stream tasks

audio_stream_pause

Pause the audio stream

audio_stream_status

Get TX/RX running status and UDP packet counters

Vision

Capability

Description

vision_describe

Generate a visual description of a camera image

Web Search

Capability

Description

web_search

Perform a web search and return summarized results

Long-Term Memory

Capability

Description

memory_store

Store a long-term memory entry (content is required; source and tags are optional, tags comma-separated)

memory_recall

Search long-term memory by keyword (keyword, max_results optional)

memory_update

Update the content of a long-term memory entry by id (id, content required)

memory_forget

Delete a long-term memory entry by id

memory_list

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

local_send_text

Send a text message to the local WebUI

telegram_send_text

Send a text message via Telegram Bot

feishu_send_message

Send a message via Feishu

wechat_send_text

Send a text message via WeChat

qq_send_message

Send a message via QQ

im_send_media

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 a cap command, 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 .c file; there is no central table listing all caps.

Capability Descriptor

claw_cap_descriptor_t describes a single capability. Its core fields:

Field

Description

id / name

Identifier and display name (used when calling)

family

Logical grouping tag (included in list output)

description / input_schema_json

Human- and model-facing description, plus the JSON Schema parameter definition

kind

CLAW_CAP_KIND_INVOKE (invocation-oriented), CLAW_CAP_KIND_EMITTER (produces events), CLAW_CAP_KIND_BOTH

cap_flags

Flag bits such as CLAW_CAP_FLAG_LLM_ACCESS (allows LLM invocation), CLAW_CAP_FLAG_EVENTS_OUT, etc.

init / start / stop

Optional lifecycle hooks

execute

When actually executed, receives JSON string input and the call context claw_cap_call_context_t (containing session_id / channel / chat_id / caller); mallocs the result and writes it into *output (the caller is responsible for free)

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):

  1. Agent: the LLM issues a tool call → bridged via call_cap → claw_cap_call, with caller = LLM.

  2. AT console: AT+CLAW=cap,<id>[,<json>], with caller = MANUAL.

  3. Event router: the rule action rtk_cap, with caller = 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

on_init

Register capability groups; before claw_cap_start_all()

AGENT

on_agent

Append context providers / completion observers / set visibility; between claw_agent_init and claw_agent_start

IO

on_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 CONFIG_CLAW_CAP_*

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 contains group_id, tools (number of tools), runtime_enabled, llm_visible, and is_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. The claw_cap_set_output(output, fmt, ...) helper automatically allocates a large enough buffer.

  • Return RTK_SUCCESS on success; on allocation failure, leave *output as NULL and return RTK_ERR_NOMEM.

  • Any temporary memory allocated inside execute must 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).