/* Plugins: plugin.json exec tools (stdin JSON, env, timeout, perm), skills * and MCP entries from plugins, and workspace trust. */ #define _POSIX_C_SOURCE 200809L #include "cst_agent.h" #include "cst_ext.h" #include "cst_mcp.h" #include "cst_plugins.h" #include "cst_skills.h" #include "cst_tools.h" #include #include #include #include #include static int nfail; #define CHECK(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \ nfail++; \ } \ } while (0) static char g_root[256]; static void put(const char *rel, const char *text) { char p[512]; snprintf(p, sizeof(p), "%s/%s", g_root, rel); for (char *q = p + strlen(g_root) + 1; *q; q++) { if (*q == '/') { *q = 0; mkdir(p, 0755); *q = '/'; } } FILE *fp = fopen(p, "w"); if (fp) { fputs(text, fp); fclose(fp); } } static void run(const char *ws, const char *tool, const char *k1, const char *v1, const char *k2, const char *v2, cst_tool_result_t *r) { ctl_tool_call_t c; memset(&c, 0, sizeof(c)); snprintf(c.name, sizeof(c.name), "%s", tool); if (k1) { snprintf(c.args[c.nargs].key, sizeof(c.args[0].key), "%s", k1); c.args[c.nargs].value = (char *)v1; c.args[c.nargs].value_len = strlen(v1); c.nargs++; } if (k2) { snprintf(c.args[c.nargs].key, sizeof(c.args[0].key), "%s", k2); c.args[c.nargs].value = (char *)v2; c.args[c.nargs].value_len = strlen(v2); c.nargs++; } cst_tool_ctx_t ctx; memset(&ctx, 0, sizeof(ctx)); ctx.workspace_root = ws; memset(r, 0, sizeof(*r)); cst_tool_run(&ctx, &c, r); } int main(void) { snprintf(g_root, sizeof(g_root), "/tmp/cst-plugins-%d", (int)getpid()); mkdir(g_root, 0755); char ws[300], cfgdir[300]; snprintf(ws, sizeof(ws), "%s/ws", g_root); snprintf(cfgdir, sizeof(cfgdir), "%s/cfg", g_root); setenv("XDG_CONFIG_HOME", cfgdir, 1); put("cfg/cst-agent/plugins/echo/plugin.json", "{\"name\": \"echo\", \"description\": \"Test plugin.\",\n" " \"tools\": [\n" " {\"name\": \"echo_args\", \"description\": \"Echo stdin.\",\n" " \"command\": [\"sh\", \"run.sh\", \"--flag\"], \"perm\": \"read\",\n" " \"parameters\": {\"type\": \"object\", \"properties\": {\n" " \"name\": {\"type\": \"string\"}, \"count\": {\"type\": " "\"integer\"},\n" " \"tags\": {\"type\": \"array\"}}, \"required\": [\"name\"]}},\n" " {\"name\": \"slow\", \"command\": \"sleep 5\", \"timeout\": 1},\n" " {\"name\": \"missing\", \"command\": [\"cst-no-such-cmd-xyz\"], " "\"perm\": \"write\"},\n" " {\"name\": \"piped\", \"command\": \"tr a-z A-Z | tr -d '{}'\", " "\"perm\": \"net\"},\n" " {\"name\": \"bad\"}\n" " ],\n" " \"mcpServers\": {\"plug-srv\": {\"command\": \"true\"}}}\n"); put("cfg/cst-agent/plugins/echo/run.sh", "cat\necho\necho \"arg=$1 dir=$(basename \"$CST_PLUGIN_DIR\") " "tool=$CST_TOOL ws=$(basename \"$CST_WORKSPACE\")\"\n"); put("cfg/cst-agent/plugins/echo/skills/plug-skill/SKILL.md", "---\nname: plug-skill\ndescription: Shipped by a plugin.\n---\nP\n"); put("cfg/cst-agent/plugins/notaplugin/readme.txt", "no plugin.json\n"); put("ws/.agent/plugins/local/plugin.json", "{\"tools\": [{\"name\": \"local_tool\", \"command\": \"echo local\"}]}"); put("ws/keep.txt", "x\n"); int untrusted = -1; int n = cst_plugins_load(ws, cst_settings_trusted(ws), &untrusted); CHECK(n == 1, "one user plugin (workspace not trusted)"); CHECK(untrusted == 1, "workspace plugin counted as untrusted"); CHECK(!cst_tool_find("local_tool"), "untrusted workspace plugin not loaded"); const cst_tool_def_t *d = cst_tool_find("echo_args"); CHECK(d && d->perm == CST_TP_READ, "perm read"); d = cst_tool_find("slow"); CHECK(d && d->perm == CST_TP_SHELL, "perm defaults to shell"); d = cst_tool_find("missing"); CHECK(d && d->perm == CST_TP_WRITE, "perm write"); d = cst_tool_find("piped"); CHECK(d && d->perm == CST_TP_NET, "perm net"); CHECK(!cst_tool_find("bad"), "tool without command skipped"); cst_plugin_info_t info[CST_PLUGINS_MAX]; int ni = cst_plugins_info(info, CST_PLUGINS_MAX); CHECK(ni == 1 && strcmp(info[0].name, "echo") == 0 && info[0].ntools == 4 && info[0].nmcp == 1 && info[0].nskills_root == 1 && strstr(info[0].error, "bad"), "plugin info"); cst_tool_result_t r; run(ws, "echo_args", "count", "3", "name", "x y", &r); CHECK(r.exit_code == 0 && r.out && strstr(r.out, "\"count\":3") && strstr(r.out, "\"name\":\"x y\""), "typed JSON on stdin"); CHECK(r.out && strstr(r.out, "arg=--flag dir=echo tool=echo_args ws=ws"), "argv and CST_* environment"); cst_tool_result_free(&r); run(ws, "echo_args", "count", "3", NULL, NULL, &r); CHECK(r.exit_code != 0 && r.out && strstr(r.out, "name"), "schema required enforced"); cst_tool_result_free(&r); run(ws, "piped", "name", "abc", NULL, NULL, &r); CHECK(r.exit_code == 0 && r.out && strstr(r.out, "\"NAME\":\"ABC\""), "string command: whole pipeline reads stdin"); cst_tool_result_free(&r); run(ws, "slow", NULL, NULL, NULL, NULL, &r); CHECK(r.exit_code == 124 && r.out && strstr(r.out, "timed out after 1 s"), "per-tool timeout"); cst_tool_result_free(&r); run(ws, "missing", NULL, NULL, NULL, NULL, &r); CHECK(r.exit_code == 127 && r.out && strstr(r.out, "command not found"), "missing command hint"); cst_tool_result_free(&r); cst_skill_t sv[CST_SKILLS_MAX]; int ns = cst_skills_list(ws, sv, CST_SKILLS_MAX); CHECK(ns == 1 && strcmp(sv[0].name, "plug-skill") == 0 && strcmp(sv[0].source, "plugin:echo") == 0, "plugin skills root"); cst_mcp_info_t mi[CST_MCP_MAX]; int nm = cst_mcp_info(mi, CST_MCP_MAX); CHECK(nm == 1 && strcmp(mi[0].name, "plug-srv") == 0 && strcmp(mi[0].source, "plugin:echo") == 0, "plugin MCP server config"); cst_mcp_stop_all(); cst_plugins_unload(); CHECK(!cst_tool_find("echo_args"), "unload drops tools"); CHECK(cst_skills_list(ws, sv, CST_SKILLS_MAX) == 0, "unload drops skills"); /* trust the workspace -> its plugin loads too */ CHECK(cst_settings_trust(ws, 1) == 0 && cst_settings_trusted(ws), "trust saved"); n = cst_plugins_load(ws, cst_settings_trusted(ws), &untrusted); CHECK(n == 2 && untrusted == 0 && cst_tool_find("local_tool"), "trusted workspace plugin loads"); run(ws, "local_tool", NULL, NULL, NULL, NULL, &r); CHECK(r.exit_code == 0 && r.out && strstr(r.out, "local"), "local tool"); cst_tool_result_free(&r); cst_plugins_unload(); cst_mcp_stop_all(); CHECK(cst_settings_trust(ws, 0) == 0 && !cst_settings_trusted(ws), "untrust saved"); /* extensions: the minimal profile loads nothing */ cst_agent_config_t cfg; memset(&cfg, 0, sizeof(cfg)); snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", ws); cfg.tool_profile = CST_TOOLS_MINIMAL; char rep[256]; cst_extensions_load(&cfg, rep, sizeof(rep)); CHECK(!rep[0] && !cst_tool_find("echo_args"), "minimal: no extensions"); cfg.tool_profile = CST_TOOLS_DEFAULT; cst_extensions_load(&cfg, rep, sizeof(rep)); CHECK(strstr(rep, "1 skill") && strstr(rep, "1 plugin (4 tools)") && strstr(rep, "untrusted"), "extensions report"); CHECK(cst_tool_find("skill") && cst_tool_find("echo_args"), "extensions register tools"); cst_extensions_unload(); CHECK(!cst_tool_find("skill") && !cst_tool_find("echo_args"), "extensions unload"); char cmd[400]; snprintf(cmd, sizeof(cmd), "rm -rf %s", g_root); if (system(cmd) != 0) fprintf(stderr, "note: cleanup failed\n"); if (nfail) { fprintf(stderr, "test_plugins: %d failure(s)\n", nfail); return 1; } printf("test_plugins: OK\n"); return 0; }