#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE 700 #endif #include "cst_plugins.h" #include #include #include #include #include #include "cJSON.h" #include "cst_mcp.h" #include "cst_sandbox.h" #include "cst_skills.h" #include "cst_tools.h" #include "cst_util.h" #define PLUGIN_JSON_MAX (256 * 1024) #define PLUGIN_OUT_MAX (512 * 1024) #define PLUGIN_TIMEOUT_DEF 60 #define PLUGIN_TIMEOUT_MAX 600 /* What a registered plugin tool runs. */ typedef struct ptool { char tool[CTL_MAX_NAME]; char plugin[64]; char dir[OS_PATH_MAX]; char *shell; /* complete command line (argv quoted, or the raw string) */ int timeout; struct ptool *next; } ptool_t; static ptool_t *g_tools; static cst_plugin_info_t g_info[CST_PLUGINS_MAX]; static int g_ninfo; static char *slurp(const char *path, size_t max) { FILE *fp = fopen(path, "rb"); if (!fp) return NULL; cst_buf_t b = {0}; char tmp[8192]; size_t n; while ((n = fread(tmp, 1, sizeof(tmp), fp)) > 0) { if (b.len + n > max) { cst_buf_free(&b); fclose(fp); return NULL; } cst_buf_append(&b, tmp, n); } fclose(fp); if (!b.buf) cst_buf_puts(&b, ""); cst_strip_bom(b.buf, &b.len); return cst_buf_steal(&b); } /* POSIX sh single-quoting. */ static void sh_quote(cst_buf_t *b, const char *s) { cst_buf_puts(b, "'"); for (; *s; s++) { if (*s == '\'') cst_buf_puts(b, "'\\''"); else cst_buf_append(b, s, 1); } cst_buf_puts(b, "'"); } static int is_file(const char *p) { struct stat st; return stat(p, &st) == 0 && S_ISREG(st.st_mode); } static int is_dir(const char *p) { struct stat st; return stat(p, &st) == 0 && S_ISDIR(st.st_mode); } static cst_tool_perm_t parse_perm(const char *s) { if (!s) return CST_TP_SHELL; if (strcmp(s, "read") == 0) return CST_TP_READ; if (strcmp(s, "write") == 0) return CST_TP_WRITE; if (strcmp(s, "net") == 0) return CST_TP_NET; return CST_TP_SHELL; } static int plugin_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r, void *ud) { const ptool_t *pt = ud; r->exit_code = 1; const cst_tool_def_t *def = cst_tool_find(pt->tool); char *args = cst_tool_args_json(def, c); char tmp[OS_PATH_MAX]; int fd = os_mkstemp(tmp, sizeof(tmp), "cst-plugin-"); if (fd < 0 || !args) { if (fd >= 0) { close(fd); unlink(tmp); } free(args); r->out = strdup("error: cannot create the argument file"); r->out_len = r->out ? strlen(r->out) : 0; return 0; } size_t al = strlen(args); int wok = write(fd, args, al) == (ssize_t)al; close(fd); free(args); if (!wok) { unlink(tmp); r->out = strdup("error: cannot write the argument file"); r->out_len = r->out ? strlen(r->out) : 0; return 0; } char root[OS_PATH_MAX]; if (os_realpath(ctx->workspace_root, root, sizeof(root)) != 0) snprintf(root, sizeof(root), "%s", ctx->workspace_root); cst_buf_t cmd = {0}; cst_buf_puts(&cmd, "CST_PLUGIN_DIR="); sh_quote(&cmd, pt->dir); cst_buf_puts(&cmd, " CST_WORKSPACE="); sh_quote(&cmd, root); cst_buf_puts(&cmd, " CST_TOOL="); sh_quote(&cmd, pt->tool); cst_buf_puts(&cmd, "; export CST_PLUGIN_DIR CST_WORKSPACE CST_TOOL; "); cst_buf_puts(&cmd, pt->shell); cst_buf_puts(&cmd, " < "); sh_quote(&cmd, tmp); int old = os_get_run_timeout(); os_set_run_timeout(pt->timeout); os_run_result_t rr = {0}; int rc = ctx->sandbox ? cst_sandbox_run_shell(root, cmd.buf, PLUGIN_OUT_MAX, &rr) : os_run_shell_c(root, cmd.buf, PLUGIN_OUT_MAX, &rr); os_set_run_timeout(old); cst_buf_free(&cmd); unlink(tmp); if (rc != 0) { os_run_result_free(&rr); r->out = strdup("error: could not start the plugin command"); r->out_len = r->out ? strlen(r->out) : 0; return 0; } cst_buf_t out = {0}; if (rr.out) cst_buf_append(&out, rr.out, rr.out_len); if (rr.exit_code == 124) cst_buf_printf(&out, "\n[plugin %s: timed out after %d s]", pt->plugin, pt->timeout); else if (rr.exit_code == 127) cst_buf_printf(&out, "\n[plugin %s: command not found — check \"command\" " "in %s/plugin.json]", pt->plugin, pt->dir); if (!out.buf) cst_buf_puts(&out, ""); r->exit_code = rr.exit_code; os_run_result_free(&rr); r->out_len = out.len; r->out = cst_buf_steal(&out); return 0; } /* argv array -> quoted command line; relative entries found in the plugin * directory become absolute. String commands are used as they are. */ static char *build_shell(const cJSON *command, const char *dir) { if (cJSON_IsString(command) && command->valuestring[0]) { /* a group, so the stdin redirect covers pipelines too */ cst_buf_t b = {0}; cst_buf_printf(&b, "{ %s\n}", command->valuestring); return cst_buf_steal(&b); } if (!cJSON_IsArray(command) || cJSON_GetArraySize(command) == 0) return NULL; cst_buf_t b = {0}; cst_buf_puts(&b, "exec"); const cJSON *it = NULL; cJSON_ArrayForEach(it, command) { if (!cJSON_IsString(it)) { cst_buf_free(&b); return NULL; } const char *a = it->valuestring; char abs[OS_PATH_MAX * 2]; if (a[0] && a[0] != '/' && a[0] != '-' && snprintf(abs, sizeof(abs), "%s/%s", dir, a) < (int)sizeof(abs) && is_file(abs)) a = abs; cst_buf_puts(&b, " "); sh_quote(&b, a); } return cst_buf_steal(&b); } static void load_plugin(const char *dir, const char *dirname, const char *source) { if (g_ninfo >= CST_PLUGINS_MAX) return; cst_plugin_info_t *info = &g_info[g_ninfo]; memset(info, 0, sizeof(*info)); snprintf(info->dir, sizeof(info->dir), "%s", dir); snprintf(info->source, sizeof(info->source), "%s", source); cst_tool_sanitize_name(dirname, info->name, sizeof(info->name)); char pj[OS_PATH_MAX + 16]; snprintf(pj, sizeof(pj), "%s/plugin.json", dir); char *text = slurp(pj, PLUGIN_JSON_MAX); if (!text) return; /* not a plugin directory */ g_ninfo++; cJSON *root = cJSON_Parse(text); free(text); if (!cJSON_IsObject(root)) { snprintf(info->error, sizeof(info->error), "plugin.json is not a JSON " "object"); cJSON_Delete(root); return; } const cJSON *nm = cJSON_GetObjectItemCaseSensitive(root, "name"); if (cJSON_IsString(nm) && nm->valuestring[0]) cst_tool_sanitize_name(nm->valuestring, info->name, sizeof(info->name)); const cJSON *ds = cJSON_GetObjectItemCaseSensitive(root, "description"); if (cJSON_IsString(ds)) snprintf(info->desc, sizeof(info->desc), "%s", ds->valuestring); char owner[96]; snprintf(owner, sizeof(owner), "plugin:%.63s", info->name); const cJSON *tools = cJSON_GetObjectItemCaseSensitive(root, "tools"); const cJSON *t = NULL; cJSON_ArrayForEach(t, tools) { const cJSON *tn = cJSON_GetObjectItemCaseSensitive(t, "name"); if (!cJSON_IsString(tn) || !tn->valuestring[0]) { snprintf(info->error, sizeof(info->error), "a tool has no name"); continue; } char tname[CTL_MAX_NAME]; cst_tool_sanitize_name(tn->valuestring, tname, sizeof(tname)); char *shell = build_shell(cJSON_GetObjectItemCaseSensitive(t, "command"), dir); if (!shell) { snprintf(info->error, sizeof(info->error), "tool %s: \"command\" must be a string or an array of strings", tname); continue; } const cJSON *params = cJSON_GetObjectItemCaseSensitive(t, "parameters"); if (!params) params = cJSON_GetObjectItemCaseSensitive(t, "inputSchema"); char *schema = cJSON_IsObject(params) ? cJSON_PrintUnformatted(params) : strdup("{\"type\":\"object\",\"properties\":{}}"); const cJSON *pm = cJSON_GetObjectItemCaseSensitive(t, "perm"); const cJSON *to = cJSON_GetObjectItemCaseSensitive(t, "timeout"); const cJSON *td = cJSON_GetObjectItemCaseSensitive(t, "description"); ptool_t *pt = calloc(1, sizeof(*pt)); if (!pt || !schema) { free(pt); free(schema); free(shell); continue; } snprintf(pt->tool, sizeof(pt->tool), "%s", tname); snprintf(pt->plugin, sizeof(pt->plugin), "%s", info->name); snprintf(pt->dir, sizeof(pt->dir), "%s", dir); pt->shell = shell; pt->timeout = cJSON_IsNumber(to) && to->valueint > 0 ? (to->valueint > PLUGIN_TIMEOUT_MAX ? PLUGIN_TIMEOUT_MAX : to->valueint) : PLUGIN_TIMEOUT_DEF; cst_tool_def_t d; memset(&d, 0, sizeof(d)); d.name = tname; d.desc = cJSON_IsString(td) ? td->valuestring : ""; d.schema_json = schema; d.perm = parse_perm(cJSON_IsString(pm) ? pm->valuestring : NULL); d.run_ex = plugin_run; d.ud = pt; d.owner = owner; if (cst_tool_register(&d) != 0) { snprintf(info->error, sizeof(info->error), "tool %s: name already taken", tname); free(pt->shell); free(pt); } else { pt->next = g_tools; g_tools = pt; info->ntools++; } free(schema); } const cJSON *sk = cJSON_GetObjectItemCaseSensitive(root, "skills"); char sdir[OS_PATH_MAX * 2]; snprintf(sdir, sizeof(sdir), "%s/%s", dir, cJSON_IsString(sk) ? sk->valuestring : "skills"); if (is_dir(sdir)) { cst_skills_add_root(sdir, owner); info->nskills_root = 1; } const cJSON *servers = cJSON_GetObjectItemCaseSensitive(root, "mcpServers"); if (!servers) { const cJSON *mcp = cJSON_GetObjectItemCaseSensitive(root, "mcp"); servers = cJSON_GetObjectItemCaseSensitive(mcp, "mcpServers"); if (!servers) servers = cJSON_GetObjectItemCaseSensitive(mcp, "servers"); } const cJSON *sv = NULL; cJSON_ArrayForEach(sv, servers) { char *sj = cJSON_PrintUnformatted(sv); if (sj && sv->string && cst_mcp_add_config_json(sv->string, sj, dir, owner) == 0) info->nmcp++; free(sj); } cJSON_Delete(root); } static int scan(const char *root, const char *source) { int n = 0; os_dir_t *d = os_dir_open(root); if (!d) return 0; char *names[CST_PLUGINS_MAX * 2]; int nn = 0; const os_dirent_t *de; while (nn < CST_PLUGINS_MAX * 2 && (de = os_dir_next(d)) != NULL) { if (de->name[0] == '.') continue; names[nn] = strdup(de->name); if (names[nn]) nn++; } os_dir_close(d); for (int i = 1; i < nn; i++) for (int j = i; j > 0 && strcmp(names[j - 1], names[j]) > 0; j--) { char *t = names[j]; names[j] = names[j - 1]; names[j - 1] = t; } for (int i = 0; i < nn; i++) { char dir[OS_PATH_MAX]; if (snprintf(dir, sizeof(dir), "%s/%s", root, names[i]) < (int)sizeof(dir) && is_dir(dir)) { int before = g_ninfo; load_plugin(dir, names[i], source); n += g_ninfo > before; } free(names[i]); } return n; } static int count_plugin_dirs(const char *root) { int n = 0; os_dir_t *d = os_dir_open(root); if (!d) return 0; const os_dirent_t *de; while ((de = os_dir_next(d)) != NULL) { char pj[OS_PATH_MAX]; if (de->name[0] != '.' && snprintf(pj, sizeof(pj), "%s/%s/plugin.json", root, de->name) < (int)sizeof(pj) && is_file(pj)) n++; } os_dir_close(d); return n; } int cst_plugins_load(const char *ws, int ws_trusted, int *untrusted) { if (untrusted) *untrusted = 0; int n = 0; char p[OS_PATH_MAX]; if (ws && ws[0]) { snprintf(p, sizeof(p), "%s/.agent/plugins", ws); if (ws_trusted) n += scan(p, "workspace"); else if (untrusted) *untrusted = count_plugin_dirs(p); } char cfg[OS_PATH_MAX]; if (os_config_dir(cfg, sizeof(cfg)) == 0) { snprintf(p, sizeof(p), "%.4000s/plugins", cfg); n += scan(p, "user"); } return n; } void cst_plugins_unload(void) { for (int i = 0; i < g_ninfo; i++) { char owner[96]; snprintf(owner, sizeof(owner), "plugin:%.63s", g_info[i].name); cst_tool_unregister_owner(owner); } cst_skills_clear_roots("plugin:"); while (g_tools) { ptool_t *n = g_tools->next; free(g_tools->shell); free(g_tools); g_tools = n; } g_ninfo = 0; } int cst_plugins_info(cst_plugin_info_t *out, int max) { int n = g_ninfo < max ? g_ninfo : max; for (int i = 0; i < n; i++) out[i] = g_info[i]; return n; }