* Copyright (C) 2026 Xiaomi Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "tools/tool_shell.h"
#include "agent_config.h"
#include "agent_compat.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <dirent.h>
#include <limits.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <sys/wait.h>
#include "cJSON.h"
static const char *TAG = "shell";
#define SHELL_OUTPUT_MAX 4096
#define SHELL_CMD_MAX 256
#if AGENT_SHELL_SECURITY == AGENT_SHELL_SECURITY_ALLOWLIST
static const char *s_blocked[] = {
"reboot", "shutdown", "poweroff", "mkfs",
"dd", "flash_eraseall", "format", "rm",
"mv", "mount", "umount", "insmod", "rmmod",
"kill", "killall", "ifconfig", "route",
"iptables", "passwd", "su", "chmod", "chown",
NULL
};
#endif
#if AGENT_SHELL_SECURITY == AGENT_SHELL_SECURITY_FULL
static const char *s_critical[] = {
"reboot", "shutdown", "poweroff", "mkfs",
"format", "flash_eraseall", "insmod", "rmmod",
NULL
};
#endif
#if AGENT_SHELL_SECURITY == AGENT_SHELL_SECURITY_ALLOWLIST
static const char *s_allowed[] = {
"free", "ps", "df", "uptime", "uname", "cat", "ls",
"echo", "date", "env", "pwd", "id", "whoami",
"curl", "wget", "ping", "nslookup",
"lvctl", "nxplayer", "fbcapture", "input", "pm", "am",
"jq", "cut", "uniq", "head", "tail", "tr", "wc", "grep", "sed",
"dmesg", "hexdump",
"lua", "quickjs",
"voice_start", "voice_stop",
"voice_test_tts", "voice_test_asr",
"set_voice_tts", "set_voice_asr",
"cron_start", "heap_info", "config_show",
"node_list", "node_start", "node_stop", "net_status",
"router_status", "router_profile", "router_set",
"set_gateway", "set_mqtt",
"session_list", "memory_read",
"mcp_list", "mcp_discover",
NULL
};
#endif
#if AGENT_SHELL_SECURITY == AGENT_SHELL_SECURITY_ALLOWLIST
static bool has_shell_meta(const char *cmd)
{
const char *meta = "|;&`$(){}><\n\\";
for (const char *p = cmd; *p; p++) {
if (strchr(meta, *p)) return true;
}
return false;
}
#endif
static int is_blocked(const char *cmd)
{
#if AGENT_SHELL_SECURITY == AGENT_SHELL_SECURITY_DENY
return 1;
#elif AGENT_SHELL_SECURITY == AGENT_SHELL_SECURITY_FULL
* This mode is designed for skill authoring and development.
* Skills can use pipes, redirects, and command chaining. */
char first[64];
int i = 0;
while (cmd[i] == ' ') i++;
int j = 0;
while (cmd[i] && cmd[i] != ' ' && j < (int)sizeof(first) - 1)
first[j++] = cmd[i++];
first[j] = '\0';
const char *basename = strrchr(first, '/');
const char *name = basename ? basename + 1 : first;
for (int k = 0; s_critical[k]; k++) {
if (strcmp(name, s_critical[k]) == 0)
return 1;
}
return 0;
#else
* This is the production-safe mode. */
if (has_shell_meta(cmd))
return 1;
char first[64];
int i = 0;
while (cmd[i] == ' ') i++;
int j = 0;
while (cmd[i] && cmd[i] != ' ' && j < (int)sizeof(first) - 1)
first[j++] = cmd[i++];
first[j] = '\0';
const char *basename = strrchr(first, '/');
const char *name = basename ? basename + 1 : first;
for (int k = 0; s_allowed[k]; k++) {
if (strcmp(name, s_allowed[k]) == 0)
return 0;
}
for (int k = 0; s_blocked[k]; k++) {
if (strcmp(name, s_blocked[k]) == 0)
return 1;
}
return 1;
#endif
}
static size_t read_proc_file(const char *path, char *buf, size_t buf_size)
{
FILE *fp = fopen(path, "r");
if (!fp) return 0;
size_t n = fread(buf, 1, buf_size - 1, fp);
buf[n] = '\0';
fclose(fp);
return n;
}
static int builtin_free(char *output, size_t output_size)
{
char buf[1024] = {0};
if (read_proc_file("/proc/meminfo", buf, sizeof(buf)) == 0)
return ERROR;
snprintf(output, output_size, "%s", buf);
return OK;
}
static int builtin_ps(char *output, size_t output_size)
{
DIR *d = opendir("/proc");
if (!d) return ERROR;
size_t off = 0;
off += snprintf(output + off, output_size - off,
"%-6s %-4s %-8s %-20s %s\n",
"PID", "PRI", "STACK", "STATUS", "NAME");
struct dirent *ent;
while ((ent = readdir(d)) != NULL && off < output_size - 128) {
if (ent->d_name[0] < '0' || ent->d_name[0] > '9')
continue;
char path[128];
char buf[512] = {0};
snprintf(path, sizeof(path), "/proc/%s/status", ent->d_name);
if (read_proc_file(path, buf, sizeof(buf)) > 0) {
char *nl = strchr(buf, '\n');
if (nl) *nl = '\0';
off += snprintf(output + off, output_size - off,
"%-6s %s\n", ent->d_name, buf);
}
}
closedir(d);
return OK;
}
static int builtin_df(char *output, size_t output_size)
{
static const char *mounts[] = { "/", "/tmp", "/data", NULL };
size_t off = 0;
off += snprintf(output + off, output_size - off,
"%-12s %12s %12s %12s\n",
"Filesystem", "Total", "Used", "Free");
for (int i = 0; mounts[i]; i++) {
struct statfs sfs;
if (statfs(mounts[i], &sfs) == 0) {
unsigned long total = (unsigned long)sfs.f_blocks * sfs.f_bsize;
unsigned long avail = (unsigned long)sfs.f_bavail * sfs.f_bsize;
unsigned long used = total - avail;
off += snprintf(output + off, output_size - off,
"%-12s %10luKB %10luKB %10luKB\n",
mounts[i], total / 1024, used / 1024, avail / 1024);
}
if (off >= output_size - 1) break;
}
return OK;
}
static int builtin_uptime(char *output, size_t output_size)
{
char buf[128] = {0};
if (read_proc_file("/proc/uptime", buf, sizeof(buf)) == 0)
return ERROR;
snprintf(output, output_size, "Uptime: %s", buf);
return OK;
}
static int builtin_uname(char *output, size_t output_size)
{
char buf[256] = {0};
if (read_proc_file("/proc/version", buf, sizeof(buf)) > 0) {
snprintf(output, output_size, "%s", buf);
return OK;
}
snprintf(output, output_size, "NuttX (version info unavailable)");
return OK;
}
static const char *s_allowed_prefixes[] = {
"/proc/", "/data/agent/", "/tmp/", NULL
};
static bool is_path_allowed(const char *path)
{
char resolved[PATH_MAX];
if (realpath(path, resolved) == NULL) {
strncpy(resolved, path, sizeof(resolved) - 1);
resolved[sizeof(resolved) - 1] = '\0';
}
size_t rlen = strlen(resolved);
if (rlen > 1 && resolved[rlen - 1] == '/')
resolved[--rlen] = '\0';
for (int i = 0; s_allowed_prefixes[i]; i++) {
size_t plen = strlen(s_allowed_prefixes[i]);
size_t cmp_len = (plen > 1 && s_allowed_prefixes[i][plen - 1] == '/')
? plen - 1 : plen;
if (strncmp(resolved, s_allowed_prefixes[i], cmp_len) == 0
&& (resolved[cmp_len] == '\0' || resolved[cmp_len] == '/'))
return true;
}
return false;
}
static int builtin_cat(const char *path, char *output, size_t output_size)
{
if (!path || !path[0]) return ERROR;
if (!is_path_allowed(path)) {
snprintf(output, output_size, "Access denied: path outside allowed directories");
return ERROR;
}
size_t n = read_proc_file(path, output, output_size);
return n > 0 ? OK : ERROR;
}
static int builtin_dmesg(char *output, size_t output_size)
{
size_t n = read_proc_file("/dev/kmsg", output, output_size);
if (n > 0)
return OK;
n = read_proc_file("/proc/kmsg", output, output_size);
if (n > 0)
return OK;
snprintf(output, output_size, "(dmesg not available on this platform)");
return OK;
}
static int builtin_popen_cmd(const char *cmd, char *output, size_t output_size)
{
#ifdef CONFIG_SYSTEM_POPEN
FILE *fp = popen(cmd, "r");
if (!fp) {
snprintf(output, output_size,
"Failed to execute: %s (%s)", cmd, strerror(errno));
return ERROR;
}
size_t n = fread(output, 1, output_size - 1, fp);
output[n] = '\0';
int status = pclose(fp);
if (status != 0 && n == 0) {
snprintf(output, output_size,
"Command failed with exit code %d", WEXITSTATUS(status));
return ERROR;
}
return (status == 0) ? OK : ERROR;
#else
(void)cmd;
snprintf(output, output_size, "popen not available (enable CONFIG_SYSTEM_POPEN)");
return ERROR;
#endif
}
static int builtin_ls(const char *path, char *output, size_t output_size)
{
char dir_buf[PATH_MAX];
const char *dir = (path && path[0]) ? path : "/data/agent";
strncpy(dir_buf, dir, sizeof(dir_buf) - 1);
dir_buf[sizeof(dir_buf) - 1] = '\0';
size_t dlen = strlen(dir_buf);
if (dlen > 1 && dir_buf[dlen - 1] == '/')
dir_buf[dlen - 1] = '\0';
if (!is_path_allowed(dir_buf) && !is_path_allowed(dir)) {
snprintf(output, output_size, "Access denied: path outside allowed directories");
return ERROR;
}
DIR *d = opendir(dir_buf);
if (!d) {
snprintf(output, output_size, "Cannot open directory %s: %s",
dir_buf, strerror(errno));
return ERROR;
}
size_t off = 0;
struct dirent *ent;
while ((ent = readdir(d)) != NULL && off < output_size - 64) {
off += snprintf(output + off, output_size - off, "%s\n", ent->d_name);
}
closedir(d);
if (off == 0)
snprintf(output, output_size, "(empty)");
return OK;
}
int tool_run_shell_execute(const char *input_json, char *output, size_t output_size)
{
const char *command = NULL;
cJSON *root = cJSON_Parse(input_json);
if (root) {
cJSON *c = cJSON_GetObjectItem(root, "command");
if (c && cJSON_IsString(c))
command = c->valuestring;
}
if (!command || command[0] == '\0') {
snprintf(output, output_size, "{\"error\":\"Missing 'command' parameter\"}");
cJSON_Delete(root);
return ERROR;
}
if (strlen(command) > SHELL_CMD_MAX) {
snprintf(output, output_size, "{\"error\":\"Command too long (max %d)\"}", SHELL_CMD_MAX);
cJSON_Delete(root);
return ERROR;
}
if (is_blocked(command)) {
syslog(LOG_WARNING, "[%s] Blocked dangerous command: %s\n", TAG, command);
snprintf(output, output_size, "{\"error\":\"Command '%s' is blocked for safety\"}", command);
cJSON_Delete(root);
return ERROR;
}
syslog(LOG_INFO, "[%s] Executing: %s\n", TAG, command);
* tool_run_shell_execute is called from parallel tool threads
* (16KB stack) and SHELL_OUTPUT_MAX (4096) on stack would consume
* too much of the available stack space. */
char *cmd_buf = calloc(1, SHELL_OUTPUT_MAX);
if (!cmd_buf) {
snprintf(output, output_size, "{\"error\":\"OOM\"}");
cJSON_Delete(root);
return ERROR;
}
int ret = ERROR;
while (*command == ' ') command++;
if (strcmp(command, "free") == 0 || strncmp(command, "free ", 5) == 0) {
ret = builtin_free(cmd_buf, SHELL_OUTPUT_MAX);
} else if (strcmp(command, "ps") == 0 || strncmp(command, "ps ", 3) == 0) {
ret = builtin_ps(cmd_buf, SHELL_OUTPUT_MAX);
} else if (strcmp(command, "df") == 0 || strncmp(command, "df ", 3) == 0) {
ret = builtin_df(cmd_buf, SHELL_OUTPUT_MAX);
} else if (strcmp(command, "uptime") == 0) {
ret = builtin_uptime(cmd_buf, SHELL_OUTPUT_MAX);
} else if (strcmp(command, "uname") == 0 || strncmp(command, "uname ", 6) == 0) {
ret = builtin_uname(cmd_buf, SHELL_OUTPUT_MAX);
} else if (strncmp(command, "cat ", 4) == 0) {
const char *arg = command + 4;
while (*arg == ' ') arg++;
ret = builtin_cat(arg, cmd_buf, SHELL_OUTPUT_MAX);
} else if (strncmp(command, "ls", 2) == 0) {
const char *arg = command + 2;
while (*arg == ' ') arg++;
ret = builtin_ls(arg[0] ? arg : NULL, cmd_buf, SHELL_OUTPUT_MAX);
} else if (strcmp(command, "dmesg") == 0 || strncmp(command, "dmesg ", 6) == 0) {
ret = builtin_dmesg(cmd_buf, SHELL_OUTPUT_MAX);
} else if (strncmp(command, "pm ", 3) == 0 || strcmp(command, "pm") == 0) {
ret = builtin_popen_cmd(command, cmd_buf, SHELL_OUTPUT_MAX);
} else if (strncmp(command, "am ", 3) == 0 || strcmp(command, "am") == 0) {
ret = builtin_popen_cmd(command, cmd_buf, SHELL_OUTPUT_MAX);
} else {
#ifdef CONFIG_SYSTEM_POPEN
* Command has already passed security checks (is_blocked). */
FILE *fp = popen(command, "r");
if (fp) {
size_t n = fread(cmd_buf, 1, SHELL_OUTPUT_MAX - 1, fp);
cmd_buf[n] = '\0';
int status = pclose(fp);
ret = (status == 0) ? OK : ERROR;
if (ret != OK && n == 0) {
snprintf(cmd_buf, SHELL_OUTPUT_MAX,
"Command failed with exit code %d", WEXITSTATUS(status));
}
syslog(LOG_INFO, "[%s] popen(%s) exit=%d, %zu bytes\n",
TAG, command, status, n);
} else {
syslog(LOG_ERR, "[%s] popen(%s) failed: %s\n",
TAG, command, strerror(errno));
snprintf(cmd_buf, SHELL_OUTPUT_MAX,
"Failed to execute: %s (%s)", command, strerror(errno));
ret = ERROR;
}
#else
syslog(LOG_WARNING, "[%s] Unsupported command: %s\n", TAG, command);
snprintf(cmd_buf, SHELL_OUTPUT_MAX,
"Unsupported command: %s (enable CONFIG_SYSTEM_POPEN for external commands)",
command);
ret = ERROR;
#endif
}
cJSON_Delete(root);
if (ret != OK) {
snprintf(output, output_size, "{\"error\":\"Command failed: %s\"}", command);
free(cmd_buf);
return ERROR;
}
cJSON *result = cJSON_CreateObject();
cJSON_AddNumberToObject(result, "exit_code", 0);
cJSON_AddStringToObject(result, "output", cmd_buf);
free(cmd_buf);
char *json_str = cJSON_PrintUnformatted(result);
cJSON_Delete(result);
if (json_str) {
strncpy(output, json_str, output_size - 1);
output[output_size - 1] = '\0';
syslog(LOG_INFO, "[%s] Result: %d bytes\n", TAG, (int)strlen(output));
free(json_str);
}
return OK;
}