Files
zhouli 639982debd feat(cap_lua): add configurable Lua package.path support
- Add `cap_lua_add_package_path_dir()` API to register shared Lua
  library directories at runtime
- Automatically prepend the running script's own directory and all
  registered shared dirs to `package.path` before execution
- Script-local directory takes priority over shared library dirs
- Validate paths: must be absolute, no ".." traversal, within size limit
- Deduplicate entries in the path dir linked list
2026-04-28 15:01:26 +08:00

713 lines
23 KiB
C

/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "app_capabilities.h"
#include "app_lua_modules.h"
#include <ctype.h>
#include <stdbool.h>
#include <string.h>
#if CONFIG_APP_CLAW_CAP_FILES
#include "cap_files.h"
#endif
#if CONFIG_APP_CLAW_CAP_IM_FEISHU
#include "cap_im_feishu.h"
#endif
#if CONFIG_APP_CLAW_CAP_IM_QQ
#include "cap_im_qq.h"
#endif
#if CONFIG_APP_CLAW_CAP_IM_TG
#include "cap_im_tg.h"
#endif
#if CONFIG_APP_CLAW_CAP_IM_WECHAT
#include "cap_im_wechat.h"
#endif
#if CONFIG_APP_CLAW_CAP_LLM_INSPECT
#include "cap_llm_inspect.h"
#endif
#if CONFIG_APP_CLAW_CAP_LUA
#include "cap_lua.h"
#endif
#if CONFIG_APP_CLAW_CAP_MCP_CLIENT
#include "cap_mcp_client.h"
#endif
#if CONFIG_APP_CLAW_CAP_MCP_SERVER
#include "cap_mcp_server.h"
#endif
#if CONFIG_APP_CLAW_CAP_ROUTER_MGR
#include "cap_router_mgr.h"
#endif
#if CONFIG_APP_CLAW_CAP_SCHEDULER
#include "cap_scheduler.h"
#endif
#if CONFIG_APP_CLAW_CAP_SESSION_MGR
#include "cap_session_mgr.h"
#endif
#if CONFIG_APP_CLAW_CAP_SKILL_MGR
#include "cap_skill_mgr.h"
#endif
#if CONFIG_APP_CLAW_CAP_SYSTEM
#include "cap_system.h"
#endif
#if CONFIG_APP_CLAW_CAP_TIME
#include "cap_time.h"
#endif
#if CONFIG_APP_CLAW_CAP_WEB_SEARCH
#include "cap_web_search.h"
#endif
#include "claw_cap.h"
#include "claw_memory.h"
#include "esp_check.h"
#include "esp_log.h"
static const char *TAG = "app_capabilities";
#define APP_IM_ATTACHMENT_MAX_BYTES (2 * 1024 * 1024)
typedef esp_err_t (*app_cap_prepare_fn)(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths);
typedef esp_err_t (*app_cap_register_fn)(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths);
typedef struct {
const char *group_id;
const char *display_name;
const char *label;
bool llm_visible_by_default;
app_cap_prepare_fn prepare;
app_cap_register_fn reg;
} app_capability_group_entry_t;
static bool app_cap_groups_config_empty(const char *value)
{
size_t i;
if (!value) {
return true;
}
for (i = 0; value[i]; i++) {
if (!isspace((unsigned char)value[i])) {
return false;
}
}
return true;
}
static char *app_cap_trim_token(char *token)
{
char *end;
if (!token) {
return NULL;
}
while (*token && isspace((unsigned char)*token)) {
token++;
}
end = token + strlen(token);
while (end > token && isspace((unsigned char)end[-1])) {
end--;
}
*end = '\0';
return token;
}
static int app_cap_find_group_entry(const app_capability_group_entry_t *entries,
size_t count,
const char *group_id)
{
size_t i;
if (!entries || !group_id || !group_id[0]) {
return -1;
}
for (i = 0; i < count; i++) {
if (entries[i].group_id && strcmp(entries[i].group_id, group_id) == 0) {
return (int)i;
}
}
return -1;
}
static esp_err_t app_cap_build_group_map(const char *configured_groups,
const app_capability_group_entry_t *entries,
size_t entry_count,
bool *selected_map,
bool select_all_if_empty,
bool use_llm_defaults_if_empty)
{
char *groups_copy = NULL;
char *saveptr = NULL;
char *token = NULL;
size_t i;
if (!entries || !selected_map) {
return ESP_ERR_INVALID_ARG;
}
memset(selected_map, 0, entry_count * sizeof(selected_map[0]));
if (app_cap_groups_config_empty(configured_groups)) {
for (i = 0; i < entry_count; i++) {
selected_map[i] = select_all_if_empty || (use_llm_defaults_if_empty &&
entries[i].llm_visible_by_default);
}
return ESP_OK;
}
groups_copy = malloc(strlen(configured_groups) + 1);
if (!groups_copy) {
return ESP_ERR_NO_MEM;
}
memcpy(groups_copy, configured_groups, strlen(configured_groups) + 1);
for (token = strtok_r(groups_copy, ",", &saveptr);
token;
token = strtok_r(NULL, ",", &saveptr)) {
char *trimmed = app_cap_trim_token(token);
int index;
if (!trimmed || !trimmed[0]) {
continue;
}
if (strcmp(trimmed, "__none__") == 0 || strcmp(trimmed, "none") == 0) {
continue;
}
index = app_cap_find_group_entry(entries, entry_count, trimmed);
if (index < 0) {
ESP_LOGW(TAG, "Ignoring unknown or unavailable capability group: %s", trimmed);
continue;
}
selected_map[index] = true;
}
free(groups_copy);
return ESP_OK;
}
static esp_err_t app_cap_register_entry(const app_capability_group_entry_t *entry,
const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
claw_cap_list_t cap_list;
claw_cap_group_list_t group_list;
esp_err_t err;
if (!entry || !entry->reg) {
return ESP_ERR_INVALID_ARG;
}
err = entry->reg(config, paths);
cap_list = claw_cap_list();
group_list = claw_cap_list_groups();
if (err != ESP_OK) {
ESP_LOGE(TAG, "%s failed: %s (groups=%u, caps=%u)",
entry->label, esp_err_to_name(err),
(unsigned)group_list.count,
(unsigned)cap_list.count);
return err;
}
ESP_LOGI(TAG, "%s ok (groups=%u, caps=%u)",
entry->label,
(unsigned)group_list.count,
(unsigned)cap_list.count);
return ESP_OK;
}
#if CONFIG_APP_CLAW_CAP_FILES
static esp_err_t app_cap_prepare_files(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
return cap_files_set_base_dir(paths->fatfs_base_path);
}
static esp_err_t app_cap_register_files(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_files_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_IM_QQ
static esp_err_t app_cap_prepare_im_qq(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
ESP_RETURN_ON_ERROR(cap_im_qq_set_attachment_config(&(cap_im_qq_attachment_config_t) {
.storage_root_dir = paths->im_attachment_root,
.max_inbound_file_bytes = APP_IM_ATTACHMENT_MAX_BYTES,
.enable_inbound_attachments = true,
}),
TAG, "Failed to set QQ attachment config");
if (config->qq_app_id[0] && config->qq_app_secret[0]) {
ESP_RETURN_ON_ERROR(cap_im_qq_set_credentials(config->qq_app_id, config->qq_app_secret),
TAG, "Failed to set QQ credentials");
}
return ESP_OK;
}
static esp_err_t app_cap_register_im_qq(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_im_qq_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_IM_FEISHU
static esp_err_t app_cap_prepare_im_feishu(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
ESP_RETURN_ON_ERROR(cap_im_feishu_set_attachment_config(&(cap_im_feishu_attachment_config_t) {
.storage_root_dir = paths->im_attachment_root,
.max_inbound_file_bytes = APP_IM_ATTACHMENT_MAX_BYTES,
.enable_inbound_attachments = true,
}),
TAG, "Failed to set Feishu attachment config");
if (config->feishu_app_id[0] && config->feishu_app_secret[0]) {
ESP_RETURN_ON_ERROR(cap_im_feishu_set_credentials(config->feishu_app_id,
config->feishu_app_secret),
TAG, "Failed to set Feishu credentials");
}
return ESP_OK;
}
static esp_err_t app_cap_register_im_feishu(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_im_feishu_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_IM_TG
static esp_err_t app_cap_prepare_im_tg(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
ESP_RETURN_ON_ERROR(cap_im_tg_set_attachment_config(&(cap_im_tg_attachment_config_t) {
.storage_root_dir = paths->im_attachment_root,
.max_inbound_file_bytes = APP_IM_ATTACHMENT_MAX_BYTES,
.enable_inbound_attachments = true,
}),
TAG, "Failed to set Telegram attachment config");
if (config->tg_bot_token[0]) {
ESP_RETURN_ON_ERROR(cap_im_tg_set_token(config->tg_bot_token),
TAG, "Failed to set Telegram bot token");
}
return ESP_OK;
}
static esp_err_t app_cap_register_im_tg(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_im_tg_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_IM_WECHAT
static esp_err_t app_cap_prepare_im_wechat(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
ESP_RETURN_ON_ERROR(cap_im_wechat_set_attachment_config(&(cap_im_wechat_attachment_config_t) {
.storage_root_dir = paths->im_attachment_root,
.max_inbound_file_bytes = APP_IM_ATTACHMENT_MAX_BYTES,
.enable_inbound_attachments = true,
}),
TAG, "Failed to set WeChat attachment config");
if (config->wechat_token[0] && config->wechat_base_url[0]) {
ESP_RETURN_ON_ERROR(cap_im_wechat_set_client_config(&(cap_im_wechat_client_config_t) {
.token = config->wechat_token,
.base_url = config->wechat_base_url,
.cdn_base_url = config->wechat_cdn_base_url,
.account_id = config->wechat_account_id,
}),
TAG, "Failed to set WeChat client config");
}
return ESP_OK;
}
static esp_err_t app_cap_register_im_wechat(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_im_wechat_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_SCHEDULER
static esp_err_t app_cap_register_scheduler(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_scheduler_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_LUA
static esp_err_t app_cap_prepare_lua(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
char path_temp[96];
snprintf(path_temp, sizeof(path_temp), "%s/builtin", paths->lua_root_dir);
cap_lua_add_package_path_dir(path_temp);
return app_lua_modules_register(config, paths->fatfs_base_path);
}
static esp_err_t app_cap_register_lua(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
return cap_lua_register_group(paths->lua_root_dir);
}
#endif
#if CONFIG_APP_CLAW_CAP_MCP_CLIENT
static esp_err_t app_cap_register_mcp_client(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_mcp_client_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_MCP_SERVER
static esp_err_t app_cap_register_mcp_server(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_mcp_server_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_SKILL_MGR
static esp_err_t app_cap_register_skill_mgr(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_skill_mgr_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_SYSTEM
static esp_err_t app_cap_register_system(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_system_register_group();
}
#endif
#if CONFIG_APP_CLAW_MEMORY_MODE_FULL
static esp_err_t app_cap_register_memory(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return claw_memory_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_TIME
static esp_err_t app_cap_register_time(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_time_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_LLM_INSPECT
static esp_err_t app_cap_register_llm_inspect(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_llm_inspect_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_WEB_SEARCH
static esp_err_t app_cap_prepare_web_search(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)paths;
if (config->search_brave_key[0]) {
ESP_RETURN_ON_ERROR(cap_web_search_set_brave_key(config->search_brave_key),
TAG, "Failed to set Brave search key");
}
if (config->search_tavily_key[0]) {
ESP_RETURN_ON_ERROR(cap_web_search_set_tavily_key(config->search_tavily_key),
TAG, "Failed to set Tavily search key");
}
return ESP_OK;
}
static esp_err_t app_cap_register_web_search(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_web_search_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_ROUTER_MGR
static esp_err_t app_cap_register_router_mgr(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_router_mgr_register_group();
}
#endif
#if CONFIG_APP_CLAW_CAP_SESSION_MGR
static esp_err_t app_cap_register_session_mgr(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
(void)config;
(void)paths;
return cap_session_mgr_register_group();
}
#endif
static const app_capability_group_entry_t s_capability_group_entries[] = {
#if CONFIG_APP_CLAW_CAP_IM_QQ
{ "cap_im_qq", "QQ", "Register QQ cap", false, app_cap_prepare_im_qq, app_cap_register_im_qq },
#endif
#if CONFIG_APP_CLAW_CAP_IM_FEISHU
{ "cap_im_feishu", "Feishu", "Register Feishu cap", false, app_cap_prepare_im_feishu, app_cap_register_im_feishu },
#endif
#if CONFIG_APP_CLAW_CAP_IM_TG
{ "cap_im_tg", "Telegram", "Register Telegram cap", false, app_cap_prepare_im_tg, app_cap_register_im_tg },
#endif
#if CONFIG_APP_CLAW_CAP_IM_WECHAT
{ "cap_im_wechat", "WeChat", "Register WeChat cap", false, app_cap_prepare_im_wechat, app_cap_register_im_wechat },
#endif
#if CONFIG_APP_CLAW_CAP_FILES
{ "cap_files", "Files", "Register files cap", true, app_cap_prepare_files, app_cap_register_files },
#endif
#if CONFIG_APP_CLAW_CAP_SCHEDULER
{ "cap_scheduler", "Scheduler", "Register scheduler cap", false, NULL, app_cap_register_scheduler },
#endif
#if CONFIG_APP_CLAW_CAP_LUA
{ "cap_lua", "Lua", "Register Lua cap", true, app_cap_prepare_lua, app_cap_register_lua },
#endif
#if CONFIG_APP_CLAW_CAP_MCP_CLIENT
{ "cap_mcp_client", "MCP Client", "Register MCP client cap", false, NULL, app_cap_register_mcp_client },
#endif
#if CONFIG_APP_CLAW_CAP_MCP_SERVER
{ "cap_mcp_server", "MCP Server", "Register MCP server cap", false, NULL, app_cap_register_mcp_server },
#endif
#if CONFIG_APP_CLAW_CAP_SKILL_MGR
{ "cap_skill", "Skill Manager", "Register skill cap", true, NULL, app_cap_register_skill_mgr },
#endif
#if CONFIG_APP_CLAW_CAP_SYSTEM
{ "cap_system", "System", "Register system cap", true, NULL, app_cap_register_system },
#endif
#if CONFIG_APP_CLAW_MEMORY_MODE_FULL
{ "claw_memory", "Memory", "Register claw_memory group", true, NULL, app_cap_register_memory },
#endif
#if CONFIG_APP_CLAW_CAP_TIME
{ "cap_time", "Time", "Register time cap", false, NULL, app_cap_register_time },
#endif
#if CONFIG_APP_CLAW_CAP_LLM_INSPECT
{ "cap_llm_inspect", "LLM Inspect", "Register LLM inspect cap", false, NULL, app_cap_register_llm_inspect },
#endif
#if CONFIG_APP_CLAW_CAP_WEB_SEARCH
{ "cap_web_search", "Web Search", "Register web search cap", false, app_cap_prepare_web_search, app_cap_register_web_search },
#endif
#if CONFIG_APP_CLAW_CAP_ROUTER_MGR
{ "cap_router_mgr", "Router Manager", "Register router manager cap", false, NULL, app_cap_register_router_mgr },
#endif
#if CONFIG_APP_CLAW_CAP_SESSION_MGR
{ "cap_session_mgr", "Session Manager", "Register session manager cap", false, NULL, app_cap_register_session_mgr },
#endif
};
static const app_capability_group_info_t s_capability_group_infos[] = {
#if CONFIG_APP_CLAW_CAP_IM_QQ
{ "cap_im_qq", "QQ", false },
#endif
#if CONFIG_APP_CLAW_CAP_IM_FEISHU
{ "cap_im_feishu", "Feishu", false },
#endif
#if CONFIG_APP_CLAW_CAP_IM_TG
{ "cap_im_tg", "Telegram", false },
#endif
#if CONFIG_APP_CLAW_CAP_IM_WECHAT
{ "cap_im_wechat", "WeChat", false },
#endif
#if CONFIG_APP_CLAW_CAP_FILES
{ "cap_files", "Files", true },
#endif
#if CONFIG_APP_CLAW_CAP_SCHEDULER
{ "cap_scheduler", "Scheduler", false },
#endif
#if CONFIG_APP_CLAW_CAP_LUA
{ "cap_lua", "Lua", true },
#endif
#if CONFIG_APP_CLAW_CAP_MCP_CLIENT
{ "cap_mcp_client", "MCP Client", false },
#endif
#if CONFIG_APP_CLAW_CAP_MCP_SERVER
{ "cap_mcp_server", "MCP Server", false },
#endif
#if CONFIG_APP_CLAW_CAP_SKILL_MGR
{ "cap_skill", "Skill Manager", true },
#endif
#if CONFIG_APP_CLAW_CAP_SYSTEM
{ "cap_system", "System", true },
#endif
#if CONFIG_APP_CLAW_MEMORY_MODE_FULL
{ "claw_memory", "Memory", true },
#endif
#if CONFIG_APP_CLAW_CAP_TIME
{ "cap_time", "Time", false },
#endif
#if CONFIG_APP_CLAW_CAP_LLM_INSPECT
{ "cap_llm_inspect", "LLM Inspect", false },
#endif
#if CONFIG_APP_CLAW_CAP_WEB_SEARCH
{ "cap_web_search", "Web Search", false },
#endif
#if CONFIG_APP_CLAW_CAP_ROUTER_MGR
{ "cap_router_mgr", "Router Manager", false },
#endif
#if CONFIG_APP_CLAW_CAP_SESSION_MGR
{ "cap_session_mgr", "Session Manager", false },
#endif
};
esp_err_t app_capabilities_init(const app_claw_config_t *config,
const app_claw_storage_paths_t *paths)
{
const size_t entry_count = sizeof(s_capability_group_entries) / sizeof(s_capability_group_entries[0]);
bool *enabled_map = NULL;
bool *llm_visible_map = NULL;
const char **llm_visible_groups = NULL;
size_t llm_visible_group_count = 0;
size_t i;
esp_err_t ret = ESP_OK;
if (!config || !paths) {
return ESP_ERR_INVALID_ARG;
}
ESP_RETURN_ON_ERROR(claw_cap_init(), TAG, "Failed to init claw_cap");
enabled_map = calloc(entry_count > 0 ? entry_count : 1, sizeof(enabled_map[0]));
llm_visible_map = calloc(entry_count > 0 ? entry_count : 1, sizeof(llm_visible_map[0]));
llm_visible_groups = calloc(entry_count > 0 ? entry_count : 1, sizeof(llm_visible_groups[0]));
if (!enabled_map || !llm_visible_map || !llm_visible_groups) {
free(enabled_map);
free(llm_visible_map);
free(llm_visible_groups);
return ESP_ERR_NO_MEM;
}
ESP_GOTO_ON_ERROR(app_cap_build_group_map(config->enabled_cap_groups,
s_capability_group_entries,
entry_count,
enabled_map,
true,
false),
cleanup, TAG, "Failed to parse capability whitelist");
ESP_GOTO_ON_ERROR(app_cap_build_group_map(config->llm_visible_cap_groups,
s_capability_group_entries,
entry_count,
llm_visible_map,
false,
true),
cleanup, TAG, "Failed to parse LLM-visible capability groups");
for (i = 0; i < entry_count; i++) {
const app_capability_group_entry_t *entry = &s_capability_group_entries[i];
if (!enabled_map[i]) {
ESP_LOGI(TAG, "Skipping capability group at init: %s", entry->group_id);
continue;
}
if (entry->prepare) {
ESP_GOTO_ON_ERROR(entry->prepare(config, paths),
cleanup, TAG, "Failed to prepare %s", entry->group_id);
}
ESP_GOTO_ON_ERROR(app_cap_register_entry(entry, config, paths),
cleanup, TAG, "Failed to register %s", entry->group_id);
if (llm_visible_map[i]) {
llm_visible_groups[llm_visible_group_count++] = entry->group_id;
}
}
ret = claw_cap_set_llm_visible_groups(llm_visible_groups, llm_visible_group_count);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to set LLM-visible capability groups: %s", esp_err_to_name(ret));
goto cleanup;
}
ret = claw_cap_start_all();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start capabilities: %s", esp_err_to_name(ret));
goto cleanup;
}
cleanup:
free(enabled_map);
free(llm_visible_map);
free(llm_visible_groups);
return ret;
}
esp_err_t app_capabilities_get_compiled_groups(const app_capability_group_info_t **groups,
size_t *count)
{
if (!groups || !count) {
return ESP_ERR_INVALID_ARG;
}
*groups = s_capability_group_infos;
*count = sizeof(s_capability_group_infos) / sizeof(s_capability_group_infos[0]);
return ESP_OK;
}