Extensions: skills, plugins, MCP, sub-agents; configurable keys
- Skills (SKILL.md discovery, skill tool, /skills /skill) - plugin.json exec tools with permission prompts, timeouts and workspace trust (/plugins) - MCP stdio client: JSON-RPC initialize/tools/list/tools/call, mcp__server__tool, /mcp and sidebar status - task tool with general/explore/plan and .agent/agents/*.md roles, parallel read-only runs, cancel propagation, /agents - Keymap with Ctrl+X leader, keybinds.json overrides and /keys panel - Collapsed thinking, throttled streaming Markdown, leaner tool output, parallel read-only tools - Spawn: close-on-exec pipes and a process-wide fork lock - Tests for keymap, skills, plugins, MCP, sub-agents, shell compaction; docs/plugins.md
This commit is contained in:
+589
-105
@@ -7,6 +7,8 @@
|
||||
#include "cst_metrics.h"
|
||||
#include "cst_perms.h"
|
||||
#include "cst_sandbox.h"
|
||||
#include "cst_skills.h"
|
||||
#include "cst_subagent.h"
|
||||
#include "cst_tools.h"
|
||||
#include "cst_undo.h"
|
||||
#include "cst_util.h"
|
||||
@@ -16,6 +18,9 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <sys/utsname.h>
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -211,20 +216,156 @@ static void put_workflow(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
|
||||
#define PROJECT_NOTES_MAX (16 * 1024)
|
||||
|
||||
/* Default-profile prompt: static sections first (prefix-cache friendly),
|
||||
* then the protocol, then per-session facts. */
|
||||
static const char DEF_INTRO[] =
|
||||
"You are CST-Agent, a coding agent working in the user's local "
|
||||
"workspace. All tool paths are relative to the workspace root and shell "
|
||||
"commands run there.\n\n"
|
||||
"Principles:\n"
|
||||
"- Reply in the user's language. Be concise: lead with the result, no "
|
||||
"filler.\n"
|
||||
"- Understand before changing: look at the relevant code instead of "
|
||||
"guessing; never invent files, APIs or tool results.\n"
|
||||
"- Make the smallest change that fully solves the task, in the style of "
|
||||
"the surrounding code; leave unrelated code alone.\n"
|
||||
"- Verify (build, tests, or running it) before saying it works; if you "
|
||||
"could not verify, say so.\n"
|
||||
"- Keep going until the task is done or truly blocked.\n\n";
|
||||
|
||||
static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "\nTool use:\n");
|
||||
if (tool_on(cfg, "run_shell") &&
|
||||
(tool_on(cfg, "grep") || tool_on(cfg, "glob")))
|
||||
cst_buf_puts(b, "- Use the file tools rather than run_shell to look "
|
||||
"around: read_file (not cat/head/tail), grep (not "
|
||||
"grep/rg), glob and list_dir (not find/ls).\n");
|
||||
cst_buf_puts(b, "- Independent read-only calls can go out together in one "
|
||||
"reply; they run in parallel.\n");
|
||||
if (tool_on(cfg, "read_file"))
|
||||
cst_buf_puts(b, "- Read a file before editing it and again after a failed "
|
||||
"edit. The `N|` prefixes in read_file output are not part "
|
||||
"of the file.\n");
|
||||
cst_buf_puts(b, "- When a call fails, read the error and change approach "
|
||||
"instead of repeating the same call.\n");
|
||||
}
|
||||
|
||||
static void put_safety(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "\nSafety:\n"
|
||||
"- Do not commit, push or rewrite git history unless asked. "
|
||||
"Do not delete or overwrite files you did not create unless "
|
||||
"the task requires it. Never print secrets or API keys.\n");
|
||||
if (tool_on(cfg, "run_shell"))
|
||||
cst_buf_puts(b, "- run_shell may run in a sandbox (writes only inside the "
|
||||
"workspace and temp dir, no network). If a command is "
|
||||
"blocked the user is asked whether to rerun it outside; "
|
||||
"do not try to work around the sandbox.\n");
|
||||
}
|
||||
|
||||
/* Current branch from .git/HEAD of the workspace or a parent directory. */
|
||||
static void git_branch(const char *ws, char *out, size_t n) {
|
||||
out[0] = 0;
|
||||
char dir[4096];
|
||||
snprintf(dir, sizeof(dir), "%s", ws ? ws : ".");
|
||||
for (int depth = 0; depth < 8; depth++) {
|
||||
char p[4200], line[512];
|
||||
snprintf(p, sizeof(p), "%s/.git", dir);
|
||||
FILE *fp = fopen(p, "r");
|
||||
if (fp) {
|
||||
/* worktree / submodule: ".git" is a file "gitdir: <path>" */
|
||||
char gd[4096] = "";
|
||||
if (fgets(line, sizeof(line), fp) && strncmp(line, "gitdir: ", 8) == 0) {
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
if (line[8] == '/')
|
||||
snprintf(gd, sizeof(gd), "%s", line + 8);
|
||||
else
|
||||
snprintf(gd, sizeof(gd), "%.2000s/%.2000s", dir, line + 8);
|
||||
}
|
||||
fclose(fp);
|
||||
if (gd[0])
|
||||
snprintf(p, sizeof(p), "%s/HEAD", gd);
|
||||
else
|
||||
snprintf(p, sizeof(p), "%s/.git/HEAD", dir);
|
||||
fp = fopen(p, "r");
|
||||
if (fp) {
|
||||
if (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
if (strncmp(line, "ref: refs/heads/", 16) == 0)
|
||||
snprintf(out, n, "%.100s", line + 16);
|
||||
else if (strlen(line) >= 12)
|
||||
snprintf(out, n, "detached at %.12s", line);
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
char *sl = strrchr(dir, '/');
|
||||
if (!sl || sl == dir)
|
||||
return;
|
||||
*sl = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void os_label(char *out, size_t n) {
|
||||
#ifdef _WIN32
|
||||
snprintf(out, n, "Windows");
|
||||
#else
|
||||
struct utsname u;
|
||||
if (uname(&u) == 0)
|
||||
snprintf(out, n, "%s %s %s", u.sysname, u.release, u.machine);
|
||||
else
|
||||
snprintf(out, n, "Unix");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void put_env(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
char osl[256], br[128], day[32];
|
||||
os_label(osl, sizeof(osl));
|
||||
time_t now = time(NULL);
|
||||
struct tm tmv;
|
||||
os_localtime(&now, &tmv);
|
||||
strftime(day, sizeof(day), "%Y-%m-%d", &tmv);
|
||||
git_branch(cfg->workspace_root, br, sizeof(br));
|
||||
cst_buf_printf(b, "\nEnvironment:\n- OS: %s; date: %s\n- Workspace: %s%s%s%s\n",
|
||||
osl, day, cfg->workspace_root[0] ? cfg->workspace_root : ".",
|
||||
br[0] ? " (git: " : " (not a git repo", br,
|
||||
")");
|
||||
const char *ap = cfg->auto_approve ? "yolo (no confirmations)"
|
||||
: cfg->approval == CST_APPROVAL_ASK
|
||||
? "ask (the user confirms writes and commands)"
|
||||
: cfg->approval == CST_APPROVAL_AUTO
|
||||
? "auto (edits run; risky commands need confirmation)"
|
||||
: "yolo (no confirmations)";
|
||||
if (tool_on(cfg, "run_shell")) {
|
||||
int sb = cst_sandbox_probe();
|
||||
cst_buf_printf(b, "- Approval: %s; shell sandbox: %s\n", ap,
|
||||
sb == CST_SANDBOX_NONE ? "none" : cst_sandbox_name(sb));
|
||||
} else {
|
||||
cst_buf_printf(b, "- Approval: %s\n", ap);
|
||||
}
|
||||
}
|
||||
|
||||
char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
|
||||
const char *project_notes) {
|
||||
int minimal = !cfg || cfg->tool_profile == CST_TOOLS_MINIMAL;
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, PROMPT_BASE);
|
||||
cst_buf_puts(&b, minimal ? PROMPT_BASE : DEF_INTRO);
|
||||
if (cst_protocol_is_native(p))
|
||||
cst_buf_puts(&b, SEC_NATIVE);
|
||||
cst_buf_puts(&b, minimal ? SEC_NATIVE
|
||||
: "Call tools through the native "
|
||||
"function-calling interface.\n");
|
||||
if (minimal) {
|
||||
cst_buf_puts(&b, PROMPT_TOOLS);
|
||||
} else {
|
||||
char *tools = cst_tools_prompt(cfg);
|
||||
cst_buf_puts(&b, tools ? tools : "");
|
||||
free(tools);
|
||||
/* native requests carry the descriptions in tools[] already */
|
||||
if (!cst_protocol_is_native(p)) {
|
||||
char *tools = cst_tools_prompt(cfg);
|
||||
cst_buf_puts(&b, tools ? tools : "");
|
||||
free(tools);
|
||||
}
|
||||
put_workflow(&b, cfg);
|
||||
put_tool_use(&b, cfg);
|
||||
put_safety(&b, cfg);
|
||||
}
|
||||
switch (p) {
|
||||
case CST_PROTO_CTL:
|
||||
@@ -241,12 +382,19 @@ char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
|
||||
}
|
||||
if (!minimal && cst_protocol_is_ctl(p))
|
||||
cst_buf_puts(&b, SEC_CTL_NUMBERS);
|
||||
if (cfg && cfg->plan_mode)
|
||||
cst_buf_puts(&b, "\nPLAN MODE: you may only read and research (no file "
|
||||
"changes, no shell). Investigate, then answer with a "
|
||||
"concrete step-by-step plan: files to touch, what to "
|
||||
"change, how to verify. The user switches to Build mode "
|
||||
"to carry it out.\n");
|
||||
/* per-workspace, rarely changing */
|
||||
if (!minimal && tool_on(cfg, "skill")) {
|
||||
char *ix = cst_skills_index(cfg->workspace_root);
|
||||
if (ix)
|
||||
cst_buf_puts(&b, ix);
|
||||
free(ix);
|
||||
}
|
||||
if (!minimal && tool_on(cfg, "task")) {
|
||||
char *ix = cst_subagent_index(cfg->workspace_root);
|
||||
if (ix)
|
||||
cst_buf_puts(&b, ix);
|
||||
free(ix);
|
||||
}
|
||||
if (project_notes && project_notes[0]) {
|
||||
size_t n = strlen(project_notes);
|
||||
if (n > PROJECT_NOTES_MAX)
|
||||
@@ -256,6 +404,29 @@ char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
|
||||
cst_buf_append(&b, project_notes, n);
|
||||
cst_buf_puts(&b, "\n");
|
||||
}
|
||||
/* per-session facts last */
|
||||
if (!minimal)
|
||||
put_env(&b, cfg);
|
||||
if (!minimal && cfg->depth > 0) {
|
||||
cst_buf_puts(&b, "\nSUB-AGENT: another agent delegated one task to you "
|
||||
"with the task tool. It sees none of your tool calls or "
|
||||
"outputs, only your final message, and the user is not "
|
||||
"watching this conversation (you cannot ask questions). "
|
||||
"Work autonomously, then end with one self-contained "
|
||||
"report: what you found or changed, with file paths "
|
||||
"and line numbers, and anything left open.\n");
|
||||
if (cfg->role_prompt && cfg->role_prompt[0]) {
|
||||
cst_buf_puts(&b, "\nRole instructions:\n");
|
||||
cst_buf_puts(&b, cfg->role_prompt);
|
||||
cst_buf_puts(&b, "\n");
|
||||
}
|
||||
}
|
||||
if (cfg && cfg->plan_mode)
|
||||
cst_buf_puts(&b, "\nPLAN MODE: you may only read and research (no file "
|
||||
"changes, no shell). Investigate, then answer with a "
|
||||
"concrete step-by-step plan: files to touch, what to "
|
||||
"change, how to verify. The user switches to Build mode "
|
||||
"to carry it out.\n");
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
@@ -285,6 +456,8 @@ static const char NUDGE_FINAL[] =
|
||||
/* Session keeps tool output up to this size; the wire cap is separate. */
|
||||
#define TOOL_OUT_CAP (48 * 1024)
|
||||
#define READ_FP_MAX 256
|
||||
#define FAIL_FP_MAX 32
|
||||
#define PAR_MAX 4
|
||||
#define MAX_CTL_RETRIES 3
|
||||
|
||||
/* --- agent --------------------------------------------------------------- */
|
||||
@@ -302,6 +475,14 @@ struct cst_agent {
|
||||
unsigned write_epoch;
|
||||
read_fp_t reads[READ_FP_MAX];
|
||||
int nreads;
|
||||
/* this user turn: failing calls (hash -> count) and applied writes */
|
||||
struct {
|
||||
uint64_t hash;
|
||||
int count;
|
||||
} fails[FAIL_FP_MAX];
|
||||
int nfails;
|
||||
read_fp_t writes[FAIL_FP_MAX];
|
||||
int nwrites;
|
||||
int turn_no;
|
||||
char *notes; /* AGENTS.md text, or NULL */
|
||||
char *todo; /* latest todo_write list, or NULL */
|
||||
@@ -489,6 +670,15 @@ static void set_output(pcall_t *pc, const char *s, size_t n, int code) {
|
||||
pc->exit_code = code;
|
||||
}
|
||||
|
||||
static void append_output(pcall_t *pc, const char *note) {
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_append(&b, pc->output ? pc->output : "", pc->output_len);
|
||||
cst_buf_puts(&b, note);
|
||||
free(pc->output);
|
||||
pc->output_len = b.len;
|
||||
pc->output = cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
static void emit_call_metrics(turn_t *t, const pcall_t *pc) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(o, "type", "call");
|
||||
@@ -516,11 +706,23 @@ static void undo_snapshot(cst_agent_t *a, const ctl_tool_call_t *c) {
|
||||
if (n <= 0)
|
||||
return;
|
||||
char sid[256];
|
||||
cst_agent_session_id(a, sid, sizeof(sid));
|
||||
if (a->cfg.undo_sid[0])
|
||||
snprintf(sid, sizeof(sid), "%s", a->cfg.undo_sid);
|
||||
else
|
||||
cst_agent_session_id(a, sid, sizeof(sid));
|
||||
for (int i = 0; i < n; i++)
|
||||
cst_undo_snapshot(a->cfg.workspace_root, sid, a->undo_tag, paths[i]);
|
||||
}
|
||||
|
||||
void cst_agent_undo_key(const cst_agent_t *a, char *sid, size_t sid_sz,
|
||||
char *tag, size_t tag_sz) {
|
||||
if (a->cfg.undo_sid[0])
|
||||
snprintf(sid, sid_sz, "%s", a->cfg.undo_sid);
|
||||
else
|
||||
cst_agent_session_id(a, sid, sid_sz);
|
||||
snprintf(tag, tag_sz, "%s", a->undo_tag);
|
||||
}
|
||||
|
||||
static void agent_set_todo(const char *text, void *ud) {
|
||||
turn_t *t = ud;
|
||||
free(t->a->todo);
|
||||
@@ -613,10 +815,34 @@ static void sandbox_escalate(turn_t *t, cst_tool_ctx_t *ctx,
|
||||
tr->out = cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
/* Execute (or refuse) one confirmed call. Irreversible once started. */
|
||||
static void exec_call(turn_t *t, pcall_t *pc) {
|
||||
if (pc->done)
|
||||
return;
|
||||
/* One call through approval, execution and bookkeeping. exec_begin and
|
||||
* exec_end run on the agent thread (they call hooks); exec_run may run on a
|
||||
* worker thread for read-only batches. */
|
||||
typedef struct {
|
||||
cst_tool_ctx_t ctx;
|
||||
const cst_tool_def_t *def;
|
||||
const char *cmd;
|
||||
uint64_t h;
|
||||
int run;
|
||||
int mutating;
|
||||
cst_tool_result_t tr;
|
||||
} exec_t;
|
||||
|
||||
static int fail_count(cst_agent_t *a, uint64_t h, int bump) {
|
||||
for (int i = 0; i < a->nfails; i++) {
|
||||
if (a->fails[i].hash == h)
|
||||
return bump ? ++a->fails[i].count : a->fails[i].count;
|
||||
}
|
||||
if (!bump)
|
||||
return 0;
|
||||
int slot = a->nfails < FAIL_FP_MAX ? a->nfails++ : (int)(h % FAIL_FP_MAX);
|
||||
a->fails[slot].hash = h;
|
||||
a->fails[slot].count = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void exec_begin(turn_t *t, pcall_t *pc, exec_t *x) {
|
||||
memset(x, 0, sizeof(*x));
|
||||
pc->done = 1;
|
||||
cst_agent_t *a = t->a;
|
||||
ctl_tool_call_t *c = &pc->call;
|
||||
@@ -624,22 +850,27 @@ static void exec_call(turn_t *t, pcall_t *pc) {
|
||||
char detail[512];
|
||||
args_detail(c, detail, sizeof(detail));
|
||||
const cst_tool_def_t *def = cst_tool_find(c->name);
|
||||
x->def = def;
|
||||
|
||||
cst_tool_ctx_t ctx;
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.workspace_root = a->cfg.workspace_root;
|
||||
ctx.set_todo = agent_set_todo;
|
||||
ctx.todo_ud = t;
|
||||
cst_tool_ctx_t *ctx = &x->ctx;
|
||||
ctx->workspace_root = a->cfg.workspace_root;
|
||||
ctx->compact = a->cfg.tool_profile != CST_TOOLS_MINIMAL;
|
||||
ctx->set_todo = agent_set_todo;
|
||||
ctx->todo_ud = t;
|
||||
ctx->agent = a;
|
||||
ctx->hooks = t->hooks;
|
||||
if (t->hooks && t->hooks->on_ask_user) {
|
||||
ctx.ask = agent_ask;
|
||||
ctx.ask_ud = t;
|
||||
ctx->ask = agent_ask;
|
||||
ctx->ask_ud = t;
|
||||
}
|
||||
|
||||
if (a->cancel) {
|
||||
const char *msg = "error: cancelled by the user before it ran";
|
||||
set_output(pc, msg, strlen(msg), 130);
|
||||
pc->status = PC_DENIED;
|
||||
} else if (!def || !cst_tool_active(&a->cfg, def)) {
|
||||
return;
|
||||
}
|
||||
if (!def || !cst_tool_active(&a->cfg, def)) {
|
||||
char *names = cst_tools_names(&a->cfg);
|
||||
char msg[768];
|
||||
snprintf(msg, sizeof(msg), "error: unknown tool \"%.64s\" (allowed: %s)",
|
||||
@@ -648,99 +879,151 @@ static void exec_call(turn_t *t, pcall_t *pc) {
|
||||
set_output(pc, msg, strlen(msg), 1);
|
||||
pc->status = PC_UNKNOWN;
|
||||
hook_status(t, "unknown tool");
|
||||
} else if (pc->invalid) {
|
||||
return;
|
||||
}
|
||||
if (pc->invalid) {
|
||||
const char *msg = "error: tool arguments are not a valid JSON object (or "
|
||||
"exceed 1 MiB); nothing was executed";
|
||||
set_output(pc, msg, strlen(msg), 1);
|
||||
pc->status = PC_INVALID;
|
||||
} else {
|
||||
int mutating = def->perm == CST_TP_SHELL || def->perm == CST_TP_WRITE;
|
||||
uint64_t h = call_hash(c);
|
||||
int dup = 0;
|
||||
if (def->perm == CST_TP_READ) {
|
||||
for (int i = 0; i < a->nreads; i++) {
|
||||
if (a->reads[i].hash == h && a->reads[i].epoch == a->write_epoch) {
|
||||
dup = 1;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* a sub-agent that may write changes files like a write tool does */
|
||||
x->mutating = def->perm == CST_TP_SHELL || def->perm == CST_TP_WRITE ||
|
||||
(strcmp(def->name, "task") == 0 &&
|
||||
!(def->parallel && def->parallel(c, def->ud)));
|
||||
uint64_t h = call_hash(c);
|
||||
x->h = h;
|
||||
int dup = 0;
|
||||
/* plugin and MCP results can change between identical calls */
|
||||
int external = def->owner && strncmp(def->owner, "builtin:", 8) != 0;
|
||||
if (def->perm == CST_TP_READ && !external) {
|
||||
for (int i = 0; i < a->nreads; i++) {
|
||||
if (a->reads[i].hash == h && a->reads[i].epoch == a->write_epoch) {
|
||||
dup = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int allow = 1, ask = 0;
|
||||
const char *cmd = def->perm == CST_TP_SHELL ? tool_cmd(c) : NULL;
|
||||
if (!dup && needs_confirm(&a->cfg, def->perm) && !a->cfg.auto_approve &&
|
||||
!cst_perms_tool_allowed(a->perms, c->name) &&
|
||||
!(cmd && cst_perms_shell_allowed(a->perms, cmd))) {
|
||||
ask = 1;
|
||||
if (a->cfg.approval == CST_APPROVAL_AUTO) {
|
||||
if (def->perm == CST_TP_WRITE) {
|
||||
} else if (def->perm == CST_TP_WRITE && ctx->compact) {
|
||||
/* the same write already applied and nothing changed since */
|
||||
for (int i = 0; i < a->nwrites; i++) {
|
||||
if (a->writes[i].hash == h && a->writes[i].epoch == a->write_epoch) {
|
||||
dup = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
int allow = 1, ask = 0;
|
||||
const char *cmd = def->perm == CST_TP_SHELL ? tool_cmd(c) : NULL;
|
||||
x->cmd = cmd;
|
||||
if (!dup && needs_confirm(&a->cfg, def->perm) && !a->cfg.auto_approve &&
|
||||
!cst_perms_tool_allowed(a->perms, c->name) &&
|
||||
!(cmd && cst_perms_shell_allowed(a->perms, cmd))) {
|
||||
ask = 1;
|
||||
if (a->cfg.approval == CST_APPROVAL_AUTO) {
|
||||
if (def->perm == CST_TP_WRITE) {
|
||||
ask = 0;
|
||||
} else if (def->perm == CST_TP_SHELL) {
|
||||
if (cst_sandbox_probe() != CST_SANDBOX_NONE) {
|
||||
ctx->sandbox = 1;
|
||||
ask = 0;
|
||||
} else if (cmd && cst_shell_is_safe(cmd)) {
|
||||
ask = 0;
|
||||
} else if (def->perm == CST_TP_SHELL) {
|
||||
if (cst_sandbox_probe() != CST_SANDBOX_NONE) {
|
||||
ctx.sandbox = 1;
|
||||
ask = 0;
|
||||
} else if (cmd && cst_shell_is_safe(cmd)) {
|
||||
ask = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ask) {
|
||||
if (t->hooks && t->hooks->on_permission) {
|
||||
char *preview = NULL;
|
||||
if (def->preview)
|
||||
cst_tool_preview(&ctx, c, &preview);
|
||||
else if (cmd)
|
||||
preview = strdup(cmd);
|
||||
int ans = t->hooks->on_permission(
|
||||
c->name, detail[0] ? detail : c->name, preview,
|
||||
t->hooks->userdata);
|
||||
free(preview);
|
||||
allow = ans != CST_PERM_DENY;
|
||||
if (ans == CST_PERM_ALWAYS_TOOL) {
|
||||
cst_perms_allow_tool(a->perms, c->name);
|
||||
} else if (ans == CST_PERM_ALWAYS_PREFIX && cmd) {
|
||||
char pre[128];
|
||||
cst_perms_suggest_prefix(cmd, pre, sizeof(pre));
|
||||
if (pre[0])
|
||||
cst_perms_allow_prefix(a->perms, pre);
|
||||
}
|
||||
} else {
|
||||
allow = 0;
|
||||
}
|
||||
if (ask) {
|
||||
if (t->hooks && t->hooks->on_permission) {
|
||||
char *preview = NULL;
|
||||
if (def->preview)
|
||||
cst_tool_preview(ctx, c, &preview);
|
||||
else if (cmd)
|
||||
preview = strdup(cmd);
|
||||
int ans = t->hooks->on_permission(c->name, detail[0] ? detail : c->name,
|
||||
preview, t->hooks->userdata);
|
||||
free(preview);
|
||||
allow = ans != CST_PERM_DENY;
|
||||
if (ans == CST_PERM_ALWAYS_TOOL) {
|
||||
cst_perms_allow_tool(a->perms, c->name);
|
||||
} else if (ans == CST_PERM_ALWAYS_PREFIX && cmd) {
|
||||
char pre[128];
|
||||
cst_perms_suggest_prefix(cmd, pre, sizeof(pre));
|
||||
if (pre[0])
|
||||
cst_perms_allow_prefix(a->perms, pre);
|
||||
}
|
||||
}
|
||||
if (dup) {
|
||||
const char *msg =
|
||||
strcmp(c->name, "read_file") == 0
|
||||
? "note: this exact read_file already ran earlier in this turn "
|
||||
"and no file changed since; reuse that output."
|
||||
: "note: this exact call already ran earlier in this turn and "
|
||||
"no file changed since; reuse that output.";
|
||||
set_output(pc, msg, strlen(msg), 0);
|
||||
pc->status = PC_DUP;
|
||||
hook_status(t, "skip duplicate read");
|
||||
} else if (!allow) {
|
||||
set_output(pc, "denied by user", 14, 1);
|
||||
pc->status = PC_DENIED;
|
||||
hook_tool(t, "denied", c, detail);
|
||||
} else {
|
||||
hook_tool(t, "run", c, NULL);
|
||||
if (def->perm == CST_TP_WRITE)
|
||||
undo_snapshot(a, c);
|
||||
cst_tool_result_t tr = {0};
|
||||
cst_tool_run(&ctx, c, &tr);
|
||||
if (ctx.sandbox && cmd &&
|
||||
cst_sandbox_blocked(tr.out, tr.out_len, tr.exit_code))
|
||||
sandbox_escalate(t, &ctx, c, cmd, &tr);
|
||||
set_output(pc, tr.out ? tr.out : "", tr.out_len, tr.exit_code);
|
||||
cst_tool_result_free(&tr);
|
||||
if (mutating)
|
||||
a->write_epoch++;
|
||||
else if (pc->exit_code == 0 && def->perm == CST_TP_READ) {
|
||||
int slot = a->nreads < READ_FP_MAX ? a->nreads++ : (int)(h % READ_FP_MAX);
|
||||
a->reads[slot].hash = h;
|
||||
a->reads[slot].epoch = a->write_epoch;
|
||||
allow = 0;
|
||||
}
|
||||
}
|
||||
if (dup == 2) {
|
||||
const char *msg = "note: this exact edit was already applied earlier in "
|
||||
"this turn and nothing changed since; it was not run "
|
||||
"again. Re-read the file if unsure of its state.";
|
||||
set_output(pc, msg, strlen(msg), 0);
|
||||
pc->status = PC_DUP;
|
||||
hook_status(t, "skip repeated edit");
|
||||
} else if (dup) {
|
||||
const char *msg =
|
||||
strcmp(c->name, "read_file") == 0
|
||||
? "note: this exact read_file already ran earlier in this turn "
|
||||
"and no file changed since; reuse that output."
|
||||
: "note: this exact call already ran earlier in this turn and "
|
||||
"no file changed since; reuse that output.";
|
||||
set_output(pc, msg, strlen(msg), 0);
|
||||
pc->status = PC_DUP;
|
||||
hook_status(t, "skip duplicate read");
|
||||
} else if (!allow) {
|
||||
set_output(pc, "denied by user", 14, 1);
|
||||
pc->status = PC_DENIED;
|
||||
hook_tool(t, "denied", c, detail);
|
||||
} else {
|
||||
hook_tool(t, "run", c, NULL);
|
||||
if (def->perm == CST_TP_WRITE)
|
||||
undo_snapshot(a, c);
|
||||
x->run = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void exec_run(exec_t *x, const ctl_tool_call_t *c) {
|
||||
cst_tool_run(&x->ctx, c, &x->tr);
|
||||
}
|
||||
|
||||
static void exec_end(turn_t *t, pcall_t *pc, exec_t *x) {
|
||||
cst_agent_t *a = t->a;
|
||||
ctl_tool_call_t *c = &pc->call;
|
||||
if (x->run) {
|
||||
if (x->ctx.sandbox && x->cmd &&
|
||||
cst_sandbox_blocked(x->tr.out, x->tr.out_len, x->tr.exit_code))
|
||||
sandbox_escalate(t, &x->ctx, c, x->cmd, &x->tr);
|
||||
cst_tool_result_t *tr = &x->tr;
|
||||
if (x->ctx.compact) {
|
||||
if (tr->exit_code != 0 && fail_count(a, x->h, 1) >= 2) {
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_append(&b, tr->out ? tr->out : "", tr->out_len);
|
||||
cst_buf_printf(&b, "\n[cst: this exact call has now failed %d times "
|
||||
"in this turn. Do not repeat it: read the error, "
|
||||
"inspect the code, and change approach.]",
|
||||
fail_count(a, x->h, 0));
|
||||
free(tr->out);
|
||||
tr->out_len = b.len;
|
||||
tr->out = cst_buf_steal(&b);
|
||||
}
|
||||
}
|
||||
set_output(pc, tr->out ? tr->out : "", tr->out_len, tr->exit_code);
|
||||
cst_tool_result_free(tr);
|
||||
if (x->mutating)
|
||||
a->write_epoch++;
|
||||
if (x->def->perm == CST_TP_WRITE && pc->exit_code == 0) {
|
||||
int slot = a->nwrites < FAIL_FP_MAX ? a->nwrites++
|
||||
: (int)(x->h % FAIL_FP_MAX);
|
||||
a->writes[slot].hash = x->h;
|
||||
a->writes[slot].epoch = a->write_epoch;
|
||||
} else if (pc->exit_code == 0 && x->def->perm == CST_TP_READ) {
|
||||
int slot = a->nreads < READ_FP_MAX ? a->nreads++ : (int)(x->h % READ_FP_MAX);
|
||||
a->reads[slot].hash = x->h;
|
||||
a->reads[slot].epoch = a->write_epoch;
|
||||
}
|
||||
}
|
||||
pc->t_exec_end = rel_ms(t);
|
||||
if (t->hooks && t->hooks->on_tool_result)
|
||||
@@ -756,6 +1039,82 @@ static void exec_call(turn_t *t, pcall_t *pc) {
|
||||
emit_call_metrics(t, pc);
|
||||
}
|
||||
|
||||
/* Execute (or refuse) one confirmed call. Irreversible once started. */
|
||||
static void exec_call(turn_t *t, pcall_t *pc) {
|
||||
if (pc->done)
|
||||
return;
|
||||
exec_t x;
|
||||
exec_begin(t, pc, &x);
|
||||
if (x.run)
|
||||
exec_run(&x, &pc->call);
|
||||
exec_end(t, pc, &x);
|
||||
}
|
||||
|
||||
/* Read-only calls that may share a parallel batch. */
|
||||
static int parallel_ok(const turn_t *t, const pcall_t *pc) {
|
||||
if (pc->done || pc->invalid)
|
||||
return 0;
|
||||
const cst_tool_def_t *def = cst_tool_find(pc->call.name);
|
||||
if (!def || !cst_tool_active(&t->a->cfg, def))
|
||||
return 0;
|
||||
return def->perm == CST_TP_READ ||
|
||||
(def->parallel && def->parallel(&pc->call, def->ud));
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
exec_t *x;
|
||||
const ctl_tool_call_t *c;
|
||||
volatile int *cancel;
|
||||
} par_job_t;
|
||||
|
||||
static void par_main(void *p) {
|
||||
par_job_t *j = p;
|
||||
/* not reset after: the job may run inline on the agent thread */
|
||||
os_set_cancel_flag(j->cancel);
|
||||
exec_run(j->x, j->c);
|
||||
}
|
||||
|
||||
static void exec_parallel(turn_t *t, int from, int to) {
|
||||
exec_t xs[PAR_MAX];
|
||||
par_job_t jobs[PAR_MAX];
|
||||
void *args[PAR_MAX];
|
||||
int n = 0;
|
||||
for (int i = from; i < to; i++)
|
||||
exec_begin(t, &t->calls[i], &xs[i - from]);
|
||||
for (int i = from; i < to; i++) {
|
||||
if (!xs[i - from].run)
|
||||
continue;
|
||||
jobs[n].x = &xs[i - from];
|
||||
jobs[n].c = &t->calls[i].call;
|
||||
jobs[n].cancel = &t->a->cancel;
|
||||
args[n] = &jobs[n];
|
||||
n++;
|
||||
}
|
||||
if (n > 1)
|
||||
hook_status(t, "running reads in parallel");
|
||||
os_run_parallel(par_main, args, n);
|
||||
for (int i = from; i < to; i++)
|
||||
exec_end(t, &t->calls[i], &xs[i - from]);
|
||||
}
|
||||
|
||||
/* Calls left after the stream: consecutive read-only calls run together. */
|
||||
static void exec_pending(turn_t *t) {
|
||||
int par = t->a->cfg.tool_profile != CST_TOOLS_MINIMAL;
|
||||
for (int i = 0; i < t->ncalls;) {
|
||||
int j = i;
|
||||
while (par && j < t->ncalls && j - i < PAR_MAX &&
|
||||
parallel_ok(t, &t->calls[j]))
|
||||
j++;
|
||||
if (j - i >= 2) {
|
||||
exec_parallel(t, i, j);
|
||||
i = j;
|
||||
} else {
|
||||
exec_call(t, &t->calls[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- stream routing ------------------------------------------------------ */
|
||||
|
||||
static void on_text(turn_t *t, const char *s, size_t n) {
|
||||
@@ -1061,6 +1420,59 @@ static int ctx_window(void) {
|
||||
return CST_AGENT_CTX_WINDOW;
|
||||
}
|
||||
|
||||
/* Big tool outputs from turns this far back go out as a one-line stub (the
|
||||
* session file keeps them whole). */
|
||||
#define STUB_TURNS 3
|
||||
#define STUB_MIN (2 * 1024)
|
||||
|
||||
/* "path"/"pattern"/"cmd"/"url" argument of call id in a tool_calls array. */
|
||||
static void call_hint(const cJSON *calls, const char *id, char *out,
|
||||
size_t n) {
|
||||
out[0] = 0;
|
||||
const cJSON *tc = NULL;
|
||||
cJSON_ArrayForEach(tc, calls) {
|
||||
const cJSON *cid = cJSON_GetObjectItemCaseSensitive(tc, "id");
|
||||
if (!cJSON_IsString(cid) || strcmp(cid->valuestring, id) != 0)
|
||||
continue;
|
||||
const cJSON *fn = cJSON_GetObjectItemCaseSensitive(tc, "function");
|
||||
const cJSON *ar = cJSON_GetObjectItemCaseSensitive(fn, "arguments");
|
||||
cJSON *args = cJSON_IsString(ar) ? cJSON_Parse(ar->valuestring) : NULL;
|
||||
static const char *const keys[] = {"path", "pattern", "cmd", "command",
|
||||
"url"};
|
||||
for (size_t k = 0; k < sizeof(keys) / sizeof(keys[0]); k++) {
|
||||
const cJSON *v = cJSON_GetObjectItemCaseSensitive(args, keys[k]);
|
||||
if (cJSON_IsString(v)) {
|
||||
size_t l = strlen(v->valuestring);
|
||||
size_t keep = cst_utf8_floor(v->valuestring, l > 80 ? 80 : l);
|
||||
snprintf(out, n, " %.*s%s", (int)keep, v->valuestring,
|
||||
keep < l ? "…" : "");
|
||||
for (char *q = out; *q; q++)
|
||||
if (*q == '\n')
|
||||
*q = ' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(args);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static char *stub_text(const char *name, const char *hint, const char *body,
|
||||
size_t n) {
|
||||
size_t lines = 0;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
lines += body[i] == '\n';
|
||||
if (n && body[n - 1] != '\n')
|
||||
lines++;
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b,
|
||||
"[output elided: %s%s returned %zu line%s (%.1f KB) in an "
|
||||
"earlier turn; call it again if you still need it]",
|
||||
name && name[0] ? name : "tool", hint, lines,
|
||||
lines == 1 ? "" : "s", (double)n / 1024.0);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
static int build_request_msgs(cst_agent_t *a, cst_protocol_t proto,
|
||||
const char *nudge, req_msgs_t *r) {
|
||||
memset(r, 0, sizeof(*r));
|
||||
@@ -1084,14 +1496,37 @@ static int build_request_msgs(cst_agent_t *a, cst_protocol_t proto,
|
||||
int skip = nonsys > ctx_window() ? nonsys - ctx_window() : 0;
|
||||
int text_proto = cst_protocol_is_text(proto);
|
||||
int leading = 1;
|
||||
/* real user messages after each index (drives stubbing) */
|
||||
int *later = NULL;
|
||||
if (a->cfg.tool_profile != CST_TOOLS_MINIMAL && n > 0) {
|
||||
later = calloc((size_t)n, sizeof(int));
|
||||
int cnt = 0;
|
||||
for (int i = n - 1; later && i >= 0; i--) {
|
||||
later[i] = cnt;
|
||||
const cst_message_t *m = cst_session_get(a->session, i);
|
||||
if (m->role == CST_ROLE_USER && !m->tool_call_id[0])
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
cJSON *calls = NULL;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cst_message_t *m = cst_session_get(a->session, i);
|
||||
if (m->role == CST_ROLE_SYSTEM)
|
||||
continue;
|
||||
if (m->role == CST_ROLE_ASSISTANT && later) {
|
||||
cJSON_Delete(calls);
|
||||
calls = m->tool_calls_json && later[i] >= STUB_TURNS
|
||||
? cJSON_Parse(m->tool_calls_json)
|
||||
: NULL;
|
||||
}
|
||||
if (skip > 0) {
|
||||
skip--;
|
||||
continue;
|
||||
}
|
||||
int stub = later && later[i] >= STUB_TURNS && m->content &&
|
||||
strlen(m->content) > STUB_MIN &&
|
||||
(m->role == CST_ROLE_TOOL ||
|
||||
(m->role == CST_ROLE_USER && m->tool_call_id[0]));
|
||||
/* never start the window with orphan results */
|
||||
if (leading && (m->role == CST_ROLE_TOOL ||
|
||||
(m->role == CST_ROLE_USER && m->tool_call_id[0])))
|
||||
@@ -1111,12 +1546,52 @@ static int build_request_msgs(cst_agent_t *a, cst_protocol_t proto,
|
||||
const char *nl = strchr(body, '\n');
|
||||
if (strncmp(body, "[tool ", 6) == 0 && nl)
|
||||
body = nl + 1;
|
||||
char *st = NULL;
|
||||
if (stub) {
|
||||
char hint[128];
|
||||
call_hint(calls, m->tool_call_id, hint, sizeof(hint));
|
||||
st = stub_text(m->tool_name, hint, body, strlen(body));
|
||||
if (st)
|
||||
body = st;
|
||||
}
|
||||
mm.role = CST_ROLE_USER;
|
||||
mm.content = own(r, result_frame(m->tool_call_id, code, body,
|
||||
strlen(body), a->cfg.tool_out_cap));
|
||||
free(st);
|
||||
} else if (stub && m->role == CST_ROLE_TOOL) {
|
||||
const char *nl = strchr(m->content, '\n');
|
||||
size_t hl = nl && strncmp(m->content, "[tool ", 6) == 0
|
||||
? (size_t)(nl - m->content) + 1
|
||||
: 0;
|
||||
char hint[128];
|
||||
call_hint(calls, m->tool_call_id, hint, sizeof(hint));
|
||||
char *st = stub_text(m->tool_name, hint, m->content + hl,
|
||||
strlen(m->content + hl));
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_append(&b, m->content, hl);
|
||||
cst_buf_puts(&b, st ? st : "");
|
||||
free(st);
|
||||
mm.content = own(r, cst_buf_steal(&b));
|
||||
} else if (stub && text_proto && strncmp(m->content, "result ", 7) == 0) {
|
||||
/* text-protocol frame: "result <id> exit=<c> <n>\n<body>\nend" */
|
||||
const char *ex = strstr(m->content, " exit=");
|
||||
const char *nl = strchr(m->content, '\n');
|
||||
size_t len = strlen(m->content);
|
||||
if (ex && nl && len >= 4 && strcmp(m->content + len - 4, "\nend") == 0 &&
|
||||
nl < m->content + len - 4) {
|
||||
const char *body = nl + 1;
|
||||
size_t bn = (size_t)(m->content + len - 4 - body);
|
||||
char *st = stub_text(m->tool_name, "", body, bn);
|
||||
if (st)
|
||||
mm.content = own(r, result_frame(m->tool_call_id, atoi(ex + 6), st,
|
||||
strlen(st), 0));
|
||||
free(st);
|
||||
}
|
||||
}
|
||||
r->msgs[r->n++] = mm;
|
||||
}
|
||||
cJSON_Delete(calls);
|
||||
free(later);
|
||||
if (nudge) {
|
||||
r->msgs[r->n].role = CST_ROLE_USER;
|
||||
r->msgs[r->n].content = (char *)nudge;
|
||||
@@ -1540,6 +2015,8 @@ int cst_agent_run_turn_ex(cst_agent_t *a, const char *user_text,
|
||||
maybe_auto_compact(a, hooks);
|
||||
a->turn_no++;
|
||||
a->nreads = 0;
|
||||
a->nfails = 0;
|
||||
a->nwrites = 0;
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
struct tm tmv;
|
||||
@@ -1548,6 +2025,8 @@ int cst_agent_run_turn_ex(cst_agent_t *a, const char *user_text,
|
||||
strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", &tmv);
|
||||
snprintf(a->undo_tag, sizeof(a->undo_tag), "%s-%d-%03d", ts, os_getpid(),
|
||||
a->turn_no % 1000);
|
||||
if (a->cfg.undo_tag[0])
|
||||
snprintf(a->undo_tag, sizeof(a->undo_tag), "%s", a->cfg.undo_tag);
|
||||
}
|
||||
os_set_cancel_flag(&a->cancel);
|
||||
const cst_protocol_t proto = a->cfg.protocol;
|
||||
@@ -1662,8 +2141,13 @@ int cst_agent_run_turn_ex(cst_agent_t *a, const char *user_text,
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < t.ncalls; i++)
|
||||
exec_call(&t, &t.calls[i]); /* no-op for eagerly executed calls */
|
||||
exec_pending(&t); /* eagerly executed calls are already done */
|
||||
if (t.ncalls > 0 && a->cfg.tool_profile != CST_TOOLS_MINIMAL &&
|
||||
a->cfg.max_steps - (step + 1) == 3)
|
||||
append_output(&t.calls[t.ncalls - 1],
|
||||
"\n[cst: 3 model calls left in this turn. Finish the "
|
||||
"essential part now, verify it, and then summarize what "
|
||||
"is done and what remains.]");
|
||||
if (a->cancel) {
|
||||
if (t.ncalls > 0)
|
||||
persist_calls(&t);
|
||||
|
||||
@@ -309,6 +309,21 @@ int cst_agent_store_delete(const char *workspace, const char *id) {
|
||||
return -1;
|
||||
if (side_path(workspace, id, ".title", p, sizeof(p)) == 0)
|
||||
remove(p);
|
||||
/* sub-agent transcripts live in <id>/ */
|
||||
if (side_path(workspace, id, "", p, sizeof(p)) == 0) {
|
||||
DIR *d = opendir(p);
|
||||
if (d) {
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
char f[OS_PATH_MAX];
|
||||
if (strncmp(de->d_name, "sub-", 4) == 0 &&
|
||||
os_path_join(f, sizeof(f), p, de->d_name) == 0)
|
||||
remove(f);
|
||||
}
|
||||
closedir(d);
|
||||
rmdir(p);
|
||||
}
|
||||
}
|
||||
char cur[128];
|
||||
if (cst_agent_store_get_current(workspace, cur, sizeof(cur)) == 0 &&
|
||||
strcmp(cur, id) == 0) {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "cst_ext.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cst_mcp.h"
|
||||
#include "cst_plugins.h"
|
||||
#include "cst_skills.h"
|
||||
#include "cst_subagent.h"
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
|
||||
static char g_report[256];
|
||||
|
||||
const char *cst_extensions_report(void) { return g_report; }
|
||||
|
||||
void cst_extensions_wait(FILE *log) {
|
||||
int ready = 0, total = 0;
|
||||
cst_mcp_counts(&ready, &total);
|
||||
if (total == 0)
|
||||
return;
|
||||
cst_mcp_wait_ready(30000);
|
||||
cst_mcp_info_t info[CST_MCP_MAX];
|
||||
int n = cst_mcp_info(info, CST_MCP_MAX);
|
||||
for (int i = 0; log && i < n; i++) {
|
||||
if (info[i].state == CST_MCP_READY)
|
||||
fprintf(log, "mcp %s: %d tool%s\n", info[i].name, info[i].ntools,
|
||||
info[i].ntools == 1 ? "" : "s");
|
||||
else if (info[i].state != CST_MCP_OFF)
|
||||
fprintf(log, "mcp %s: %s\n", info[i].name,
|
||||
info[i].error[0] ? info[i].error : "not ready");
|
||||
}
|
||||
}
|
||||
|
||||
void cst_extensions_load(const cst_agent_config_t *cfg, char *report,
|
||||
size_t report_sz) {
|
||||
if (report && report_sz)
|
||||
report[0] = 0;
|
||||
g_report[0] = 0;
|
||||
if (!cfg || cfg->tool_profile == CST_TOOLS_MINIMAL)
|
||||
return;
|
||||
const char *ws = cfg->workspace_root;
|
||||
int trusted = cst_settings_trusted(ws);
|
||||
int mcp_untrusted = 0, plugin_untrusted = 0;
|
||||
/* workspace and user mcp.json come before plugin servers: first name wins */
|
||||
cst_mcp_load_configs(ws, trusted, &mcp_untrusted);
|
||||
int np = cst_plugins_load(ws, trusted, &plugin_untrusted);
|
||||
|
||||
cst_skill_t *v = calloc(CST_SKILLS_MAX, sizeof(*v));
|
||||
int ns = v ? cst_skills_list(ws, v, CST_SKILLS_MAX) : 0;
|
||||
free(v);
|
||||
if (ns > 0)
|
||||
cst_skills_register_tool();
|
||||
if (cfg->depth == 0)
|
||||
cst_subagent_register_tool(ws);
|
||||
cst_mcp_start_all(ws);
|
||||
|
||||
cst_buf_t b = {0};
|
||||
if (ns > 0)
|
||||
cst_buf_printf(&b, "%d skill%s", ns, ns == 1 ? "" : "s");
|
||||
if (np > 0) {
|
||||
cst_plugin_info_t info[CST_PLUGINS_MAX];
|
||||
int n = cst_plugins_info(info, CST_PLUGINS_MAX), nt = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
nt += info[i].ntools;
|
||||
cst_buf_printf(&b, "%s%d plugin%s (%d tool%s)", b.len ? " · " : "", np,
|
||||
np == 1 ? "" : "s", nt, nt == 1 ? "" : "s");
|
||||
}
|
||||
int ready = 0, total = 0;
|
||||
cst_mcp_counts(&ready, &total);
|
||||
if (total > 0)
|
||||
cst_buf_printf(&b, "%s%d MCP server%s", b.len ? " · " : "", total,
|
||||
total == 1 ? "" : "s");
|
||||
if (mcp_untrusted + plugin_untrusted > 0)
|
||||
cst_buf_printf(&b, "%suntrusted workspace: /plugins trust",
|
||||
b.len ? " · " : "");
|
||||
if (report && report_sz)
|
||||
snprintf(report, report_sz, "%s", b.buf ? b.buf : "");
|
||||
snprintf(g_report, sizeof(g_report), "%s", b.buf ? b.buf : "");
|
||||
cst_buf_free(&b);
|
||||
}
|
||||
|
||||
void cst_extensions_unload(void) {
|
||||
cst_mcp_stop_all();
|
||||
cst_plugins_unload();
|
||||
cst_tool_unregister_owner("builtin:skills");
|
||||
cst_tool_unregister_owner("builtin:task");
|
||||
}
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
#include "cst_keymap.h"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static const cst_key_action_info_t INFO[CST_KA_COUNT] = {
|
||||
[CST_KA_PALETTE] = {"palette", "General", "command palette", NULL},
|
||||
[CST_KA_KEYS] = {"keys", "General", "keyboard shortcuts", "/keys"},
|
||||
[CST_KA_MODE] = {"mode", "General", "switch Build / Plan mode", NULL},
|
||||
[CST_KA_APPROVAL] = {"approval", "General", "cycle approval ask/auto/yolo",
|
||||
NULL},
|
||||
[CST_KA_NEWLINE] = {"newline", "General", "insert a newline", NULL},
|
||||
[CST_KA_REDRAW] = {"redraw", "General", "redraw the screen", NULL},
|
||||
[CST_KA_QUIT] = {"quit", "General", "quit", "/quit"},
|
||||
[CST_KA_TOGGLE_TOOL] = {"toggle_tool", "View",
|
||||
"expand/collapse the last tool output", NULL},
|
||||
[CST_KA_TOGGLE_THOUGHT] = {"toggle_thought", "View",
|
||||
"expand/collapse the last thought", NULL},
|
||||
[CST_KA_THINKING_ALL] = {"thinking", "View",
|
||||
"show/hide all thoughts by default", "/thinking"},
|
||||
[CST_KA_DETAILS] = {"details", "View", "expand/collapse all tool output",
|
||||
"/details"},
|
||||
[CST_KA_TODO] = {"todo", "View", "show/hide the task panel", NULL},
|
||||
[CST_KA_SIDEBAR] = {"sidebar", "View", "show/hide the sidebar",
|
||||
"/sidebar"},
|
||||
[CST_KA_SCROLL_UP] = {"scroll_up", "View", "scroll up half a page", NULL},
|
||||
[CST_KA_SCROLL_DOWN] = {"scroll_down", "View", "scroll down half a page",
|
||||
NULL},
|
||||
[CST_KA_LINE_UP] = {"line_up", "View", "scroll up one line", NULL},
|
||||
[CST_KA_LINE_DOWN] = {"line_down", "View", "scroll down one line", NULL},
|
||||
[CST_KA_TOP] = {"top", "View", "jump to the first message", NULL},
|
||||
[CST_KA_BOTTOM] = {"bottom", "View", "jump to the latest message", NULL},
|
||||
[CST_KA_NEW_SESSION] = {"new_session", "Session", "start a new session",
|
||||
"/new"},
|
||||
[CST_KA_SESSIONS] = {"sessions", "Session", "pick a session", "/session"},
|
||||
[CST_KA_UNDO] = {"undo", "Session", "revert the last turn's file changes",
|
||||
"/undo"},
|
||||
[CST_KA_RETRY] = {"retry", "Session", "drop the last reply and ask again",
|
||||
"/retry"},
|
||||
[CST_KA_COMPACT] = {"compact", "Session", "summarize the context",
|
||||
"/compact"},
|
||||
[CST_KA_COPY] = {"copy", "Session", "copy the last reply", "/copy"},
|
||||
[CST_KA_EXPORT] = {"export", "Session", "export the transcript",
|
||||
"/export"},
|
||||
[CST_KA_MODEL] = {"model", "Model", "pick the model", "/model"},
|
||||
[CST_KA_PROVIDER] = {"provider", "Model", "switch provider", "/provider"},
|
||||
[CST_KA_EFFORT] = {"effort", "Model", "reasoning effort", "/effort"},
|
||||
[CST_KA_AGENTS] = {"agents", "Agents", "sub-agents and roles", "/agents"},
|
||||
[CST_KA_SKILLS] = {"skills", "Agents", "skills", "/skills"},
|
||||
[CST_KA_PLUGINS] = {"plugins", "Agents", "plugins and MCP servers",
|
||||
"/plugins"},
|
||||
};
|
||||
|
||||
static const struct {
|
||||
int action;
|
||||
const char *keys;
|
||||
} DEFAULTS[] = {
|
||||
{CST_KA_PALETTE, "ctrl+p"},
|
||||
{CST_KA_KEYS, "<leader> h"},
|
||||
{CST_KA_KEYS, "f1"},
|
||||
{CST_KA_MODE, "tab"},
|
||||
{CST_KA_APPROVAL, "shift+tab"},
|
||||
{CST_KA_NEWLINE, "ctrl+n"},
|
||||
{CST_KA_NEWLINE, "alt+enter"},
|
||||
{CST_KA_REDRAW, "ctrl+l"},
|
||||
{CST_KA_QUIT, "<leader> q"},
|
||||
{CST_KA_TOGGLE_TOOL, "ctrl+o"},
|
||||
{CST_KA_TOGGLE_THOUGHT, "ctrl+r"},
|
||||
{CST_KA_THINKING_ALL, "<leader> t"},
|
||||
{CST_KA_DETAILS, "<leader> d"},
|
||||
{CST_KA_TODO, "ctrl+t"},
|
||||
{CST_KA_SIDEBAR, "<leader> s"},
|
||||
{CST_KA_SCROLL_UP, "pgup"},
|
||||
{CST_KA_SCROLL_DOWN, "pgdn"},
|
||||
{CST_KA_LINE_UP, "alt+up"},
|
||||
{CST_KA_LINE_DOWN, "alt+down"},
|
||||
{CST_KA_TOP, "ctrl+home"},
|
||||
{CST_KA_BOTTOM, "ctrl+end"},
|
||||
{CST_KA_NEW_SESSION, "<leader> n"},
|
||||
{CST_KA_SESSIONS, "<leader> l"},
|
||||
{CST_KA_UNDO, "<leader> u"},
|
||||
{CST_KA_RETRY, "<leader> r"},
|
||||
{CST_KA_COMPACT, "<leader> c"},
|
||||
{CST_KA_COPY, "<leader> y"},
|
||||
{CST_KA_EXPORT, "<leader> x"},
|
||||
{CST_KA_MODEL, "<leader> m"},
|
||||
{CST_KA_PROVIDER, "<leader> p"},
|
||||
{CST_KA_EFFORT, "<leader> e"},
|
||||
{CST_KA_AGENTS, "<leader> a"},
|
||||
{CST_KA_SKILLS, "<leader> k"},
|
||||
{CST_KA_PLUGINS, "<leader> g"},
|
||||
};
|
||||
|
||||
static const struct {
|
||||
const char *name;
|
||||
int code;
|
||||
} NAMED[] = {
|
||||
{"tab", 9},
|
||||
{"enter", 13},
|
||||
{"return", 13},
|
||||
{"esc", 27},
|
||||
{"escape", 27},
|
||||
{"space", ' '},
|
||||
{"backspace", 127},
|
||||
{"up", CST_K_UP},
|
||||
{"down", CST_K_DOWN},
|
||||
{"left", CST_K_LEFT},
|
||||
{"right", CST_K_RIGHT},
|
||||
{"home", CST_K_HOME},
|
||||
{"end", CST_K_END},
|
||||
{"pgup", CST_K_PGUP},
|
||||
{"pageup", CST_K_PGUP},
|
||||
{"pgdn", CST_K_PGDN},
|
||||
{"pagedown", CST_K_PGDN},
|
||||
{"delete", CST_K_DEL},
|
||||
{"del", CST_K_DEL},
|
||||
{"insert", CST_K_INS},
|
||||
};
|
||||
|
||||
const cst_key_action_info_t *cst_key_action_info(int action) {
|
||||
return action > CST_KA_NONE && action < CST_KA_COUNT ? &INFO[action] : NULL;
|
||||
}
|
||||
|
||||
int cst_key_action_find(const char *name) {
|
||||
for (int i = 1; name && i < CST_KA_COUNT; i++)
|
||||
if (INFO[i].name && strcmp(INFO[i].name, name) == 0)
|
||||
return i;
|
||||
return CST_KA_NONE;
|
||||
}
|
||||
|
||||
static int ieq(const char *a, const char *b, size_t n) {
|
||||
if (strlen(b) != n)
|
||||
return 0;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (tolower((unsigned char)a[i]) != b[i])
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Decode one UTF-8 code point; returns bytes used (0 on error). */
|
||||
static int utf8_one(const char *s, size_t n, int *cp) {
|
||||
const unsigned char *u = (const unsigned char *)s;
|
||||
if (n >= 1 && u[0] < 0x80) {
|
||||
*cp = u[0];
|
||||
return 1;
|
||||
}
|
||||
if (n >= 2 && (u[0] & 0xE0) == 0xC0) {
|
||||
*cp = ((u[0] & 0x1F) << 6) | (u[1] & 0x3F);
|
||||
return 2;
|
||||
}
|
||||
if (n >= 3 && (u[0] & 0xF0) == 0xE0) {
|
||||
*cp = ((u[0] & 0x0F) << 12) | ((u[1] & 0x3F) << 6) | (u[2] & 0x3F);
|
||||
return 3;
|
||||
}
|
||||
if (n >= 4 && (u[0] & 0xF8) == 0xF0) {
|
||||
*cp = ((u[0] & 0x07) << 18) | ((u[1] & 0x3F) << 12) |
|
||||
((u[2] & 0x3F) << 6) | (u[3] & 0x3F);
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_key_n(const char *s, size_t n) {
|
||||
int ctrl = 0, alt = 0, shift = 0;
|
||||
for (;;) {
|
||||
const char *plus = memchr(s, '+', n);
|
||||
if (!plus || plus == s + n - 1)
|
||||
break;
|
||||
size_t m = (size_t)(plus - s);
|
||||
if (ieq(s, "ctrl", m) || ieq(s, "c", m))
|
||||
ctrl = 1;
|
||||
else if (ieq(s, "alt", m) || ieq(s, "meta", m) || ieq(s, "m", m))
|
||||
alt = 1;
|
||||
else if (ieq(s, "shift", m) || ieq(s, "s", m))
|
||||
shift = 1;
|
||||
else
|
||||
return -1;
|
||||
n -= m + 1;
|
||||
s = plus + 1;
|
||||
}
|
||||
if (n == 0)
|
||||
return -1;
|
||||
int code = -1;
|
||||
for (size_t i = 0; i < sizeof(NAMED) / sizeof(NAMED[0]); i++)
|
||||
if (ieq(s, NAMED[i].name, n))
|
||||
code = NAMED[i].code;
|
||||
if (code < 0 && (s[0] == 'f' || s[0] == 'F') && n >= 2 && n <= 3) {
|
||||
int f = atoi(s + 1);
|
||||
if (f >= 1 && f <= 12 && isdigit((unsigned char)s[1]))
|
||||
code = CST_K_F1 + f - 1;
|
||||
}
|
||||
if (code < 0) {
|
||||
int cp = 0;
|
||||
if (utf8_one(s, n, &cp) != (int)n)
|
||||
return -1;
|
||||
code = cp;
|
||||
}
|
||||
if (shift) {
|
||||
if (code == 9)
|
||||
code = CST_K_BTAB;
|
||||
else if (code < 128 && isalpha(code))
|
||||
code = toupper(code);
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
if (ctrl) {
|
||||
if (code < 128 && isalpha(code))
|
||||
code = tolower(code) - 'a' + 1;
|
||||
else if (code == CST_K_HOME)
|
||||
code = CST_K_CTRL_HOME;
|
||||
else if (code == CST_K_END)
|
||||
code = CST_K_CTRL_END;
|
||||
else if (code == CST_K_UP)
|
||||
code = CST_K_CTRL_UP;
|
||||
else if (code == CST_K_DOWN)
|
||||
code = CST_K_CTRL_DOWN;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
if (alt) {
|
||||
if (code == CST_K_UP)
|
||||
return CST_K_ALT_UP;
|
||||
if (code == CST_K_DOWN)
|
||||
return CST_K_ALT_DOWN;
|
||||
code |= CST_KEY_ALT;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
int cst_key_parse(const char *s) {
|
||||
if (!s)
|
||||
return -1;
|
||||
while (*s == ' ')
|
||||
s++;
|
||||
size_t n = strlen(s);
|
||||
while (n && s[n - 1] == ' ')
|
||||
n--;
|
||||
return parse_key_n(s, n);
|
||||
}
|
||||
|
||||
const char *cst_key_name(int code, char *buf, size_t n) {
|
||||
const char *alt = (code & CST_KEY_ALT) ? "alt+" : "";
|
||||
int c = code & ~CST_KEY_ALT;
|
||||
static const char *const SPECIAL[] = {
|
||||
"up", "down", "left", "right", "home",
|
||||
"end", "pgup", "pgdn", "shift+tab", "delete",
|
||||
"insert", "ctrl+home", "ctrl+end", "ctrl+up", "ctrl+down",
|
||||
"alt+up", "alt+down"};
|
||||
if (c == 9)
|
||||
snprintf(buf, n, "%stab", alt);
|
||||
else if (c == 13 || c == 10)
|
||||
snprintf(buf, n, "%senter", alt);
|
||||
else if (c == 27)
|
||||
snprintf(buf, n, "%sesc", alt);
|
||||
else if (c == ' ')
|
||||
snprintf(buf, n, "%sspace", alt);
|
||||
else if (c == 127 || c == 8)
|
||||
snprintf(buf, n, "%sbackspace", alt);
|
||||
else if (c >= 1 && c <= 26)
|
||||
snprintf(buf, n, "%sctrl+%c", alt, 'a' + c - 1);
|
||||
else if (c >= CST_K_F1 && c <= CST_K_LAST)
|
||||
snprintf(buf, n, "%sf%d", alt, c - CST_K_F1 + 1);
|
||||
else if (c >= CST_KEY_SPECIAL && c < CST_K_F1)
|
||||
snprintf(buf, n, "%s%s", alt, SPECIAL[c - CST_KEY_SPECIAL]);
|
||||
else if (c >= 'A' && c <= 'Z')
|
||||
snprintf(buf, n, "%sshift+%c", alt, c - 'A' + 'a');
|
||||
else if (c < 0x80)
|
||||
snprintf(buf, n, "%s%c", alt, c);
|
||||
else {
|
||||
char u[5] = {0};
|
||||
if (c < 0x800) {
|
||||
u[0] = (char)(0xC0 | (c >> 6));
|
||||
u[1] = (char)(0x80 | (c & 0x3F));
|
||||
} else if (c < 0x10000) {
|
||||
u[0] = (char)(0xE0 | (c >> 12));
|
||||
u[1] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
u[2] = (char)(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
u[0] = (char)(0xF0 | (c >> 18));
|
||||
u[1] = (char)(0x80 | ((c >> 12) & 0x3F));
|
||||
u[2] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
u[3] = (char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
snprintf(buf, n, "%s%s", alt, u);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* "ctrl+x n", "<leader> n" or "f2" → keys; returns the count (0 on error). */
|
||||
static int parse_seq(const cst_keymap_t *km, const char *s, int keys[2]) {
|
||||
int n = 0;
|
||||
while (*s) {
|
||||
while (*s == ' ')
|
||||
s++;
|
||||
if (!*s)
|
||||
break;
|
||||
size_t w = strcspn(s, " ");
|
||||
if (n == 2)
|
||||
return 0;
|
||||
int k = ieq(s, "<leader>", w) ? km->leader : parse_key_n(s, w);
|
||||
if (k < 0)
|
||||
return 0;
|
||||
keys[n++] = k;
|
||||
s += w;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void add(cst_keymap_t *km, int action, const int keys[2], int nkeys,
|
||||
int user) {
|
||||
if (km->n >= CST_KEYMAP_MAX || nkeys < 1)
|
||||
return;
|
||||
cst_binding_t *b = &km->b[km->n++];
|
||||
b->action = action;
|
||||
b->keys[0] = keys[0];
|
||||
b->keys[1] = nkeys > 1 ? keys[1] : 0;
|
||||
b->nkeys = nkeys;
|
||||
b->user = user;
|
||||
}
|
||||
|
||||
static void build_defaults(cst_keymap_t *km) {
|
||||
km->n = 0;
|
||||
for (size_t i = 0; i < sizeof(DEFAULTS) / sizeof(DEFAULTS[0]); i++) {
|
||||
int keys[2];
|
||||
int n = parse_seq(km, DEFAULTS[i].keys, keys);
|
||||
add(km, DEFAULTS[i].action, keys, n, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void cst_keymap_defaults(cst_keymap_t *km) {
|
||||
memset(km, 0, sizeof(*km));
|
||||
km->leader = 24; /* ctrl+x */
|
||||
build_defaults(km);
|
||||
}
|
||||
|
||||
static void drop_action(cst_keymap_t *km, int action) {
|
||||
int o = 0;
|
||||
for (int i = 0; i < km->n; i++)
|
||||
if (km->b[i].action != action)
|
||||
km->b[o++] = km->b[i];
|
||||
km->n = o;
|
||||
}
|
||||
|
||||
static void errf(char *err, size_t err_sz, int *bad, const char *what,
|
||||
const char *name) {
|
||||
if (err && err_sz && !*bad)
|
||||
snprintf(err, err_sz, "keybinds.json: %s \"%.60s\"", what, name);
|
||||
*bad = 1;
|
||||
}
|
||||
|
||||
int cst_keymap_apply_json(cst_keymap_t *km, const char *json, char *err,
|
||||
size_t err_sz) {
|
||||
if (err && err_sz)
|
||||
err[0] = 0;
|
||||
cJSON *root = cJSON_Parse(json ? json : "");
|
||||
if (!cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "keybinds.json: not a JSON object");
|
||||
return -1;
|
||||
}
|
||||
int bad = 0;
|
||||
const cJSON *ld = cJSON_GetObjectItemCaseSensitive(root, "leader");
|
||||
if (cJSON_IsString(ld)) {
|
||||
int k = cst_key_parse(ld->valuestring);
|
||||
if (k > 0) {
|
||||
km->leader = k;
|
||||
build_defaults(km);
|
||||
} else {
|
||||
errf(err, err_sz, &bad, "bad leader key", ld->valuestring);
|
||||
}
|
||||
}
|
||||
const cJSON *it;
|
||||
cJSON_ArrayForEach(it, root) {
|
||||
if (!it->string || strcmp(it->string, "leader") == 0)
|
||||
continue;
|
||||
int action = cst_key_action_find(it->string);
|
||||
if (action == CST_KA_NONE) {
|
||||
errf(err, err_sz, &bad, "unknown action", it->string);
|
||||
continue;
|
||||
}
|
||||
drop_action(km, action);
|
||||
const cJSON *v;
|
||||
if (cJSON_IsString(it)) {
|
||||
if (strcmp(it->valuestring, "none") == 0)
|
||||
continue;
|
||||
int keys[2], n = parse_seq(km, it->valuestring, keys);
|
||||
if (n)
|
||||
add(km, action, keys, n, 1);
|
||||
else
|
||||
errf(err, err_sz, &bad, "bad key", it->valuestring);
|
||||
} else if (cJSON_IsArray(it)) {
|
||||
cJSON_ArrayForEach(v, it) {
|
||||
if (!cJSON_IsString(v))
|
||||
continue;
|
||||
int keys[2], n = parse_seq(km, v->valuestring, keys);
|
||||
if (n)
|
||||
add(km, action, keys, n, 1);
|
||||
else
|
||||
errf(err, err_sz, &bad, "bad key", v->valuestring);
|
||||
}
|
||||
} else {
|
||||
errf(err, err_sz, &bad, "expected a string or list for", it->string);
|
||||
}
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return bad ? -1 : 0;
|
||||
}
|
||||
|
||||
int cst_keymap_load(cst_keymap_t *km, char *err, size_t err_sz) {
|
||||
cst_keymap_defaults(km);
|
||||
if (err && err_sz)
|
||||
err[0] = 0;
|
||||
char dir[OS_PATH_MAX], path[OS_PATH_MAX];
|
||||
if (os_config_dir(dir, sizeof(dir)) != 0 ||
|
||||
os_path_join(path, sizeof(path), dir, "keybinds.json") != 0)
|
||||
return 0;
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp)
|
||||
return 0;
|
||||
char buf[16384];
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
fclose(fp);
|
||||
buf[n] = 0;
|
||||
return cst_keymap_apply_json(km, buf, err, err_sz);
|
||||
}
|
||||
|
||||
int cst_keymap_lookup(const cst_keymap_t *km, const int *seq, int n,
|
||||
int *action) {
|
||||
int prefix = 0;
|
||||
for (int i = 0; i < km->n; i++) {
|
||||
const cst_binding_t *b = &km->b[i];
|
||||
if (b->keys[0] != seq[0])
|
||||
continue;
|
||||
if (b->nkeys == n && (n == 1 || b->keys[1] == seq[1])) {
|
||||
*action = b->action;
|
||||
return CST_KM_MATCH;
|
||||
}
|
||||
if (n == 1 && b->nkeys == 2)
|
||||
prefix = 1;
|
||||
}
|
||||
return prefix ? CST_KM_PREFIX : CST_KM_NONE;
|
||||
}
|
||||
|
||||
static void seq_name(const cst_binding_t *b, char *buf, size_t n) {
|
||||
char a[32], c[32];
|
||||
cst_key_name(b->keys[0], a, sizeof(a));
|
||||
if (b->nkeys > 1)
|
||||
snprintf(buf, n, "%s %s", a, cst_key_name(b->keys[1], c, sizeof(c)));
|
||||
else
|
||||
snprintf(buf, n, "%s", a);
|
||||
}
|
||||
|
||||
void cst_keymap_describe(const cst_keymap_t *km, int action, char *buf,
|
||||
size_t n) {
|
||||
size_t o = 0;
|
||||
buf[0] = 0;
|
||||
for (int i = 0; i < km->n && o + 1 < n; i++) {
|
||||
if (km->b[i].action != action)
|
||||
continue;
|
||||
char s[80];
|
||||
seq_name(&km->b[i], s, sizeof(s));
|
||||
int w = snprintf(buf + o, n - o, "%s%s", o ? ", " : "", s);
|
||||
if (w < 0 || (size_t)w >= n - o)
|
||||
break;
|
||||
o += (size_t)w;
|
||||
}
|
||||
}
|
||||
|
||||
int cst_keymap_conflicts(const cst_keymap_t *km, char *buf, size_t n) {
|
||||
int count = 0;
|
||||
size_t o = 0;
|
||||
if (n)
|
||||
buf[0] = 0;
|
||||
for (int i = 0; i < km->n; i++) {
|
||||
const cst_binding_t *a = &km->b[i];
|
||||
for (int j = i + 1; j < km->n; j++) {
|
||||
const cst_binding_t *b = &km->b[j];
|
||||
int same = a->keys[0] == b->keys[0] && a->nkeys == b->nkeys &&
|
||||
(a->nkeys == 1 || a->keys[1] == b->keys[1]);
|
||||
int shadow = a->keys[0] == b->keys[0] && a->nkeys != b->nkeys;
|
||||
if ((!same && !shadow) || a->action == b->action)
|
||||
continue;
|
||||
count++;
|
||||
char s[80];
|
||||
seq_name(a->nkeys <= b->nkeys ? a : b, s, sizeof(s));
|
||||
if (o + 1 < n) {
|
||||
int w = snprintf(buf + o, n - o, "%s%s: %s, %s",
|
||||
o ? "\n" : "", s, INFO[a->action].name,
|
||||
INFO[b->action].name);
|
||||
if (w > 0 && (size_t)w < n - o)
|
||||
o += (size_t)w;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
+4
-3
@@ -9,6 +9,7 @@
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -477,9 +478,9 @@ static void sse_reset(sse_t *s) {
|
||||
|
||||
/* --- record / replay ----------------------------------------------------- */
|
||||
|
||||
static int g_trace_seq;
|
||||
static atomic_int g_trace_seq;
|
||||
|
||||
void cst_llm_trace_reset(void) { g_trace_seq = 0; }
|
||||
void cst_llm_trace_reset(void) { atomic_store(&g_trace_seq, 0); }
|
||||
|
||||
static void trace_path(const char *dir, int seq, const char *ext, char *out,
|
||||
size_t n) {
|
||||
@@ -575,7 +576,7 @@ int cst_llm_chat_stream(const cst_llm_request_t *req, char *err,
|
||||
return -1;
|
||||
}
|
||||
|
||||
int seq = g_trace_seq++;
|
||||
int seq = atomic_fetch_add(&g_trace_seq, 1);
|
||||
const char *rec_dir = getenv("CST_RECORD_SSE");
|
||||
const char *rep_dir = getenv("CST_REPLAY_SSE");
|
||||
if (rec_dir && rec_dir[0]) {
|
||||
|
||||
+10
@@ -1,4 +1,5 @@
|
||||
#include "cst_agent.h"
|
||||
#include "cst_ext.h"
|
||||
#include "cst_tools.h"
|
||||
#include "cst_version.h"
|
||||
#include "os_compat.h"
|
||||
@@ -540,6 +541,14 @@ int main(int argc, char **argv) {
|
||||
cfg.api_base, cfg.model, cfg.effort, cst_protocol_name(cfg.protocol),
|
||||
cst_tool_profile_name(cfg.tool_profile), cfg.max_steps);
|
||||
|
||||
{
|
||||
char ext[256];
|
||||
cst_extensions_load(&cfg, ext, sizeof(ext));
|
||||
if (ext[0] && cfg.headless)
|
||||
fprintf(stderr, "extensions: %s\n", ext);
|
||||
if (cfg.headless)
|
||||
cst_extensions_wait(stderr);
|
||||
}
|
||||
cst_agent_t *agent = cst_agent_create(&cfg);
|
||||
if (!agent) {
|
||||
fprintf(stderr, "failed to create agent (session path?)\n");
|
||||
@@ -571,6 +580,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
cst_agent_destroy(agent);
|
||||
cst_extensions_unload();
|
||||
curl_global_cleanup();
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,936 @@
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
#ifndef _XOPEN_SOURCE
|
||||
#define _XOPEN_SOURCE 700
|
||||
#endif
|
||||
|
||||
#include "cst_mcp.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#define close _close
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
#include "cst_version.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#define MCP_PEND 16
|
||||
#define MCP_INIT_TIMEOUT_MS 30000
|
||||
#define MCP_REQ_TIMEOUT_MS 60000
|
||||
#define MCP_LINE_MAX (16 * 1024 * 1024)
|
||||
#define MCP_OUT_MAX (256 * 1024)
|
||||
#define MCP_DESC_MAX 1024
|
||||
#define MCP_PROTOCOL "2025-06-18"
|
||||
|
||||
typedef struct mcp_srv mcp_srv_t;
|
||||
|
||||
typedef struct mtool {
|
||||
mcp_srv_t *srv;
|
||||
char orig[256]; /* the server's own tool name */
|
||||
struct mtool *next;
|
||||
} mtool_t;
|
||||
|
||||
struct mcp_srv {
|
||||
char name[64];
|
||||
char source[48];
|
||||
char owner[96];
|
||||
char cwd[OS_PATH_MAX];
|
||||
char command[256];
|
||||
char **argv; /* NULL-terminated */
|
||||
char **env; /* "K=V", NULL-terminated */
|
||||
int auto_all;
|
||||
char **auto_tools; /* NULL-terminated */
|
||||
int disabled;
|
||||
char err_log[OS_PATH_MAX];
|
||||
|
||||
pthread_mutex_t mu; /* state, pending */
|
||||
pthread_mutex_t wmu; /* writes to the server's stdin */
|
||||
pthread_cond_t cv;
|
||||
int state;
|
||||
char error[200];
|
||||
int ntools;
|
||||
atomic_int stopping;
|
||||
os_proc_t proc;
|
||||
int spawned;
|
||||
pthread_t reader, starter;
|
||||
int reader_on, starter_on;
|
||||
long next_id;
|
||||
struct {
|
||||
long id;
|
||||
int used, done;
|
||||
char *json;
|
||||
} pend[MCP_PEND];
|
||||
mtool_t *tools;
|
||||
};
|
||||
|
||||
static mcp_srv_t *g_srv[CST_MCP_MAX];
|
||||
static int g_nsrv;
|
||||
static char g_ws[OS_PATH_MAX];
|
||||
|
||||
/* ---------- config ---------- */
|
||||
|
||||
static void strv_free(char **v) {
|
||||
for (int i = 0; v && v[i]; i++)
|
||||
free(v[i]);
|
||||
free(v);
|
||||
}
|
||||
|
||||
/* ${NAME} -> getenv(NAME) (empty when unset). */
|
||||
static char *expand(const char *s) {
|
||||
cst_buf_t b = {0};
|
||||
while (*s) {
|
||||
const char *e;
|
||||
if (s[0] == '$' && s[1] == '{' && (e = strchr(s + 2, '}')) != NULL &&
|
||||
e - s - 2 < 128) {
|
||||
char var[130];
|
||||
memcpy(var, s + 2, (size_t)(e - s - 2));
|
||||
var[e - s - 2] = 0;
|
||||
const char *v = getenv(var);
|
||||
if (v)
|
||||
cst_buf_puts(&b, v);
|
||||
s = e + 1;
|
||||
} else {
|
||||
cst_buf_append(&b, s, 1);
|
||||
s++;
|
||||
}
|
||||
}
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
static mcp_srv_t *find_srv(const char *name) {
|
||||
for (int i = 0; i < g_nsrv; i++)
|
||||
if (strcmp(g_srv[i]->name, name) == 0)
|
||||
return g_srv[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void srv_free(mcp_srv_t *s) {
|
||||
if (!s)
|
||||
return;
|
||||
strv_free(s->argv);
|
||||
strv_free(s->env);
|
||||
strv_free(s->auto_tools);
|
||||
while (s->tools) {
|
||||
mtool_t *n = s->tools->next;
|
||||
free(s->tools);
|
||||
s->tools = n;
|
||||
}
|
||||
for (int i = 0; i < MCP_PEND; i++)
|
||||
free(s->pend[i].json);
|
||||
pthread_mutex_destroy(&s->mu);
|
||||
pthread_mutex_destroy(&s->wmu);
|
||||
pthread_cond_destroy(&s->cv);
|
||||
free(s);
|
||||
}
|
||||
|
||||
int cst_mcp_add_config_json(const char *name, const char *server_json,
|
||||
const char *cwd, const char *source) {
|
||||
if (!name || !name[0] || !server_json || g_nsrv >= CST_MCP_MAX)
|
||||
return -1;
|
||||
char sname[64];
|
||||
cst_tool_sanitize_name(name, sname, sizeof(sname));
|
||||
if (find_srv(sname))
|
||||
return -1;
|
||||
cJSON *j = cJSON_Parse(server_json);
|
||||
if (!cJSON_IsObject(j)) {
|
||||
cJSON_Delete(j);
|
||||
return -1;
|
||||
}
|
||||
mcp_srv_t *s = calloc(1, sizeof(*s));
|
||||
if (!s) {
|
||||
cJSON_Delete(j);
|
||||
return -1;
|
||||
}
|
||||
pthread_mutex_init(&s->mu, NULL);
|
||||
pthread_mutex_init(&s->wmu, NULL);
|
||||
pthread_cond_init(&s->cv, NULL);
|
||||
snprintf(s->name, sizeof(s->name), "%s", sname);
|
||||
snprintf(s->source, sizeof(s->source), "%s", source ? source : "user");
|
||||
snprintf(s->owner, sizeof(s->owner), "mcp:%s", sname);
|
||||
|
||||
const cJSON *dis = cJSON_GetObjectItemCaseSensitive(j, "disabled");
|
||||
const cJSON *cmd = cJSON_GetObjectItemCaseSensitive(j, "command");
|
||||
const cJSON *url = cJSON_GetObjectItemCaseSensitive(j, "url");
|
||||
const cJSON *type = cJSON_GetObjectItemCaseSensitive(j, "type");
|
||||
if (cJSON_IsTrue(dis)) {
|
||||
s->disabled = 1;
|
||||
snprintf(s->error, sizeof(s->error), "disabled in the config");
|
||||
}
|
||||
if (url || (cJSON_IsString(type) && strcmp(type->valuestring, "stdio"))) {
|
||||
s->state = CST_MCP_FAILED;
|
||||
snprintf(s->error, sizeof(s->error),
|
||||
"only stdio servers (\"command\") are supported");
|
||||
} else if (!cJSON_IsString(cmd) || !cmd->valuestring[0]) {
|
||||
s->state = CST_MCP_FAILED;
|
||||
snprintf(s->error, sizeof(s->error), "missing \"command\"");
|
||||
} else {
|
||||
const cJSON *args = cJSON_GetObjectItemCaseSensitive(j, "args");
|
||||
int na = cJSON_IsArray(args) ? cJSON_GetArraySize(args) : 0;
|
||||
s->argv = calloc((size_t)na + 2, sizeof(char *));
|
||||
if (s->argv) {
|
||||
s->argv[0] = expand(cmd->valuestring);
|
||||
int k = 1;
|
||||
const cJSON *a = NULL;
|
||||
cJSON_ArrayForEach(a, args) {
|
||||
if (cJSON_IsString(a))
|
||||
s->argv[k++] = expand(a->valuestring);
|
||||
else if (cJSON_IsNumber(a)) {
|
||||
char nb[32];
|
||||
snprintf(nb, sizeof(nb), "%g", a->valuedouble);
|
||||
s->argv[k++] = strdup(nb);
|
||||
}
|
||||
}
|
||||
cst_buf_t c = {0};
|
||||
for (int i = 0; s->argv[i]; i++)
|
||||
cst_buf_printf(&c, "%s%s", i ? " " : "", s->argv[i]);
|
||||
snprintf(s->command, sizeof(s->command), "%s", c.buf ? c.buf : "");
|
||||
cst_buf_free(&c);
|
||||
}
|
||||
const cJSON *env = cJSON_GetObjectItemCaseSensitive(j, "env");
|
||||
int ne = cJSON_IsObject(env) ? cJSON_GetArraySize(env) : 0;
|
||||
s->env = calloc((size_t)ne + 1, sizeof(char *));
|
||||
if (s->env) {
|
||||
int k = 0;
|
||||
const cJSON *e = NULL;
|
||||
cJSON_ArrayForEach(e, env) {
|
||||
if (!cJSON_IsString(e) || !e->string)
|
||||
continue;
|
||||
char *v = expand(e->valuestring);
|
||||
cst_buf_t kv = {0};
|
||||
cst_buf_printf(&kv, "%s=%s", e->string, v ? v : "");
|
||||
free(v);
|
||||
s->env[k++] = cst_buf_steal(&kv);
|
||||
}
|
||||
}
|
||||
}
|
||||
const cJSON *wd = cJSON_GetObjectItemCaseSensitive(j, "cwd");
|
||||
if (cJSON_IsString(wd) && wd->valuestring[0]) {
|
||||
char *w = expand(wd->valuestring);
|
||||
if (w && (w[0] == '/' || !cwd || !cwd[0]
|
||||
#ifdef _WIN32
|
||||
|| (w[0] && w[1] == ':')
|
||||
#endif
|
||||
))
|
||||
snprintf(s->cwd, sizeof(s->cwd), "%s", w);
|
||||
else if (w)
|
||||
snprintf(s->cwd, sizeof(s->cwd), "%.2000s/%.2000s", cwd, w);
|
||||
free(w);
|
||||
} else if (cwd && cwd[0]) {
|
||||
snprintf(s->cwd, sizeof(s->cwd), "%s", cwd);
|
||||
}
|
||||
const cJSON *aa = cJSON_GetObjectItemCaseSensitive(j, "autoApprove");
|
||||
if (cJSON_IsTrue(aa)) {
|
||||
s->auto_all = 1;
|
||||
} else if (cJSON_IsArray(aa)) {
|
||||
int n = cJSON_GetArraySize(aa), k = 0;
|
||||
s->auto_tools = calloc((size_t)n + 1, sizeof(char *));
|
||||
const cJSON *t = NULL;
|
||||
cJSON_ArrayForEach(t, aa) {
|
||||
if (s->auto_tools && cJSON_IsString(t))
|
||||
s->auto_tools[k++] = strdup(t->valuestring);
|
||||
}
|
||||
}
|
||||
cJSON_Delete(j);
|
||||
g_srv[g_nsrv++] = s;
|
||||
return 0;
|
||||
}
|
||||
|
||||
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, "");
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
static int load_file(const char *path, const char *cwd, const char *source,
|
||||
int count_only) {
|
||||
char *text = slurp(path, 1024 * 1024);
|
||||
if (!text)
|
||||
return 0;
|
||||
cJSON *root = cJSON_Parse(text);
|
||||
free(text);
|
||||
const cJSON *servers = cJSON_GetObjectItemCaseSensitive(root, "mcpServers");
|
||||
if (!servers)
|
||||
servers = cJSON_GetObjectItemCaseSensitive(root, "servers");
|
||||
int n = 0;
|
||||
const cJSON *sv = NULL;
|
||||
cJSON_ArrayForEach(sv, servers) {
|
||||
if (!sv->string)
|
||||
continue;
|
||||
if (count_only) {
|
||||
n++;
|
||||
continue;
|
||||
}
|
||||
char *sj = cJSON_PrintUnformatted(sv);
|
||||
if (sj && cst_mcp_add_config_json(sv->string, sj, cwd, source) == 0)
|
||||
n++;
|
||||
free(sj);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return n;
|
||||
}
|
||||
|
||||
int cst_mcp_load_configs(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(g_ws, sizeof(g_ws), "%s", ws);
|
||||
snprintf(p, sizeof(p), "%.4000s/.agent/mcp.json", ws);
|
||||
if (ws_trusted)
|
||||
n += load_file(p, ws, "workspace", 0);
|
||||
else if (untrusted)
|
||||
*untrusted = load_file(p, ws, "workspace", 1);
|
||||
}
|
||||
char cfg[OS_PATH_MAX];
|
||||
if (os_config_dir(cfg, sizeof(cfg)) == 0) {
|
||||
snprintf(p, sizeof(p), "%.4000s/mcp.json", cfg);
|
||||
n += load_file(p, ws, "user", 0);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ---------- JSON-RPC ---------- */
|
||||
|
||||
static void set_failed(mcp_srv_t *s, int state, const char *fmt, ...)
|
||||
__attribute__((format(printf, 3, 4)));
|
||||
|
||||
static void set_failed(mcp_srv_t *s, int state, const char *fmt, ...) {
|
||||
pthread_mutex_lock(&s->mu);
|
||||
s->state = state;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(s->error, sizeof(s->error), fmt, ap);
|
||||
va_end(ap);
|
||||
pthread_cond_broadcast(&s->cv);
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
}
|
||||
|
||||
/* Last non-empty line of the server's stderr log, for error messages. */
|
||||
static void stderr_tail(const mcp_srv_t *s, char *out, size_t sz) {
|
||||
out[0] = 0;
|
||||
if (!s->err_log[0])
|
||||
return;
|
||||
char *t = slurp(s->err_log, 4 * 1024 * 1024);
|
||||
if (!t)
|
||||
return;
|
||||
size_t n = strlen(t);
|
||||
while (n && (t[n - 1] == '\n' || t[n - 1] == '\r' || t[n - 1] == ' '))
|
||||
t[--n] = 0;
|
||||
char *line = strrchr(t, '\n');
|
||||
line = line ? line + 1 : t;
|
||||
if (*line)
|
||||
snprintf(out, sz, "%s", line);
|
||||
free(t);
|
||||
}
|
||||
|
||||
static int send_json(mcp_srv_t *s, cJSON *msg) {
|
||||
char *txt = cJSON_PrintUnformatted(msg);
|
||||
if (!txt)
|
||||
return -1;
|
||||
pthread_mutex_lock(&s->wmu);
|
||||
int rc = os_proc_write(&s->proc, txt, strlen(txt));
|
||||
if (rc == 0)
|
||||
rc = os_proc_write(&s->proc, "\n", 1);
|
||||
pthread_mutex_unlock(&s->wmu);
|
||||
free(txt);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void notify(mcp_srv_t *s, const char *method, cJSON *params) {
|
||||
cJSON *m = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(m, "jsonrpc", "2.0");
|
||||
cJSON_AddStringToObject(m, "method", method);
|
||||
if (params)
|
||||
cJSON_AddItemToObject(m, "params", params);
|
||||
send_json(s, m);
|
||||
cJSON_Delete(m);
|
||||
}
|
||||
|
||||
static int srv_alive(const mcp_srv_t *s) {
|
||||
return s->state == CST_MCP_STARTING || s->state == CST_MCP_READY;
|
||||
}
|
||||
|
||||
/* Send a request and wait for its response object. params is consumed.
|
||||
* NULL with err set on timeout, cancel, a dead server or a write error. */
|
||||
static cJSON *request(mcp_srv_t *s, const char *method, cJSON *params,
|
||||
int timeout_ms, char *err, size_t esz) {
|
||||
pthread_mutex_lock(&s->mu);
|
||||
int slot = -1;
|
||||
for (int i = 0; i < MCP_PEND && slot < 0; i++)
|
||||
if (!s->pend[i].used)
|
||||
slot = i;
|
||||
if (!srv_alive(s) || slot < 0) {
|
||||
snprintf(err, esz, "%s", slot < 0 ? "too many requests in flight"
|
||||
: "server is not running");
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
long id = ++s->next_id;
|
||||
s->pend[slot].used = 1;
|
||||
s->pend[slot].done = 0;
|
||||
s->pend[slot].id = id;
|
||||
s->pend[slot].json = NULL;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
|
||||
cJSON *m = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(m, "jsonrpc", "2.0");
|
||||
cJSON_AddNumberToObject(m, "id", (double)id);
|
||||
cJSON_AddStringToObject(m, "method", method);
|
||||
if (params)
|
||||
cJSON_AddItemToObject(m, "params", params);
|
||||
int wrc = send_json(s, m);
|
||||
cJSON_Delete(m);
|
||||
|
||||
double deadline = os_now_ms() + timeout_ms;
|
||||
char *json = NULL;
|
||||
int why = 0; /* 1 timeout, 2 cancel, 3 dead, 4 write */
|
||||
pthread_mutex_lock(&s->mu);
|
||||
if (wrc != 0)
|
||||
why = 4;
|
||||
while (!why && !s->pend[slot].done) {
|
||||
if (!srv_alive(s)) {
|
||||
why = 3;
|
||||
break;
|
||||
}
|
||||
if (os_cancel_requested()) {
|
||||
why = 2;
|
||||
break;
|
||||
}
|
||||
if (os_now_ms() > deadline) {
|
||||
why = 1;
|
||||
break;
|
||||
}
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_nsec += 100 * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) {
|
||||
ts.tv_sec++;
|
||||
ts.tv_nsec -= 1000000000L;
|
||||
}
|
||||
pthread_cond_timedwait(&s->cv, &s->mu, &ts);
|
||||
}
|
||||
if (s->pend[slot].done) {
|
||||
json = s->pend[slot].json;
|
||||
why = 0;
|
||||
}
|
||||
s->pend[slot].json = NULL;
|
||||
s->pend[slot].used = 0;
|
||||
char serr[200];
|
||||
snprintf(serr, sizeof(serr), "%s", s->error);
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
|
||||
if (why == 1 || why == 2) {
|
||||
cJSON *p = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(p, "requestId", (double)id);
|
||||
cJSON_AddStringToObject(p, "reason", why == 1 ? "timeout" : "cancelled");
|
||||
notify(s, "notifications/cancelled", p);
|
||||
}
|
||||
if (why) {
|
||||
if (why == 1)
|
||||
snprintf(err, esz, "no response to %s within %d s", method,
|
||||
timeout_ms / 1000);
|
||||
else if (why == 2)
|
||||
snprintf(err, esz, "cancelled");
|
||||
else if (why == 3)
|
||||
snprintf(err, esz, "server stopped%s%s", serr[0] ? ": " : "", serr);
|
||||
else
|
||||
snprintf(err, esz, "cannot write to the server");
|
||||
free(json);
|
||||
return NULL;
|
||||
}
|
||||
cJSON *resp = cJSON_Parse(json);
|
||||
free(json);
|
||||
if (!resp)
|
||||
snprintf(err, esz, "invalid JSON response");
|
||||
return resp;
|
||||
}
|
||||
|
||||
/* JSON-RPC error object -> "message (code N)". */
|
||||
static int rpc_error(const cJSON *resp, char *err, size_t esz) {
|
||||
const cJSON *e = cJSON_GetObjectItemCaseSensitive(resp, "error");
|
||||
if (!e)
|
||||
return 0;
|
||||
const cJSON *msg = cJSON_GetObjectItemCaseSensitive(e, "message");
|
||||
const cJSON *code = cJSON_GetObjectItemCaseSensitive(e, "code");
|
||||
snprintf(err, esz, "%s (code %d)",
|
||||
cJSON_IsString(msg) ? msg->valuestring : "error",
|
||||
cJSON_IsNumber(code) ? code->valueint : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void answer_server_request(mcp_srv_t *s, const cJSON *msg) {
|
||||
const cJSON *id = cJSON_GetObjectItemCaseSensitive(msg, "id");
|
||||
const cJSON *method = cJSON_GetObjectItemCaseSensitive(msg, "method");
|
||||
cJSON *r = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(r, "jsonrpc", "2.0");
|
||||
cJSON_AddItemToObject(r, "id", cJSON_Duplicate(id, 1));
|
||||
const char *m = cJSON_IsString(method) ? method->valuestring : "";
|
||||
if (strcmp(m, "ping") == 0) {
|
||||
cJSON_AddItemToObject(r, "result", cJSON_CreateObject());
|
||||
} else if (strcmp(m, "roots/list") == 0 && g_ws[0]) {
|
||||
cJSON *res = cJSON_AddObjectToObject(r, "result");
|
||||
cJSON *roots = cJSON_AddArrayToObject(res, "roots");
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
char real[OS_PATH_MAX], uri[OS_PATH_MAX + 16];
|
||||
if (os_realpath(g_ws, real, sizeof(real)) != 0)
|
||||
snprintf(real, sizeof(real), "%s", g_ws);
|
||||
snprintf(uri, sizeof(uri), "file://%s", real);
|
||||
cJSON_AddStringToObject(root, "uri", uri);
|
||||
cJSON_AddStringToObject(root, "name", "workspace");
|
||||
cJSON_AddItemToArray(roots, root);
|
||||
} else {
|
||||
cJSON *e = cJSON_AddObjectToObject(r, "error");
|
||||
cJSON_AddNumberToObject(e, "code", -32601);
|
||||
cJSON_AddStringToObject(e, "message", "method not supported by client");
|
||||
}
|
||||
send_json(s, r);
|
||||
cJSON_Delete(r);
|
||||
}
|
||||
|
||||
static void handle_line(mcp_srv_t *s, const char *line) {
|
||||
cJSON *msg = cJSON_Parse(line);
|
||||
if (!cJSON_IsObject(msg)) {
|
||||
cJSON_Delete(msg); /* servers sometimes log to stdout; ignore */
|
||||
return;
|
||||
}
|
||||
const cJSON *id = cJSON_GetObjectItemCaseSensitive(msg, "id");
|
||||
const cJSON *method = cJSON_GetObjectItemCaseSensitive(msg, "method");
|
||||
if (method) {
|
||||
if (id)
|
||||
answer_server_request(s, msg);
|
||||
} else if (cJSON_IsNumber(id)) {
|
||||
long want = (long)id->valuedouble;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
for (int i = 0; i < MCP_PEND; i++) {
|
||||
if (s->pend[i].used && !s->pend[i].done && s->pend[i].id == want) {
|
||||
s->pend[i].json = strdup(line);
|
||||
s->pend[i].done = 1;
|
||||
pthread_cond_broadcast(&s->cv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
}
|
||||
cJSON_Delete(msg);
|
||||
}
|
||||
|
||||
static void *reader_main(void *arg) {
|
||||
mcp_srv_t *s = arg;
|
||||
cst_buf_t line = {0};
|
||||
char tmp[16384];
|
||||
int overflow = 0;
|
||||
for (;;) {
|
||||
long n = os_proc_read(&s->proc, tmp, sizeof(tmp), 200);
|
||||
if (n == -2) {
|
||||
if (atomic_load(&s->stopping))
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
if (n <= 0)
|
||||
break;
|
||||
for (long i = 0; i < n; i++) {
|
||||
if (tmp[i] != '\n') {
|
||||
if (line.len < MCP_LINE_MAX)
|
||||
cst_buf_append(&line, tmp + i, 1);
|
||||
else
|
||||
overflow = 1;
|
||||
continue;
|
||||
}
|
||||
if (line.len && line.buf[line.len - 1] == '\r')
|
||||
line.buf[--line.len] = 0;
|
||||
if (line.len && !overflow)
|
||||
handle_line(s, line.buf);
|
||||
line.len = 0;
|
||||
if (line.buf)
|
||||
line.buf[0] = 0;
|
||||
overflow = 0;
|
||||
}
|
||||
}
|
||||
cst_buf_free(&line);
|
||||
if (!atomic_load(&s->stopping)) {
|
||||
char tail[160];
|
||||
stderr_tail(s, tail, sizeof(tail));
|
||||
set_failed(s, CST_MCP_EXITED, "server exited%s%s", tail[0] ? ": " : "",
|
||||
tail);
|
||||
} else {
|
||||
set_failed(s, CST_MCP_EXITED, "stopped");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---------- tools ---------- */
|
||||
|
||||
static int auto_approved(const mcp_srv_t *s, const char *tool) {
|
||||
if (s->auto_all)
|
||||
return 1;
|
||||
for (int i = 0; s->auto_tools && s->auto_tools[i]; i++)
|
||||
if (strcmp(s->auto_tools[i], tool) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void put_content(cst_buf_t *b, const cJSON *item) {
|
||||
const cJSON *type = cJSON_GetObjectItemCaseSensitive(item, "type");
|
||||
const char *t = cJSON_IsString(type) ? type->valuestring : "";
|
||||
if (b->len && b->buf[b->len - 1] != '\n')
|
||||
cst_buf_puts(b, "\n");
|
||||
if (strcmp(t, "text") == 0) {
|
||||
const cJSON *x = cJSON_GetObjectItemCaseSensitive(item, "text");
|
||||
if (cJSON_IsString(x))
|
||||
cst_buf_puts(b, x->valuestring);
|
||||
} else if (strcmp(t, "image") == 0 || strcmp(t, "audio") == 0) {
|
||||
const cJSON *mt = cJSON_GetObjectItemCaseSensitive(item, "mimeType");
|
||||
const cJSON *d = cJSON_GetObjectItemCaseSensitive(item, "data");
|
||||
cst_buf_printf(b, "[%s %s, %zu bytes base64, not shown]", t,
|
||||
cJSON_IsString(mt) ? mt->valuestring : "",
|
||||
cJSON_IsString(d) ? strlen(d->valuestring) : (size_t)0);
|
||||
} else if (strcmp(t, "resource") == 0) {
|
||||
const cJSON *r = cJSON_GetObjectItemCaseSensitive(item, "resource");
|
||||
const cJSON *x = cJSON_GetObjectItemCaseSensitive(r, "text");
|
||||
const cJSON *uri = cJSON_GetObjectItemCaseSensitive(r, "uri");
|
||||
if (cJSON_IsString(x))
|
||||
cst_buf_puts(b, x->valuestring);
|
||||
else
|
||||
cst_buf_printf(b, "[resource %s]",
|
||||
cJSON_IsString(uri) ? uri->valuestring : "");
|
||||
} else if (strcmp(t, "resource_link") == 0) {
|
||||
const cJSON *uri = cJSON_GetObjectItemCaseSensitive(item, "uri");
|
||||
const cJSON *nm = cJSON_GetObjectItemCaseSensitive(item, "name");
|
||||
cst_buf_printf(b, "[link %s%s%s]",
|
||||
cJSON_IsString(uri) ? uri->valuestring : "",
|
||||
cJSON_IsString(nm) ? " " : "",
|
||||
cJSON_IsString(nm) ? nm->valuestring : "");
|
||||
} else {
|
||||
char *raw = cJSON_PrintUnformatted(item);
|
||||
if (raw)
|
||||
cst_buf_puts(b, raw);
|
||||
free(raw);
|
||||
}
|
||||
}
|
||||
|
||||
static int set_out(cst_tool_result_t *r, int code, const char *fmt, ...)
|
||||
__attribute__((format(printf, 3, 4)));
|
||||
|
||||
static int set_out(cst_tool_result_t *r, int code, const char *fmt, ...) {
|
||||
cst_buf_t b = {0};
|
||||
char tmp[1024];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(tmp, sizeof(tmp), fmt, ap);
|
||||
va_end(ap);
|
||||
cst_buf_puts(&b, tmp);
|
||||
r->exit_code = code;
|
||||
r->out_len = b.len;
|
||||
r->out = cst_buf_steal(&b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mcp_tool_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r, void *ud) {
|
||||
(void)ctx;
|
||||
mtool_t *mt = ud;
|
||||
mcp_srv_t *s = mt->srv;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
int st = s->state;
|
||||
char serr[200];
|
||||
snprintf(serr, sizeof(serr), "%s", s->error);
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
if (st != CST_MCP_READY)
|
||||
return set_out(r, 1, "error: MCP server %s is not running%s%s", s->name,
|
||||
serr[0] ? ": " : "", serr);
|
||||
char *aj = cst_tool_args_json(cst_tool_find(c->name), c);
|
||||
cJSON *args = aj ? cJSON_Parse(aj) : NULL;
|
||||
free(aj);
|
||||
cJSON *params = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(params, "name", mt->orig);
|
||||
cJSON_AddItemToObject(params, "arguments",
|
||||
args ? args : cJSON_CreateObject());
|
||||
char err[300];
|
||||
cJSON *resp =
|
||||
request(s, "tools/call", params, MCP_REQ_TIMEOUT_MS, err, sizeof(err));
|
||||
if (!resp)
|
||||
return set_out(r, 1, "error: %s: %s", s->name, err);
|
||||
if (rpc_error(resp, err, sizeof(err))) {
|
||||
cJSON_Delete(resp);
|
||||
return set_out(r, 1, "error: %s: %s", s->name, err);
|
||||
}
|
||||
const cJSON *res = cJSON_GetObjectItemCaseSensitive(resp, "result");
|
||||
const cJSON *content = cJSON_GetObjectItemCaseSensitive(res, "content");
|
||||
cst_buf_t b = {0};
|
||||
const cJSON *it = NULL;
|
||||
cJSON_ArrayForEach(it, content) {
|
||||
put_content(&b, it);
|
||||
if (b.len > MCP_OUT_MAX)
|
||||
break;
|
||||
}
|
||||
if (!b.len) {
|
||||
const cJSON *sc = cJSON_GetObjectItemCaseSensitive(res, "structuredContent");
|
||||
char *raw = sc ? cJSON_Print(sc) : NULL;
|
||||
cst_buf_puts(&b, raw ? raw : "(no output)");
|
||||
free(raw);
|
||||
}
|
||||
if (b.len > MCP_OUT_MAX) {
|
||||
size_t cut = MCP_OUT_MAX;
|
||||
while (cut > 0 && ((unsigned char)b.buf[cut] & 0xC0) == 0x80)
|
||||
cut--;
|
||||
size_t more = b.len - cut;
|
||||
b.len = cut;
|
||||
b.buf[cut] = 0;
|
||||
cst_buf_printf(&b, "\n… [%zu more bytes cut]", more);
|
||||
}
|
||||
r->exit_code =
|
||||
cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(res, "isError")) ? 1 : 0;
|
||||
cJSON_Delete(resp);
|
||||
r->out_len = b.len;
|
||||
r->out = cst_buf_steal(&b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void register_tool(mcp_srv_t *s, const cJSON *t) {
|
||||
const cJSON *nm = cJSON_GetObjectItemCaseSensitive(t, "name");
|
||||
if (!cJSON_IsString(nm) || !nm->valuestring[0] ||
|
||||
strlen(nm->valuestring) >= sizeof(((mtool_t *)0)->orig))
|
||||
return;
|
||||
char full[340], reg[CTL_MAX_NAME];
|
||||
snprintf(full, sizeof(full), "mcp__%.63s__%.255s", s->name,
|
||||
nm->valuestring);
|
||||
cst_tool_sanitize_name(full, reg, sizeof(reg));
|
||||
mtool_t *mt = calloc(1, sizeof(*mt));
|
||||
if (!mt)
|
||||
return;
|
||||
mt->srv = s;
|
||||
snprintf(mt->orig, sizeof(mt->orig), "%s", nm->valuestring);
|
||||
const cJSON *desc = cJSON_GetObjectItemCaseSensitive(t, "description");
|
||||
const cJSON *schema = cJSON_GetObjectItemCaseSensitive(t, "inputSchema");
|
||||
char d[MCP_DESC_MAX + 8];
|
||||
snprintf(d, sizeof(d), "%s", cJSON_IsString(desc) ? desc->valuestring : "");
|
||||
if (cJSON_IsString(desc) && strlen(desc->valuestring) > MCP_DESC_MAX) {
|
||||
size_t cut = MCP_DESC_MAX;
|
||||
while (cut > 0 && ((unsigned char)d[cut] & 0xC0) == 0x80)
|
||||
cut--;
|
||||
snprintf(d + cut, sizeof(d) - cut, "…");
|
||||
}
|
||||
char *sj = cJSON_IsObject(schema)
|
||||
? cJSON_PrintUnformatted(schema)
|
||||
: strdup("{\"type\":\"object\",\"properties\":{}}");
|
||||
cst_tool_def_t def;
|
||||
memset(&def, 0, sizeof(def));
|
||||
def.name = reg;
|
||||
def.desc = d;
|
||||
def.schema_json = sj;
|
||||
def.perm = auto_approved(s, nm->valuestring) ? CST_TP_READ : CST_TP_NET;
|
||||
def.run_ex = mcp_tool_run;
|
||||
def.ud = mt;
|
||||
def.owner = s->owner;
|
||||
if (sj && cst_tool_register(&def) == 0) {
|
||||
mt->next = s->tools;
|
||||
s->tools = mt;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
s->ntools++;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
} else {
|
||||
free(mt);
|
||||
}
|
||||
free(sj);
|
||||
}
|
||||
|
||||
static void *starter_main(void *arg) {
|
||||
mcp_srv_t *s = arg;
|
||||
char err[300];
|
||||
int fd = os_mkstemp(s->err_log, sizeof(s->err_log), "cst-mcp-");
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
else
|
||||
s->err_log[0] = 0;
|
||||
const char *cwd = s->cwd[0] ? s->cwd : g_ws;
|
||||
if (os_proc_spawn(cwd, (const char *const *)s->argv,
|
||||
(const char *const *)s->env,
|
||||
s->err_log[0] ? s->err_log : NULL, &s->proc) != 0) {
|
||||
set_failed(s, CST_MCP_FAILED, "cannot start %s", s->argv[0]);
|
||||
return NULL;
|
||||
}
|
||||
s->spawned = 1;
|
||||
if (pthread_create(&s->reader, NULL, reader_main, s) != 0) {
|
||||
set_failed(s, CST_MCP_FAILED, "cannot create the reader thread");
|
||||
return NULL;
|
||||
}
|
||||
s->reader_on = 1;
|
||||
|
||||
cJSON *p = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p, "protocolVersion", MCP_PROTOCOL);
|
||||
cJSON *caps = cJSON_AddObjectToObject(p, "capabilities");
|
||||
cJSON *roots = cJSON_AddObjectToObject(caps, "roots");
|
||||
cJSON_AddFalseToObject(roots, "listChanged");
|
||||
cJSON *ci = cJSON_AddObjectToObject(p, "clientInfo");
|
||||
cJSON_AddStringToObject(ci, "name", "cst-agent");
|
||||
cJSON_AddStringToObject(ci, "version", CST_VERSION);
|
||||
cJSON *resp =
|
||||
request(s, "initialize", p, MCP_INIT_TIMEOUT_MS, err, sizeof(err));
|
||||
if (!resp || rpc_error(resp, err, sizeof(err))) {
|
||||
cJSON_Delete(resp);
|
||||
char tail[160];
|
||||
stderr_tail(s, tail, sizeof(tail));
|
||||
if (!atomic_load(&s->stopping))
|
||||
set_failed(s, CST_MCP_FAILED, "initialize: %.150s%s%.120s", err,
|
||||
tail[0] && !strstr(err, tail) ? " — " : "",
|
||||
tail[0] && !strstr(err, tail) ? tail : "");
|
||||
return NULL;
|
||||
}
|
||||
cJSON_Delete(resp);
|
||||
notify(s, "notifications/initialized", NULL);
|
||||
|
||||
char cursor[512] = "";
|
||||
for (int page = 0; page < 20; page++) {
|
||||
cJSON *lp = cJSON_CreateObject();
|
||||
if (cursor[0])
|
||||
cJSON_AddStringToObject(lp, "cursor", cursor);
|
||||
resp = request(s, "tools/list", lp, MCP_INIT_TIMEOUT_MS, err, sizeof(err));
|
||||
if (!resp || rpc_error(resp, err, sizeof(err))) {
|
||||
cJSON_Delete(resp);
|
||||
if (!atomic_load(&s->stopping))
|
||||
set_failed(s, CST_MCP_FAILED, "tools/list: %s", err);
|
||||
return NULL;
|
||||
}
|
||||
const cJSON *res = cJSON_GetObjectItemCaseSensitive(resp, "result");
|
||||
const cJSON *t = NULL;
|
||||
cJSON_ArrayForEach(t, cJSON_GetObjectItemCaseSensitive(res, "tools"))
|
||||
register_tool(s, t);
|
||||
const cJSON *nc = cJSON_GetObjectItemCaseSensitive(res, "nextCursor");
|
||||
int more = cJSON_IsString(nc) && nc->valuestring[0] &&
|
||||
strlen(nc->valuestring) < sizeof(cursor);
|
||||
if (more)
|
||||
snprintf(cursor, sizeof(cursor), "%s", nc->valuestring);
|
||||
cJSON_Delete(resp);
|
||||
if (!more)
|
||||
break;
|
||||
}
|
||||
pthread_mutex_lock(&s->mu);
|
||||
if (s->state == CST_MCP_STARTING) {
|
||||
s->state = CST_MCP_READY;
|
||||
s->error[0] = 0;
|
||||
}
|
||||
pthread_cond_broadcast(&s->cv);
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---------- lifecycle ---------- */
|
||||
|
||||
void cst_mcp_start_all(const char *ws) {
|
||||
if (ws && ws[0])
|
||||
snprintf(g_ws, sizeof(g_ws), "%s", ws);
|
||||
for (int i = 0; i < g_nsrv; i++) {
|
||||
mcp_srv_t *s = g_srv[i];
|
||||
if (s->disabled || s->state != CST_MCP_OFF || !s->argv || !s->argv[0])
|
||||
continue;
|
||||
s->state = CST_MCP_STARTING;
|
||||
if (pthread_create(&s->starter, NULL, starter_main, s) == 0)
|
||||
s->starter_on = 1;
|
||||
else
|
||||
set_failed(s, CST_MCP_FAILED, "cannot create a thread");
|
||||
}
|
||||
}
|
||||
|
||||
void cst_mcp_wait_ready(int ms) {
|
||||
double deadline = os_now_ms() + ms;
|
||||
for (;;) {
|
||||
int starting = 0;
|
||||
for (int i = 0; i < g_nsrv; i++) {
|
||||
pthread_mutex_lock(&g_srv[i]->mu);
|
||||
starting += g_srv[i]->state == CST_MCP_STARTING;
|
||||
pthread_mutex_unlock(&g_srv[i]->mu);
|
||||
}
|
||||
if (!starting || os_now_ms() > deadline)
|
||||
return;
|
||||
struct timespec ts = {0, 50 * 1000000L};
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void cst_mcp_stop_all(void) {
|
||||
for (int i = 0; i < g_nsrv; i++) {
|
||||
mcp_srv_t *s = g_srv[i];
|
||||
atomic_store(&s->stopping, 1);
|
||||
if (s->reader_on)
|
||||
pthread_join(s->reader, NULL);
|
||||
s->reader_on = 0;
|
||||
if (s->spawned)
|
||||
os_proc_kill(&s->proc);
|
||||
s->spawned = 0;
|
||||
if (s->starter_on)
|
||||
pthread_join(s->starter, NULL);
|
||||
s->starter_on = 0;
|
||||
cst_tool_unregister_owner(s->owner);
|
||||
if (s->err_log[0])
|
||||
remove(s->err_log);
|
||||
srv_free(s);
|
||||
g_srv[i] = NULL;
|
||||
}
|
||||
g_nsrv = 0;
|
||||
}
|
||||
|
||||
int cst_mcp_info(cst_mcp_info_t *out, int max) {
|
||||
int n = g_nsrv < max ? g_nsrv : max;
|
||||
for (int i = 0; i < n; i++) {
|
||||
mcp_srv_t *s = g_srv[i];
|
||||
cst_mcp_info_t *o = &out[i];
|
||||
memset(o, 0, sizeof(*o));
|
||||
snprintf(o->name, sizeof(o->name), "%s", s->name);
|
||||
snprintf(o->source, sizeof(o->source), "%s", s->source);
|
||||
snprintf(o->command, sizeof(o->command), "%s", s->command);
|
||||
pthread_mutex_lock(&s->mu);
|
||||
o->state = s->disabled ? CST_MCP_OFF : s->state;
|
||||
o->ntools = s->ntools;
|
||||
snprintf(o->error, sizeof(o->error), "%s", s->error);
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void cst_mcp_counts(int *ready, int *total) {
|
||||
int r = 0, t = 0;
|
||||
for (int i = 0; i < g_nsrv; i++) {
|
||||
if (g_srv[i]->disabled)
|
||||
continue;
|
||||
t++;
|
||||
pthread_mutex_lock(&g_srv[i]->mu);
|
||||
r += g_srv[i]->state == CST_MCP_READY;
|
||||
pthread_mutex_unlock(&g_srv[i]->mu);
|
||||
}
|
||||
if (ready)
|
||||
*ready = r;
|
||||
if (total)
|
||||
*total = t;
|
||||
}
|
||||
+378
-3
@@ -28,6 +28,7 @@
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
@@ -378,13 +379,16 @@ int os_resolve_shell(char *out, size_t out_sz) {
|
||||
#endif
|
||||
}
|
||||
|
||||
static volatile int *g_run_cancel;
|
||||
static int g_run_timeout_s;
|
||||
/* Per thread: parallel tool runs and sub-agents each watch their own flag. */
|
||||
static _Thread_local volatile int *g_run_cancel;
|
||||
static _Thread_local int g_run_timeout_s;
|
||||
|
||||
void os_set_cancel_flag(volatile int *flag) { g_run_cancel = flag; }
|
||||
|
||||
void os_set_run_timeout(int seconds) { g_run_timeout_s = seconds; }
|
||||
|
||||
int os_get_run_timeout(void) { return g_run_timeout_s; }
|
||||
|
||||
static int run_timeout_s(void) {
|
||||
if (g_run_timeout_s > 0)
|
||||
return g_run_timeout_s;
|
||||
@@ -597,6 +601,25 @@ int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
return os_run_capture(cwd, argv, max_out, r);
|
||||
}
|
||||
#else
|
||||
static void cloexec(int fd) {
|
||||
int fl = fcntl(fd, F_GETFD);
|
||||
if (fl >= 0)
|
||||
fcntl(fd, F_SETFD, fl | FD_CLOEXEC);
|
||||
}
|
||||
|
||||
/* Every pipe end is close-on-exec and pipe+fork happen under one lock, so a
|
||||
* child forked on one thread never inherits the pipes of a spawn running on
|
||||
* another (which would keep that pipe open past its process's exit). */
|
||||
static pthread_mutex_t g_spawn_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int pipe_cloexec(int fd[2]) {
|
||||
if (pipe(fd) != 0)
|
||||
return -1;
|
||||
cloexec(fd[0]);
|
||||
cloexec(fd[1]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
||||
os_run_result_t *r) {
|
||||
return os_run_capture_ex(cwd, argv, max_out, r, NULL, NULL);
|
||||
@@ -612,10 +635,14 @@ int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
memset(r, 0, sizeof(*r));
|
||||
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0)
|
||||
pthread_mutex_lock(&g_spawn_mu);
|
||||
if (pipe_cloexec(pipefd) != 0) {
|
||||
pthread_mutex_unlock(&g_spawn_mu);
|
||||
return -1;
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
pthread_mutex_unlock(&g_spawn_mu);
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return -1;
|
||||
@@ -643,6 +670,7 @@ int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
execvp(argv[0], (char *const *)argv);
|
||||
_exit(127);
|
||||
}
|
||||
pthread_mutex_unlock(&g_spawn_mu);
|
||||
setpgid(pid, pid); /* also in parent: avoid racing the child */
|
||||
close(pipefd[1]);
|
||||
size_t cap = 4096, len = 0;
|
||||
@@ -793,3 +821,350 @@ int os_chmod_private(const char *path) {
|
||||
return chmod(path, 0600);
|
||||
#endif
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
os_task_fn fn;
|
||||
void *arg;
|
||||
} os_task_t;
|
||||
|
||||
#ifdef _WIN32
|
||||
static DWORD WINAPI task_main(LPVOID p) {
|
||||
os_task_t *t = p;
|
||||
t->fn(t->arg);
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
static void *task_main(void *p) {
|
||||
os_task_t *t = p;
|
||||
t->fn(t->arg);
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
void os_run_parallel(os_task_fn fn, void *const args[], int n) {
|
||||
if (n <= 0)
|
||||
return;
|
||||
if (n == 1) {
|
||||
fn(args[0]);
|
||||
return;
|
||||
}
|
||||
os_task_t *tasks = calloc((size_t)n, sizeof(*tasks));
|
||||
#ifdef _WIN32
|
||||
HANDLE *th = calloc((size_t)n, sizeof(*th));
|
||||
#else
|
||||
pthread_t *th = calloc((size_t)n, sizeof(*th));
|
||||
char *started = calloc((size_t)n, 1);
|
||||
#endif
|
||||
if (!tasks || !th
|
||||
#ifndef _WIN32
|
||||
|| !started
|
||||
#endif
|
||||
) {
|
||||
for (int i = 0; i < n; i++)
|
||||
fn(args[i]);
|
||||
free(tasks);
|
||||
free(th);
|
||||
#ifndef _WIN32
|
||||
free(started);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
tasks[i].fn = fn;
|
||||
tasks[i].arg = args[i];
|
||||
#ifdef _WIN32
|
||||
th[i] = CreateThread(NULL, 0, task_main, &tasks[i], 0, NULL);
|
||||
if (!th[i])
|
||||
fn(args[i]);
|
||||
#else
|
||||
if (pthread_create(&th[i], NULL, task_main, &tasks[i]) == 0)
|
||||
started[i] = 1;
|
||||
else
|
||||
fn(args[i]);
|
||||
#endif
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
#ifdef _WIN32
|
||||
if (th[i]) {
|
||||
WaitForSingleObject(th[i], INFINITE);
|
||||
CloseHandle(th[i]);
|
||||
}
|
||||
#else
|
||||
if (started[i])
|
||||
pthread_join(th[i], NULL);
|
||||
#endif
|
||||
}
|
||||
#ifndef _WIN32
|
||||
free(started);
|
||||
#endif
|
||||
free(th);
|
||||
free(tasks);
|
||||
}
|
||||
|
||||
int os_cancel_requested(void) { return g_run_cancel && *g_run_cancel; }
|
||||
|
||||
volatile int *os_get_cancel_flag(void) { return g_run_cancel; }
|
||||
|
||||
#ifdef _WIN32
|
||||
static void win_quote_arg(char *out, size_t out_sz, size_t *pos,
|
||||
const char *a) {
|
||||
int need_q = !a[0] || strpbrk(a, " \t\"") != NULL;
|
||||
if (need_q && *pos + 1 < out_sz)
|
||||
out[(*pos)++] = '"';
|
||||
for (const char *p = a; *p && *pos + 2 < out_sz; p++) {
|
||||
if (*p == '"')
|
||||
out[(*pos)++] = '\\';
|
||||
out[(*pos)++] = *p;
|
||||
}
|
||||
if (need_q && *pos + 1 < out_sz)
|
||||
out[(*pos)++] = '"';
|
||||
out[*pos] = 0;
|
||||
}
|
||||
|
||||
int os_proc_spawn(const char *cwd, const char *const argv[],
|
||||
const char *const env_add[], const char *stderr_path,
|
||||
os_proc_t *p) {
|
||||
memset(p, 0, sizeof(*p));
|
||||
if (!argv || !argv[0])
|
||||
return -1;
|
||||
char cmdline[16384];
|
||||
size_t pos = 0;
|
||||
cmdline[0] = 0;
|
||||
for (int i = 0; argv[i]; i++) {
|
||||
if (i && pos + 1 < sizeof(cmdline))
|
||||
cmdline[pos++] = ' ';
|
||||
win_quote_arg(cmdline, sizeof(cmdline), &pos, argv[i]);
|
||||
}
|
||||
for (int i = 0; env_add && env_add[i]; i++)
|
||||
_putenv(env_add[i]); /* inherited by the child */
|
||||
SECURITY_ATTRIBUTES sa = {0};
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.bInheritHandle = TRUE;
|
||||
HANDLE in_r = NULL, in_w = NULL, out_r = NULL, out_w = NULL;
|
||||
if (!CreatePipe(&in_r, &in_w, &sa, 0))
|
||||
return -1;
|
||||
if (!CreatePipe(&out_r, &out_w, &sa, 0)) {
|
||||
CloseHandle(in_r);
|
||||
CloseHandle(in_w);
|
||||
return -1;
|
||||
}
|
||||
SetHandleInformation(in_w, HANDLE_FLAG_INHERIT, 0);
|
||||
SetHandleInformation(out_r, HANDLE_FLAG_INHERIT, 0);
|
||||
HANDLE err = CreateFileA(stderr_path ? stderr_path : "NUL", GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
|
||||
CREATE_ALWAYS, 0, NULL);
|
||||
STARTUPINFOA si = {0};
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdInput = in_r;
|
||||
si.hStdOutput = out_w;
|
||||
si.hStdError = err != INVALID_HANDLE_VALUE ? err : out_w;
|
||||
PROCESS_INFORMATION pi = {0};
|
||||
BOOL ok = CreateProcessA(NULL, cmdline, NULL, NULL, TRUE, CREATE_NO_WINDOW,
|
||||
NULL, cwd && cwd[0] ? cwd : NULL, &si, &pi);
|
||||
CloseHandle(in_r);
|
||||
CloseHandle(out_w);
|
||||
if (err != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(err);
|
||||
if (!ok) {
|
||||
CloseHandle(in_w);
|
||||
CloseHandle(out_r);
|
||||
return -1;
|
||||
}
|
||||
CloseHandle(pi.hThread);
|
||||
p->proc = pi.hProcess;
|
||||
p->in_w = in_w;
|
||||
p->out_r = out_r;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_proc_write(os_proc_t *p, const char *data, size_t n) {
|
||||
while (n > 0) {
|
||||
DWORD w = 0;
|
||||
if (!p->in_w || !WriteFile((HANDLE)p->in_w, data, (DWORD)n, &w, NULL) ||
|
||||
w == 0)
|
||||
return -1;
|
||||
data += w;
|
||||
n -= w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
long os_proc_read(os_proc_t *p, char *buf, size_t n, int timeout_ms) {
|
||||
if (!p->out_r)
|
||||
return -1;
|
||||
double deadline = os_now_ms() + timeout_ms;
|
||||
for (;;) {
|
||||
DWORD avail = 0;
|
||||
if (!PeekNamedPipe((HANDLE)p->out_r, NULL, 0, NULL, &avail, NULL))
|
||||
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
|
||||
if (avail > 0)
|
||||
break;
|
||||
if (os_now_ms() >= deadline)
|
||||
return -2;
|
||||
Sleep(10);
|
||||
}
|
||||
DWORD r = 0;
|
||||
if (!ReadFile((HANDLE)p->out_r, buf, (DWORD)n, &r, NULL))
|
||||
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
|
||||
return (long)r;
|
||||
}
|
||||
|
||||
void os_proc_close_stdin(os_proc_t *p) {
|
||||
if (p->in_w) {
|
||||
CloseHandle((HANDLE)p->in_w);
|
||||
p->in_w = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void os_proc_kill(os_proc_t *p) {
|
||||
os_proc_close_stdin(p);
|
||||
if (p->proc) {
|
||||
if (WaitForSingleObject((HANDLE)p->proc, 500) != WAIT_OBJECT_0)
|
||||
TerminateProcess((HANDLE)p->proc, 1);
|
||||
WaitForSingleObject((HANDLE)p->proc, 2000);
|
||||
CloseHandle((HANDLE)p->proc);
|
||||
p->proc = NULL;
|
||||
}
|
||||
if (p->out_r) {
|
||||
CloseHandle((HANDLE)p->out_r);
|
||||
p->out_r = NULL;
|
||||
}
|
||||
}
|
||||
#else
|
||||
int os_proc_spawn(const char *cwd, const char *const argv[],
|
||||
const char *const env_add[], const char *stderr_path,
|
||||
os_proc_t *p) {
|
||||
memset(p, 0, sizeof(*p));
|
||||
p->in_fd = p->out_fd = -1;
|
||||
if (!argv || !argv[0])
|
||||
return -1;
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
int in[2], out[2];
|
||||
pthread_mutex_lock(&g_spawn_mu);
|
||||
if (pipe_cloexec(in) != 0) {
|
||||
pthread_mutex_unlock(&g_spawn_mu);
|
||||
return -1;
|
||||
}
|
||||
if (pipe_cloexec(out) != 0) {
|
||||
pthread_mutex_unlock(&g_spawn_mu);
|
||||
close(in[0]);
|
||||
close(in[1]);
|
||||
return -1;
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
pthread_mutex_unlock(&g_spawn_mu);
|
||||
close(in[0]);
|
||||
close(in[1]);
|
||||
close(out[0]);
|
||||
close(out[1]);
|
||||
return -1;
|
||||
}
|
||||
if (pid == 0) {
|
||||
setpgid(0, 0);
|
||||
dup2(in[0], STDIN_FILENO);
|
||||
dup2(out[1], STDOUT_FILENO);
|
||||
int err = open(stderr_path ? stderr_path : "/dev/null",
|
||||
O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (err >= 0) {
|
||||
dup2(err, STDERR_FILENO);
|
||||
close(err);
|
||||
}
|
||||
close(in[0]);
|
||||
close(in[1]);
|
||||
close(out[0]);
|
||||
close(out[1]);
|
||||
signal(SIGPIPE, SIG_DFL);
|
||||
for (int i = 0; env_add && env_add[i]; i++)
|
||||
putenv((char *)env_add[i]);
|
||||
if (cwd && cwd[0] && chdir(cwd) != 0)
|
||||
_exit(127);
|
||||
execvp(argv[0], (char *const *)argv);
|
||||
_exit(127);
|
||||
}
|
||||
pthread_mutex_unlock(&g_spawn_mu);
|
||||
setpgid(pid, pid);
|
||||
close(in[0]);
|
||||
close(out[1]);
|
||||
p->pid = (int)pid;
|
||||
p->in_fd = in[1];
|
||||
p->out_fd = out[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_proc_write(os_proc_t *p, const char *data, size_t n) {
|
||||
while (n > 0) {
|
||||
if (p->in_fd < 0)
|
||||
return -1;
|
||||
ssize_t w = write(p->in_fd, data, n);
|
||||
if (w < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (w <= 0)
|
||||
return -1;
|
||||
data += w;
|
||||
n -= (size_t)w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
long os_proc_read(os_proc_t *p, char *buf, size_t n, int timeout_ms) {
|
||||
for (;;) {
|
||||
if (p->out_fd < 0)
|
||||
return -1;
|
||||
struct pollfd pfd = {p->out_fd, POLLIN, 0};
|
||||
int pr = poll(&pfd, 1, timeout_ms);
|
||||
if (pr < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (pr == 0)
|
||||
return -2;
|
||||
ssize_t r = read(p->out_fd, buf, n);
|
||||
if (r < 0 && errno == EINTR)
|
||||
continue;
|
||||
return (long)r;
|
||||
}
|
||||
}
|
||||
|
||||
void os_proc_close_stdin(os_proc_t *p) {
|
||||
if (p->in_fd >= 0) {
|
||||
close(p->in_fd);
|
||||
p->in_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void os_proc_kill(os_proc_t *p) {
|
||||
os_proc_close_stdin(p);
|
||||
if (p->pid > 0) {
|
||||
int status;
|
||||
/* polite first: most servers exit when stdin closes */
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (waitpid(p->pid, &status, WNOHANG) == p->pid) {
|
||||
p->pid = 0;
|
||||
break;
|
||||
}
|
||||
struct timespec ts = {0, 50 * 1000000L};
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
if (p->pid > 0) {
|
||||
kill(-p->pid, SIGTERM);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
if (waitpid(p->pid, &status, WNOHANG) == p->pid) {
|
||||
p->pid = 0;
|
||||
break;
|
||||
}
|
||||
struct timespec ts = {0, 50 * 1000000L};
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
}
|
||||
if (p->pid > 0) {
|
||||
kill(-p->pid, SIGKILL);
|
||||
waitpid(p->pid, &status, 0);
|
||||
p->pid = 0;
|
||||
}
|
||||
}
|
||||
if (p->out_fd >= 0) {
|
||||
close(p->out_fd);
|
||||
p->out_fd = -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+444
@@ -0,0 +1,444 @@
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
#ifndef _XOPEN_SOURCE
|
||||
#define _XOPEN_SOURCE 700
|
||||
#endif
|
||||
|
||||
#include "cst_plugins.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#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, "");
|
||||
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;
|
||||
#ifdef _WIN32
|
||||
(void)ctx;
|
||||
(void)c;
|
||||
(void)pt;
|
||||
r->out = strdup("error: plugin tools are not supported on Windows yet");
|
||||
r->out_len = r->out ? strlen(r->out) : 0;
|
||||
return 0;
|
||||
#else
|
||||
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;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* 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;
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(root);
|
||||
if (!d)
|
||||
return 0;
|
||||
char *names[CST_PLUGINS_MAX * 2];
|
||||
int nn = 0;
|
||||
struct dirent *de;
|
||||
while (nn < CST_PLUGINS_MAX * 2 && (de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.')
|
||||
continue;
|
||||
names[nn] = strdup(de->d_name);
|
||||
if (names[nn])
|
||||
nn++;
|
||||
}
|
||||
closedir(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]);
|
||||
}
|
||||
#else
|
||||
(void)root;
|
||||
(void)source;
|
||||
#endif
|
||||
return n;
|
||||
}
|
||||
|
||||
static int count_plugin_dirs(const char *root) {
|
||||
int n = 0;
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(root);
|
||||
if (!d)
|
||||
return 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
char pj[OS_PATH_MAX];
|
||||
if (de->d_name[0] != '.' &&
|
||||
snprintf(pj, sizeof(pj), "%s/%s/plugin.json", root, de->d_name) <
|
||||
(int)sizeof(pj) &&
|
||||
is_file(pj))
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)root;
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
+69
-1
@@ -27,7 +27,7 @@ int cst_settings_load(cst_settings_t *s) {
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp)
|
||||
return -1;
|
||||
char buf[16384];
|
||||
static _Thread_local char buf[65536];
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
fclose(fp);
|
||||
buf[n] = 0;
|
||||
@@ -42,6 +42,14 @@ int cst_settings_load(cst_settings_t *s) {
|
||||
cJSON_GetObjectItemCaseSensitive(root, "effort"));
|
||||
copy_str(s->approval, sizeof(s->approval),
|
||||
cJSON_GetObjectItemCaseSensitive(root, "approval"));
|
||||
s->show_thinking =
|
||||
cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root, "show_thinking"));
|
||||
const cJSON *tr = cJSON_GetObjectItemCaseSensitive(root, "trusted_workspaces");
|
||||
const cJSON *w;
|
||||
cJSON_ArrayForEach(w, tr) {
|
||||
if (s->ntrusted < CST_SETTINGS_MAX_TRUSTED && cJSON_IsString(w))
|
||||
copy_str(s->trusted[s->ntrusted++], sizeof(s->trusted[0]), w);
|
||||
}
|
||||
const cJSON *models = cJSON_GetObjectItemCaseSensitive(root, "models");
|
||||
const cJSON *m;
|
||||
cJSON_ArrayForEach(m, models) {
|
||||
@@ -73,6 +81,13 @@ int cst_settings_save(const cst_settings_t *s) {
|
||||
cJSON_AddStringToObject(root, "effort", s->effort);
|
||||
if (s->approval[0])
|
||||
cJSON_AddStringToObject(root, "approval", s->approval);
|
||||
if (s->show_thinking)
|
||||
cJSON_AddTrueToObject(root, "show_thinking");
|
||||
if (s->ntrusted) {
|
||||
cJSON *tr = cJSON_AddArrayToObject(root, "trusted_workspaces");
|
||||
for (int i = 0; i < s->ntrusted; i++)
|
||||
cJSON_AddItemToArray(tr, cJSON_CreateString(s->trusted[i]));
|
||||
}
|
||||
cJSON *models = cJSON_AddObjectToObject(root, "models");
|
||||
for (int i = 0; i < s->nmodels; i++)
|
||||
cJSON_AddStringToObject(models, s->models[i].provider, s->models[i].model);
|
||||
@@ -146,3 +161,56 @@ int cst_settings_remember_approval(int approval) {
|
||||
snprintf(s.approval, sizeof(s.approval), "%s", cst_approval_name(approval));
|
||||
return cst_settings_save(&s);
|
||||
}
|
||||
|
||||
int cst_settings_remember_thinking(int show_thinking) {
|
||||
cst_settings_t s;
|
||||
cst_settings_load(&s);
|
||||
s.show_thinking = show_thinking != 0;
|
||||
return cst_settings_save(&s);
|
||||
}
|
||||
|
||||
static void real_ws(const char *ws, char *out, size_t n) {
|
||||
if (!ws || os_realpath(ws, out, n) != 0)
|
||||
snprintf(out, n, "%s", ws ? ws : "");
|
||||
}
|
||||
|
||||
int cst_settings_trusted(const char *workspace) {
|
||||
char real[OS_PATH_MAX];
|
||||
real_ws(workspace, real, sizeof(real));
|
||||
cst_settings_t s;
|
||||
cst_settings_load(&s);
|
||||
for (int i = 0; i < s.ntrusted; i++)
|
||||
if (strcmp(s.trusted[i], real) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_settings_trust(const char *workspace, int on) {
|
||||
char real[OS_PATH_MAX];
|
||||
real_ws(workspace, real, sizeof(real));
|
||||
if (!real[0] || strlen(real) >= sizeof(((cst_settings_t *)0)->trusted[0]))
|
||||
return -1;
|
||||
cst_settings_t s;
|
||||
cst_settings_load(&s);
|
||||
int k = 0, found = 0;
|
||||
for (int i = 0; i < s.ntrusted; i++) {
|
||||
if (strcmp(s.trusted[i], real) == 0) {
|
||||
found = 1;
|
||||
if (!on)
|
||||
continue;
|
||||
}
|
||||
if (k != i)
|
||||
memcpy(s.trusted[k], s.trusted[i], sizeof(s.trusted[0]));
|
||||
k++;
|
||||
}
|
||||
s.ntrusted = k;
|
||||
if (on && !found) {
|
||||
if (s.ntrusted >= CST_SETTINGS_MAX_TRUSTED) {
|
||||
memmove(s.trusted[0], s.trusted[1],
|
||||
sizeof(s.trusted[0]) * (CST_SETTINGS_MAX_TRUSTED - 1));
|
||||
s.ntrusted--;
|
||||
}
|
||||
snprintf(s.trusted[s.ntrusted++], sizeof(s.trusted[0]), "%.511s", real);
|
||||
}
|
||||
return cst_settings_save(&s);
|
||||
}
|
||||
|
||||
+577
@@ -0,0 +1,577 @@
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
#ifndef _XOPEN_SOURCE
|
||||
#define _XOPEN_SOURCE 700
|
||||
#endif
|
||||
|
||||
#include "cst_skills.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
|
||||
#define SKILL_MD_MAX (256 * 1024)
|
||||
#define SKILL_FILE_MAX (256 * 1024)
|
||||
#define SKILL_LIST_FILES 60
|
||||
#define EXTRA_ROOTS 16
|
||||
|
||||
/* --- frontmatter --------------------------------------------------------- */
|
||||
|
||||
static const char *skip_bom(const char *s) {
|
||||
if ((unsigned char)s[0] == 0xEF && (unsigned char)s[1] == 0xBB &&
|
||||
(unsigned char)s[2] == 0xBF)
|
||||
return s + 3;
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Start of the frontmatter lines and of the closing fence, or NULL. */
|
||||
static const char *fm_bounds(const char *text, const char **end) {
|
||||
const char *s = skip_bom(text);
|
||||
if (strncmp(s, "---", 3) != 0)
|
||||
return NULL;
|
||||
const char *p = s + 3;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\r')
|
||||
p++;
|
||||
if (*p != '\n')
|
||||
return NULL;
|
||||
const char *start = ++p;
|
||||
while (*p) {
|
||||
if (strncmp(p, "---", 3) == 0) {
|
||||
const char *q = p + 3;
|
||||
while (*q == ' ' || *q == '\t' || *q == '\r')
|
||||
q++;
|
||||
if (*q == '\n' || !*q) {
|
||||
*end = p;
|
||||
return start;
|
||||
}
|
||||
}
|
||||
const char *nl = strchr(p, '\n');
|
||||
if (!nl)
|
||||
break;
|
||||
p = nl + 1;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *cst_fm_body(const char *text) {
|
||||
const char *end = NULL;
|
||||
if (!text || !fm_bounds(text, &end))
|
||||
return text ? skip_bom(text) : "";
|
||||
const char *nl = strchr(end, '\n');
|
||||
if (!nl)
|
||||
return end + strlen(end);
|
||||
nl++;
|
||||
while (*nl == '\n' || *nl == '\r')
|
||||
nl++;
|
||||
return nl;
|
||||
}
|
||||
|
||||
static void trim_into(const char *s, size_t n, char *out, size_t on) {
|
||||
while (n && isspace((unsigned char)*s))
|
||||
s++, n--;
|
||||
while (n && isspace((unsigned char)s[n - 1]))
|
||||
n--;
|
||||
if (n >= 2 && ((s[0] == '"' && s[n - 1] == '"') ||
|
||||
(s[0] == '\'' && s[n - 1] == '\''))) {
|
||||
s++;
|
||||
n -= 2;
|
||||
}
|
||||
if (n >= on)
|
||||
n = on - 1;
|
||||
memcpy(out, s, n);
|
||||
out[n] = 0;
|
||||
}
|
||||
|
||||
static void append_part(char *out, size_t n, const char *sep,
|
||||
const char *part) {
|
||||
size_t used = strlen(out);
|
||||
if (used + 1 >= n || !part[0])
|
||||
return;
|
||||
snprintf(out + used, n - used, "%s%s", used ? sep : "", part);
|
||||
}
|
||||
|
||||
int cst_fm_get(const char *text, const char *key, char *out, size_t n) {
|
||||
if (!out || !n)
|
||||
return 0;
|
||||
out[0] = 0;
|
||||
const char *end = NULL;
|
||||
const char *p = text ? fm_bounds(text, &end) : NULL;
|
||||
if (!p)
|
||||
return 0;
|
||||
size_t kl = strlen(key);
|
||||
while (p < end) {
|
||||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char *le = nl ? nl : end;
|
||||
if ((size_t)(le - p) > kl && strncmp(p, key, kl) == 0 && p[kl] == ':') {
|
||||
char val[1024];
|
||||
trim_into(p + kl + 1, (size_t)(le - p - kl - 1), val, sizeof(val));
|
||||
const char *q = nl ? nl + 1 : end;
|
||||
int folded = strcmp(val, ">") == 0 || strcmp(val, ">-") == 0;
|
||||
int literal = strcmp(val, "|") == 0 || strcmp(val, "|-") == 0;
|
||||
if (val[0] == '[') {
|
||||
size_t vl = strlen(val);
|
||||
if (vl && val[vl - 1] == ']')
|
||||
val[--vl] = 0;
|
||||
char *tok = strtok(val + 1, ",");
|
||||
while (tok) {
|
||||
char item[256];
|
||||
trim_into(tok, strlen(tok), item, sizeof(item));
|
||||
append_part(out, n, ",", item);
|
||||
tok = strtok(NULL, ",");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (!folded && !literal && val[0]) {
|
||||
snprintf(out, n, "%s", val);
|
||||
return 1;
|
||||
}
|
||||
/* block value: following indented lines (or "- item" list) */
|
||||
while (q < end && (*q == ' ' || *q == '\t')) {
|
||||
const char *qn = memchr(q, '\n', (size_t)(end - q));
|
||||
const char *qe = qn ? qn : end;
|
||||
char line[1024];
|
||||
trim_into(q, (size_t)(qe - q), line, sizeof(line));
|
||||
if (!folded && !literal && line[0] == '-' &&
|
||||
(line[1] == ' ' || !line[1])) {
|
||||
char item[1024];
|
||||
trim_into(line + 1, strlen(line + 1), item, sizeof(item));
|
||||
append_part(out, n, ",", item);
|
||||
} else {
|
||||
append_part(out, n, literal ? "\n" : " ", line);
|
||||
}
|
||||
q = qn ? qn + 1 : end;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
p = nl ? nl + 1 : end;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- discovery ----------------------------------------------------------- */
|
||||
|
||||
static struct {
|
||||
char dir[OS_PATH_MAX];
|
||||
char source[48];
|
||||
} g_roots[EXTRA_ROOTS];
|
||||
static int g_nroots;
|
||||
#ifndef _WIN32
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define LOCK() pthread_mutex_lock(&g_mu)
|
||||
#define UNLOCK() pthread_mutex_unlock(&g_mu)
|
||||
#else
|
||||
#define LOCK() ((void)0)
|
||||
#define UNLOCK() ((void)0)
|
||||
#endif
|
||||
|
||||
void cst_skills_add_root(const char *dir, const char *source) {
|
||||
if (!dir || !dir[0])
|
||||
return;
|
||||
LOCK();
|
||||
int dup = 0;
|
||||
for (int i = 0; i < g_nroots; i++)
|
||||
dup |= strcmp(g_roots[i].dir, dir) == 0;
|
||||
if (!dup && g_nroots < EXTRA_ROOTS) {
|
||||
snprintf(g_roots[g_nroots].dir, sizeof(g_roots[0].dir), "%s", dir);
|
||||
snprintf(g_roots[g_nroots].source, sizeof(g_roots[0].source), "%s",
|
||||
source ? source : "plugin");
|
||||
g_nroots++;
|
||||
}
|
||||
UNLOCK();
|
||||
}
|
||||
|
||||
void cst_skills_clear_roots(const char *prefix) {
|
||||
LOCK();
|
||||
int k = 0;
|
||||
size_t pl = prefix ? strlen(prefix) : 0;
|
||||
for (int i = 0; i < g_nroots; i++) {
|
||||
if (pl && strncmp(g_roots[i].source, prefix, pl) != 0)
|
||||
g_roots[k++] = g_roots[i];
|
||||
}
|
||||
g_nroots = k;
|
||||
UNLOCK();
|
||||
}
|
||||
|
||||
static char *slurp(const char *path, size_t max, size_t *len) {
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp)
|
||||
return NULL;
|
||||
cst_buf_t b = {0};
|
||||
char tmp[8192];
|
||||
size_t n;
|
||||
int big = 0;
|
||||
while ((n = fread(tmp, 1, sizeof(tmp), fp)) > 0) {
|
||||
if (b.len + n > max) {
|
||||
big = 1;
|
||||
break;
|
||||
}
|
||||
cst_buf_append(&b, tmp, n);
|
||||
}
|
||||
fclose(fp);
|
||||
if (big) {
|
||||
cst_buf_free(&b);
|
||||
return NULL;
|
||||
}
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
if (len)
|
||||
*len = b.len;
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
static int skill_cmp(const void *a, const void *b) {
|
||||
return strcmp(((const cst_skill_t *)a)->name, ((const cst_skill_t *)b)->name);
|
||||
}
|
||||
|
||||
static int have(const cst_skill_t *v, int n, const char *name) {
|
||||
for (int i = 0; i < n; i++)
|
||||
if (strcmp(v[i].name, name) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int scan_root(const char *root, const char *source, cst_skill_t *out,
|
||||
int n, int max) {
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(root);
|
||||
if (!d)
|
||||
return n;
|
||||
struct dirent *de;
|
||||
while (n < max && (de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.')
|
||||
continue;
|
||||
char dir[OS_PATH_MAX], md[OS_PATH_MAX + 16];
|
||||
if (snprintf(dir, sizeof(dir), "%s/%s", root, de->d_name) >=
|
||||
(int)sizeof(dir))
|
||||
continue;
|
||||
snprintf(md, sizeof(md), "%s/SKILL.md", dir);
|
||||
char *text = slurp(md, SKILL_MD_MAX, NULL);
|
||||
if (!text)
|
||||
continue;
|
||||
cst_skill_t s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
if (!cst_fm_get(text, "name", s.name, sizeof(s.name)) || !s.name[0])
|
||||
snprintf(s.name, sizeof(s.name), "%.63s", de->d_name);
|
||||
cst_fm_get(text, "description", s.desc, sizeof(s.desc));
|
||||
if (!s.desc[0]) {
|
||||
/* first prose line of the body */
|
||||
const char *b = cst_fm_body(text);
|
||||
for (;;) {
|
||||
while (*b == ' ' || *b == '\t' || *b == '\n' || *b == '\r')
|
||||
b++;
|
||||
if (*b != '#')
|
||||
break;
|
||||
b += strcspn(b, "\n");
|
||||
}
|
||||
trim_into(b, strcspn(b, "\r\n"), s.desc, sizeof(s.desc));
|
||||
}
|
||||
free(text);
|
||||
snprintf(s.dir, sizeof(s.dir), "%s", dir);
|
||||
snprintf(s.source, sizeof(s.source), "%s", source);
|
||||
if (!have(out, n, s.name))
|
||||
out[n++] = s;
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)root;
|
||||
(void)source;
|
||||
(void)out;
|
||||
(void)max;
|
||||
#endif
|
||||
return n;
|
||||
}
|
||||
|
||||
int cst_skills_list(const char *ws, cst_skill_t *out, int max) {
|
||||
if (!out || max <= 0)
|
||||
return 0;
|
||||
int n = 0;
|
||||
char p[OS_PATH_MAX];
|
||||
if (ws && ws[0]) {
|
||||
snprintf(p, sizeof(p), "%s/.agent/skills", ws);
|
||||
n = scan_root(p, "workspace", out, n, max);
|
||||
snprintf(p, sizeof(p), "%s/.claude/skills", ws);
|
||||
n = scan_root(p, "claude", out, n, max);
|
||||
}
|
||||
char cfg[OS_PATH_MAX];
|
||||
if (os_config_dir(cfg, sizeof(cfg)) == 0) {
|
||||
snprintf(p, sizeof(p), "%.4000s/skills", cfg);
|
||||
n = scan_root(p, "user", out, n, max);
|
||||
}
|
||||
LOCK();
|
||||
int nr = g_nroots;
|
||||
char roots[EXTRA_ROOTS][OS_PATH_MAX], srcs[EXTRA_ROOTS][48];
|
||||
for (int i = 0; i < nr; i++) {
|
||||
memcpy(roots[i], g_roots[i].dir, sizeof(roots[i]));
|
||||
memcpy(srcs[i], g_roots[i].source, sizeof(srcs[i]));
|
||||
}
|
||||
UNLOCK();
|
||||
for (int i = 0; i < nr; i++)
|
||||
n = scan_root(roots[i], srcs[i], out, n, max);
|
||||
qsort(out, (size_t)n, sizeof(*out), skill_cmp);
|
||||
return n;
|
||||
}
|
||||
|
||||
char *cst_skills_index(const char *ws) {
|
||||
cst_skill_t *v = calloc(CST_SKILLS_MAX, sizeof(*v));
|
||||
if (!v)
|
||||
return NULL;
|
||||
int n = cst_skills_list(ws, v, CST_SKILLS_MAX);
|
||||
if (n == 0) {
|
||||
free(v);
|
||||
return NULL;
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, "\nSkills (call skill(name) to load one before working on a "
|
||||
"task that matches its description):\n");
|
||||
int shown = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
char line[400];
|
||||
size_t dl = strlen(v[i].desc);
|
||||
size_t keep = cst_utf8_floor(v[i].desc, dl > 240 ? 240 : dl);
|
||||
snprintf(line, sizeof(line), "- %s: %.*s%s\n", v[i].name, (int)keep,
|
||||
v[i].desc, keep < dl ? "…" : "");
|
||||
if (b.len + strlen(line) > CST_SKILL_INDEX_MAX - 64)
|
||||
break;
|
||||
cst_buf_puts(&b, line);
|
||||
shown++;
|
||||
}
|
||||
if (shown < n)
|
||||
cst_buf_printf(&b, "- … %d more (see /skills)\n", n - shown);
|
||||
free(v);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
/* --- loading ------------------------------------------------------------- */
|
||||
|
||||
static int find_skill(const char *ws, const char *name, cst_skill_t *out) {
|
||||
cst_skill_t *v = calloc(CST_SKILLS_MAX, sizeof(*v));
|
||||
if (!v)
|
||||
return 0;
|
||||
int n = cst_skills_list(ws, v, CST_SKILLS_MAX), hit = 0;
|
||||
for (int i = 0; i < n && !hit; i++) {
|
||||
if (strcmp(v[i].name, name) == 0) {
|
||||
*out = v[i];
|
||||
hit = 1;
|
||||
}
|
||||
}
|
||||
free(v);
|
||||
return hit;
|
||||
}
|
||||
|
||||
static void list_files(const char *dir, const char *rel, int depth,
|
||||
cst_buf_t *b, int *count) {
|
||||
#ifndef _WIN32
|
||||
char full[OS_PATH_MAX];
|
||||
if (snprintf(full, sizeof(full), "%s%s%s", dir, rel[0] ? "/" : "", rel) >=
|
||||
(int)sizeof(full))
|
||||
return;
|
||||
DIR *d = opendir(full);
|
||||
if (!d)
|
||||
return;
|
||||
char *names[256];
|
||||
int nn = 0;
|
||||
struct dirent *de;
|
||||
while (nn < 256 && (de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.')
|
||||
continue;
|
||||
names[nn] = strdup(de->d_name);
|
||||
if (names[nn])
|
||||
nn++;
|
||||
}
|
||||
closedir(d);
|
||||
for (int i = 1; i < nn; i++) {
|
||||
for (int j = i; j > 0 && strcmp(names[j - 1], names[j]) > 0; j--) {
|
||||
char *tmp = names[j];
|
||||
names[j] = names[j - 1];
|
||||
names[j - 1] = tmp;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < nn; i++) {
|
||||
char crel[OS_PATH_MAX], cfull[OS_PATH_MAX + 2];
|
||||
snprintf(crel, sizeof(crel), "%s%s%.255s", rel, rel[0] ? "/" : "",
|
||||
names[i]);
|
||||
snprintf(cfull, sizeof(cfull), "%s/%s", dir, crel);
|
||||
struct stat st;
|
||||
if (stat(cfull, &st) != 0)
|
||||
continue;
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (depth < 3)
|
||||
list_files(dir, crel, depth + 1, b, count);
|
||||
} else if (strcmp(crel, "SKILL.md") != 0) {
|
||||
if (*count < SKILL_LIST_FILES)
|
||||
cst_buf_printf(b, "- %s\n", crel);
|
||||
(*count)++;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < nn; i++)
|
||||
free(names[i]);
|
||||
#else
|
||||
(void)dir;
|
||||
(void)rel;
|
||||
(void)depth;
|
||||
(void)b;
|
||||
(void)count;
|
||||
#endif
|
||||
}
|
||||
|
||||
static char *errf(int *code, const char *fmt, const char *a, const char *b) {
|
||||
char msg[1200];
|
||||
snprintf(msg, sizeof(msg), fmt, a, b);
|
||||
*code = 1;
|
||||
return strdup(msg);
|
||||
}
|
||||
|
||||
static char *names_list(const char *ws) {
|
||||
cst_skill_t *v = calloc(CST_SKILLS_MAX, sizeof(*v));
|
||||
cst_buf_t b = {0};
|
||||
int n = v ? cst_skills_list(ws, v, CST_SKILLS_MAX) : 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
cst_buf_printf(&b, "%s%s", i ? ", " : "", v[i].name);
|
||||
free(v);
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "(none)");
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
char *cst_skill_read(const char *ws, const char *name, const char *file,
|
||||
int *exit_code) {
|
||||
int dummy;
|
||||
if (!exit_code)
|
||||
exit_code = &dummy;
|
||||
*exit_code = 0;
|
||||
cst_skill_t s;
|
||||
if (!name || !name[0] || !find_skill(ws, name, &s)) {
|
||||
char *names = names_list(ws);
|
||||
char *r = errf(exit_code, "error: no skill named \"%.100s\" (available: %.900s)",
|
||||
name ? name : "", names ? names : "");
|
||||
free(names);
|
||||
return r;
|
||||
}
|
||||
if (file && file[0]) {
|
||||
char want[OS_PATH_MAX * 2], real[OS_PATH_MAX], root[OS_PATH_MAX];
|
||||
snprintf(want, sizeof(want), "%s/%s", s.dir, file);
|
||||
if (os_realpath(s.dir, root, sizeof(root)) != 0 ||
|
||||
os_realpath(want, real, sizeof(real)) != 0)
|
||||
return errf(exit_code, "error: skill %s has no file %.300s", s.name,
|
||||
file);
|
||||
if (!os_path_under(root, real))
|
||||
return errf(exit_code, "error: %.300s is outside the skill %s directory",
|
||||
file, s.name);
|
||||
size_t n = 0;
|
||||
char *body = slurp(real, SKILL_FILE_MAX, &n);
|
||||
if (!body)
|
||||
return errf(exit_code, "error: cannot read %.300s of skill %s (missing, "
|
||||
"a directory, or over 256 KiB)", file, s.name);
|
||||
if (memchr(body, 0, n)) {
|
||||
free(body);
|
||||
return errf(exit_code, "error: %.300s of skill %s is binary", file,
|
||||
s.name);
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "skill %s · %s\n", s.name, file);
|
||||
cst_buf_append(&b, body, n);
|
||||
free(body);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
char md[OS_PATH_MAX + 16];
|
||||
snprintf(md, sizeof(md), "%s/SKILL.md", s.dir);
|
||||
char *text = slurp(md, SKILL_MD_MAX, NULL);
|
||||
if (!text)
|
||||
return errf(exit_code, "error: cannot read %.300s%.1s", md, "");
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "skill %s (%s) · %s\n\n", s.name, s.source, s.dir);
|
||||
cst_buf_puts(&b, cst_fm_body(text));
|
||||
free(text);
|
||||
if (b.len && b.buf[b.len - 1] != '\n')
|
||||
cst_buf_puts(&b, "\n");
|
||||
cst_buf_t files = {0};
|
||||
int count = 0;
|
||||
list_files(s.dir, "", 0, &files, &count);
|
||||
if (count) {
|
||||
cst_buf_printf(&b, "\nFiles in this skill (read with skill(name=\"%s\", "
|
||||
"file=...); paths are relative to %s):\n",
|
||||
s.name, s.dir);
|
||||
cst_buf_append(&b, files.buf, files.len);
|
||||
if (count > SKILL_LIST_FILES)
|
||||
cst_buf_printf(&b, "- … %d more\n", count - SKILL_LIST_FILES);
|
||||
}
|
||||
cst_buf_free(&files);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
char *cst_skill_prompt(const char *ws, const char *name, const char *task) {
|
||||
int code = 0;
|
||||
char *body = cst_skill_read(ws, name, NULL, &code);
|
||||
if (!body || code != 0) {
|
||||
free(body);
|
||||
return NULL;
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "Use the \"%s\" skill for this task. Its instructions "
|
||||
"(already loaded):\n\n<skill>\n",
|
||||
name);
|
||||
cst_buf_puts(&b, body);
|
||||
cst_buf_puts(&b, "</skill>\n\n");
|
||||
cst_buf_puts(&b, task && task[0] ? task : "Apply this skill now.");
|
||||
free(body);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
/* --- tool ---------------------------------------------------------------- */
|
||||
|
||||
static const char *arg_of(const ctl_tool_call_t *c, const char *key) {
|
||||
for (int i = 0; i < c->nargs; i++)
|
||||
if (strcmp(c->args[i].key, key) == 0)
|
||||
return c->args[i].value ? c->args[i].value : "";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int skill_tool(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r, void *ud) {
|
||||
(void)ud;
|
||||
int code = 0;
|
||||
char *out = cst_skill_read(ctx->workspace_root, arg_of(c, "name"),
|
||||
arg_of(c, "file"), &code);
|
||||
r->out = out ? out : strdup("error: out of memory");
|
||||
r->out_len = r->out ? strlen(r->out) : 0;
|
||||
r->exit_code = out ? code : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int always(const ctl_tool_call_t *c, void *ud) {
|
||||
(void)c;
|
||||
(void)ud;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int cst_skills_register_tool(void) {
|
||||
if (cst_tool_find("skill"))
|
||||
return 0;
|
||||
cst_tool_def_t d;
|
||||
memset(&d, 0, sizeof(d));
|
||||
d.name = "skill";
|
||||
d.desc = "Load a skill from the Skills list: its instructions (SKILL.md) "
|
||||
"and the list of its files. Load the matching skill before "
|
||||
"starting such a task; pass file to read one of its files.";
|
||||
d.schema_json =
|
||||
"{\"type\":\"object\",\"properties\":{"
|
||||
"\"name\":{\"type\":\"string\",\"description\":\"skill name\"},"
|
||||
"\"file\":{\"type\":\"string\",\"description\":\"a file inside the "
|
||||
"skill directory (optional)\"}},\"required\":[\"name\"]}";
|
||||
d.perm = CST_TP_META;
|
||||
d.run_ex = skill_tool;
|
||||
d.parallel = always;
|
||||
d.owner = "builtin:skills";
|
||||
return cst_tool_register(&d);
|
||||
}
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
#ifndef _XOPEN_SOURCE
|
||||
#define _XOPEN_SOURCE 700
|
||||
#endif
|
||||
|
||||
#include "cst_subagent.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#endif
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_agent.h"
|
||||
#include "cst_skills.h"
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#define ROLE_FILE_MAX (64 * 1024)
|
||||
#define INDEX_MAX 2048
|
||||
|
||||
static pthread_mutex_t g_hook_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t g_seq_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char g_ws[OS_PATH_MAX];
|
||||
|
||||
/* ---------- roles ---------- */
|
||||
|
||||
static const struct {
|
||||
const char *name, *desc, *prompt;
|
||||
int read_only;
|
||||
} BUILTIN[] = {
|
||||
{"general",
|
||||
"General-purpose agent for multi-step work: researches, edits files and "
|
||||
"runs commands as needed.",
|
||||
"", 0},
|
||||
{"explore",
|
||||
"Fast read-only explorer: searches and reads the codebase to answer a "
|
||||
"question (where is X, how does Y work, which files do Z). Several can "
|
||||
"run at once.",
|
||||
"You only read. Search broadly first (grep, glob, list_dir), then read "
|
||||
"just the relevant parts. Report concrete findings: file paths with line "
|
||||
"numbers, short excerpts when they matter, and a direct answer. Say what "
|
||||
"you looked for and did not find.",
|
||||
1},
|
||||
{"plan",
|
||||
"Read-only planner: investigates the code involved and returns a "
|
||||
"step-by-step implementation plan.",
|
||||
"You only read. Investigate the code involved, then return a concrete "
|
||||
"plan: the files to change and what to change in each, risks and edge "
|
||||
"cases, and how to verify. Sketch code only where it removes ambiguity.",
|
||||
1},
|
||||
};
|
||||
#define NBUILTIN ((int)(sizeof(BUILTIN) / sizeof(BUILTIN[0])))
|
||||
|
||||
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, "");
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
static int find_role(const cst_agent_role_t *v, int n, const char *name) {
|
||||
for (int i = 0; i < n; i++)
|
||||
if (strcmp(v[i].name, name) == 0)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* "a, b ,c" -> ",a,b,c," */
|
||||
static void tool_list(const char *in, char *out, size_t sz) {
|
||||
size_t o = 0;
|
||||
out[0] = 0;
|
||||
if (!in || !in[0])
|
||||
return;
|
||||
if (o + 1 < sz)
|
||||
out[o++] = ',';
|
||||
for (const char *p = in; *p && o + 2 < sz; p++) {
|
||||
if (*p == ' ' || *p == '\t' || *p == '[' || *p == ']' || *p == '"')
|
||||
continue;
|
||||
if (*p == ',' && out[o - 1] == ',')
|
||||
continue;
|
||||
out[o++] = *p;
|
||||
}
|
||||
if (out[o - 1] != ',' && o + 1 < sz)
|
||||
out[o++] = ',';
|
||||
out[o] = 0;
|
||||
if (strcmp(out, ",") == 0)
|
||||
out[0] = 0;
|
||||
}
|
||||
|
||||
static int truthy(const char *v) {
|
||||
return strcmp(v, "true") == 0 || strcmp(v, "yes") == 0 ||
|
||||
strcmp(v, "1") == 0;
|
||||
}
|
||||
|
||||
static void load_dir(const char *dir, const char *source, cst_agent_role_t *v,
|
||||
int *n, int max) {
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(dir);
|
||||
if (!d)
|
||||
return;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL && *n < max) {
|
||||
size_t len = strlen(de->d_name);
|
||||
if (de->d_name[0] == '.' || len < 4 ||
|
||||
strcmp(de->d_name + len - 3, ".md") != 0)
|
||||
continue;
|
||||
char p[OS_PATH_MAX];
|
||||
if (snprintf(p, sizeof(p), "%s/%s", dir, de->d_name) >= (int)sizeof(p))
|
||||
continue;
|
||||
char *text = slurp(p, ROLE_FILE_MAX);
|
||||
if (!text)
|
||||
continue;
|
||||
char name[64], val[1024];
|
||||
if (!cst_fm_get(text, "name", name, sizeof(name)) || !name[0])
|
||||
snprintf(name, sizeof(name), "%.*s", (int)(len - 3), de->d_name);
|
||||
char clean[64];
|
||||
cst_tool_sanitize_name(name, clean, sizeof(clean));
|
||||
int at = find_role(v, *n, clean);
|
||||
if (at >= 0 && strcmp(v[at].source, "builtin") != 0) {
|
||||
free(text); /* the workspace one came first */
|
||||
continue;
|
||||
}
|
||||
cst_agent_role_t *r = at >= 0 ? &v[at] : &v[(*n)++];
|
||||
free(r->prompt);
|
||||
memset(r, 0, sizeof(*r));
|
||||
snprintf(r->name, sizeof(r->name), "%s", clean);
|
||||
snprintf(r->source, sizeof(r->source), "%s", source);
|
||||
if (cst_fm_get(text, "description", val, sizeof(val)))
|
||||
snprintf(r->desc, sizeof(r->desc), "%.255s", val);
|
||||
if (cst_fm_get(text, "model", val, sizeof(val)))
|
||||
snprintf(r->model, sizeof(r->model), "%.127s", val);
|
||||
if (cst_fm_get(text, "tools", val, sizeof(val)))
|
||||
tool_list(val, r->tools, sizeof(r->tools));
|
||||
if (cst_fm_get(text, "max_steps", val, sizeof(val)))
|
||||
r->max_steps = atoi(val) > 0 ? atoi(val) : 0;
|
||||
if (cst_fm_get(text, "readonly", val, sizeof(val)) ||
|
||||
cst_fm_get(text, "read_only", val, sizeof(val)))
|
||||
r->read_only = truthy(val);
|
||||
const char *body = cst_fm_body(text);
|
||||
while (*body == '\n' || *body == '\r')
|
||||
body++;
|
||||
r->prompt = strdup(body);
|
||||
free(text);
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)dir;
|
||||
(void)source;
|
||||
(void)v;
|
||||
(void)n;
|
||||
(void)max;
|
||||
#endif
|
||||
}
|
||||
|
||||
int cst_roles_list(const char *ws, cst_agent_role_t *out, int max) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < NBUILTIN && n < max; i++, n++) {
|
||||
memset(&out[n], 0, sizeof(out[n]));
|
||||
snprintf(out[n].name, sizeof(out[n].name), "%s", BUILTIN[i].name);
|
||||
snprintf(out[n].desc, sizeof(out[n].desc), "%s", BUILTIN[i].desc);
|
||||
snprintf(out[n].source, sizeof(out[n].source), "builtin");
|
||||
out[n].read_only = BUILTIN[i].read_only;
|
||||
out[n].prompt = strdup(BUILTIN[i].prompt);
|
||||
}
|
||||
char p[OS_PATH_MAX];
|
||||
if (ws && ws[0]) {
|
||||
snprintf(p, sizeof(p), "%.4000s/.agent/agents", ws);
|
||||
load_dir(p, "workspace", out, &n, max);
|
||||
}
|
||||
char cfg[OS_PATH_MAX];
|
||||
if (os_config_dir(cfg, sizeof(cfg)) == 0) {
|
||||
snprintf(p, sizeof(p), "%.4000s/agents", cfg);
|
||||
load_dir(p, "user", out, &n, max);
|
||||
}
|
||||
/* custom roles after the built-ins, by name */
|
||||
for (int i = NBUILTIN + 1; i < n; i++)
|
||||
for (int j = i; j > NBUILTIN && strcmp(out[j - 1].name, out[j].name) > 0;
|
||||
j--) {
|
||||
cst_agent_role_t t = out[j];
|
||||
out[j] = out[j - 1];
|
||||
out[j - 1] = t;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void cst_roles_free(cst_agent_role_t *v, int n) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
free(v[i].prompt);
|
||||
v[i].prompt = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
char *cst_subagent_index(const char *ws) {
|
||||
cst_agent_role_t *v = calloc(CST_ROLES_MAX, sizeof(*v));
|
||||
if (!v)
|
||||
return NULL;
|
||||
int n = cst_roles_list(ws, v, CST_ROLES_MAX);
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, "\nSub-agents (task tool, agent=<name>; default "
|
||||
"general):\n");
|
||||
for (int i = 0; i < n && b.len < INDEX_MAX; i++)
|
||||
cst_buf_printf(&b, "- %s%s: %.200s\n", v[i].name,
|
||||
v[i].read_only ? " (read-only)" : "", v[i].desc);
|
||||
cst_roles_free(v, n);
|
||||
free(v);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
/* ---------- transcripts ---------- */
|
||||
|
||||
static int sub_dir(const char *ws, const char *parent_id, char *out,
|
||||
size_t sz) {
|
||||
char dir[OS_PATH_MAX];
|
||||
if (!parent_id || !parent_id[0] || strchr(parent_id, '/') ||
|
||||
strchr(parent_id, '\\') ||
|
||||
cst_agent_store_dir(ws, dir, sizeof(dir)) != 0)
|
||||
return -1;
|
||||
return snprintf(out, sz, "%s/%s", dir, parent_id) < (int)sz ? 0 : -1;
|
||||
}
|
||||
|
||||
int cst_subagent_path(const char *ws, const char *parent_id, const char *file,
|
||||
char *out, size_t sz) {
|
||||
char dir[OS_PATH_MAX];
|
||||
if (!file || strchr(file, '/') || strchr(file, '\\') ||
|
||||
sub_dir(ws, parent_id, dir, sizeof(dir)) != 0)
|
||||
return -1;
|
||||
return snprintf(out, sz, "%s/%s.jsonl", dir, file) < (int)sz ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Reserve the next sub-<n>.jsonl. */
|
||||
static int next_file(const char *dir, char *file, size_t fsz) {
|
||||
pthread_mutex_lock(&g_seq_mu);
|
||||
int best = 0;
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(dir);
|
||||
if (d) {
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
int k = 0;
|
||||
if (sscanf(de->d_name, "sub-%d.jsonl", &k) == 1 && k > best)
|
||||
best = k;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
#endif
|
||||
int rc = -1;
|
||||
for (int k = best + 1; k < best + 100; k++) {
|
||||
char p[OS_PATH_MAX];
|
||||
snprintf(file, fsz, "sub-%d", k);
|
||||
if (snprintf(p, sizeof(p), "%s/%s.jsonl", dir, file) >= (int)sizeof(p))
|
||||
break;
|
||||
struct stat st;
|
||||
if (stat(p, &st) == 0)
|
||||
continue;
|
||||
FILE *fp = fopen(p, "a");
|
||||
if (fp) {
|
||||
fclose(fp);
|
||||
rc = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_seq_mu);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void write_meta(const char *dir, const char *file, const char *role,
|
||||
const char *desc, const char *status, int steps,
|
||||
int tools, double ms) {
|
||||
char p[OS_PATH_MAX];
|
||||
if (snprintf(p, sizeof(p), "%s/%s.json", dir, file) >= (int)sizeof(p))
|
||||
return;
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(o, "role", role);
|
||||
cJSON_AddStringToObject(o, "description", desc);
|
||||
cJSON_AddStringToObject(o, "status", status);
|
||||
cJSON_AddNumberToObject(o, "steps", steps);
|
||||
cJSON_AddNumberToObject(o, "tools", tools);
|
||||
cJSON_AddNumberToObject(o, "ms", ms);
|
||||
char *txt = cJSON_PrintUnformatted(o);
|
||||
cJSON_Delete(o);
|
||||
FILE *fp = txt ? fopen(p, "w") : NULL;
|
||||
if (fp) {
|
||||
fputs(txt, fp);
|
||||
fputc('\n', fp);
|
||||
fclose(fp);
|
||||
}
|
||||
free(txt);
|
||||
}
|
||||
|
||||
int cst_subagent_list(const char *ws, const char *parent_id,
|
||||
cst_subagent_rec_t *out, int max) {
|
||||
char dir[OS_PATH_MAX];
|
||||
if (sub_dir(ws, parent_id, dir, sizeof(dir)) != 0)
|
||||
return 0;
|
||||
int n = 0;
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(dir);
|
||||
if (!d)
|
||||
return 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL && n < max) {
|
||||
int k = 0;
|
||||
char tail[8] = "";
|
||||
if (sscanf(de->d_name, "sub-%d.%7s", &k, tail) != 2 ||
|
||||
strcmp(tail, "json") != 0)
|
||||
continue;
|
||||
char p[OS_PATH_MAX];
|
||||
if (snprintf(p, sizeof(p), "%s/%s", dir, de->d_name) >= (int)sizeof(p))
|
||||
continue;
|
||||
char *txt = slurp(p, 64 * 1024);
|
||||
cJSON *o = txt ? cJSON_Parse(txt) : NULL;
|
||||
free(txt);
|
||||
cst_subagent_rec_t *r = &out[n];
|
||||
memset(r, 0, sizeof(*r));
|
||||
snprintf(r->file, sizeof(r->file), "sub-%d", k);
|
||||
const cJSON *it;
|
||||
if ((it = cJSON_GetObjectItemCaseSensitive(o, "role")) && it->valuestring)
|
||||
snprintf(r->role, sizeof(r->role), "%s", it->valuestring);
|
||||
if ((it = cJSON_GetObjectItemCaseSensitive(o, "description")) &&
|
||||
it->valuestring)
|
||||
snprintf(r->desc, sizeof(r->desc), "%s", it->valuestring);
|
||||
if ((it = cJSON_GetObjectItemCaseSensitive(o, "status")) &&
|
||||
it->valuestring)
|
||||
snprintf(r->status, sizeof(r->status), "%s", it->valuestring);
|
||||
if ((it = cJSON_GetObjectItemCaseSensitive(o, "steps")))
|
||||
r->steps = it->valueint;
|
||||
if ((it = cJSON_GetObjectItemCaseSensitive(o, "tools")))
|
||||
r->tools = it->valueint;
|
||||
if ((it = cJSON_GetObjectItemCaseSensitive(o, "ms")))
|
||||
r->ms = it->valuedouble;
|
||||
cJSON_Delete(o);
|
||||
struct stat st;
|
||||
if (stat(p, &st) == 0)
|
||||
r->mtime = (long long)st.st_mtime;
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)out;
|
||||
(void)max;
|
||||
#endif
|
||||
/* newest first: higher number */
|
||||
for (int i = 1; i < n; i++)
|
||||
for (int j = i; j > 0 && atoi(out[j - 1].file + 4) < atoi(out[j].file + 4);
|
||||
j--) {
|
||||
cst_subagent_rec_t t = out[j];
|
||||
out[j] = out[j - 1];
|
||||
out[j - 1] = t;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ---------- the task tool ---------- */
|
||||
|
||||
typedef struct {
|
||||
const cst_agent_hooks_t *ph;
|
||||
const ctl_tool_call_t *pcall;
|
||||
const char *role;
|
||||
const char *ws;
|
||||
char last[2][80];
|
||||
int ntools;
|
||||
volatile int *parent_cancel;
|
||||
cst_agent_t *child;
|
||||
atomic_int done;
|
||||
} sub_run_t;
|
||||
|
||||
static const char *arg(const ctl_tool_call_t *c, const char *key) {
|
||||
for (int i = 0; i < c->nargs; i++)
|
||||
if (strcmp(c->args[i].key, key) == 0)
|
||||
return c->args[i].value ? c->args[i].value : "";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* "grep foo" / "read_file src/x.c" for the progress line. */
|
||||
static void step_summary(const ctl_tool_call_t *c, const char *ws, char *out,
|
||||
size_t sz) {
|
||||
static const char *keys[] = {"path", "pattern", "cmd", "command", "url",
|
||||
"query", "name", "file"};
|
||||
const char *v = NULL;
|
||||
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]) && !v; i++)
|
||||
v = arg(c, keys[i]);
|
||||
char one[48] = "";
|
||||
size_t wl = ws ? strlen(ws) : 0;
|
||||
if (v && wl && strncmp(v, ws, wl) == 0 && v[wl] == '/' && v[wl + 1])
|
||||
v += wl + 1;
|
||||
if (v) {
|
||||
size_t k = 0;
|
||||
for (; v[k] && v[k] != '\n' && k < 40; k++)
|
||||
one[k] = v[k];
|
||||
while (k > 0 && ((unsigned char)one[k] & 0xC0) == 0x80 && v[k])
|
||||
k--;
|
||||
one[k] = 0;
|
||||
}
|
||||
snprintf(out, sz, "%.30s%s%.40s", c->name, one[0] ? " " : "", one);
|
||||
}
|
||||
|
||||
static void sub_tool_event(const char *phase, const ctl_tool_call_t *call,
|
||||
const char *detail, void *ud) {
|
||||
(void)detail;
|
||||
sub_run_t *r = ud;
|
||||
if (!call || strcmp(phase, "run") != 0)
|
||||
return;
|
||||
r->ntools++;
|
||||
memcpy(r->last[0], r->last[1], sizeof(r->last[0]));
|
||||
step_summary(call, r->ws, r->last[1], sizeof(r->last[1]));
|
||||
if (!r->ph || !r->ph->on_tool_event)
|
||||
return;
|
||||
char line[256];
|
||||
snprintf(line, sizeof(line), "%s%s%s · %s", r->role,
|
||||
r->last[0][0] ? " · " : "", r->last[0], r->last[1]);
|
||||
pthread_mutex_lock(&g_hook_mu);
|
||||
r->ph->on_tool_event("progress", r->pcall, line, r->ph->userdata);
|
||||
pthread_mutex_unlock(&g_hook_mu);
|
||||
}
|
||||
|
||||
static int sub_permission(const char *tool, const char *detail,
|
||||
const char *preview, void *ud) {
|
||||
sub_run_t *r = ud;
|
||||
if (!r->ph || !r->ph->on_permission)
|
||||
return CST_PERM_DENY;
|
||||
cst_buf_t d = {0};
|
||||
size_t sbl = strlen(CST_PERM_SANDBOX);
|
||||
if (detail && strncmp(detail, CST_PERM_SANDBOX, sbl) == 0) {
|
||||
cst_buf_printf(&d, "%s[%s] %s", CST_PERM_SANDBOX, r->role, detail + sbl);
|
||||
} else {
|
||||
cst_buf_printf(&d, "[%s] %s", r->role, detail ? detail : tool);
|
||||
}
|
||||
/* one question at a time, whichever sub-agent asks */
|
||||
pthread_mutex_lock(&g_hook_mu);
|
||||
int ans = r->ph->on_permission(tool, d.buf ? d.buf : tool, preview,
|
||||
r->ph->userdata);
|
||||
pthread_mutex_unlock(&g_hook_mu);
|
||||
cst_buf_free(&d);
|
||||
return ans;
|
||||
}
|
||||
|
||||
static void *watch_main(void *p) {
|
||||
sub_run_t *r = p;
|
||||
while (!atomic_load(&r->done)) {
|
||||
if (r->parent_cancel && *r->parent_cancel)
|
||||
cst_agent_request_cancel(r->child);
|
||||
struct timespec ts = {0, 50 * 1000000L};
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int out_text(cst_tool_result_t *r, int code, const char *text) {
|
||||
r->exit_code = code;
|
||||
r->out = strdup(text);
|
||||
r->out_len = r->out ? strlen(r->out) : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int task_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r, void *ud) {
|
||||
(void)ud;
|
||||
if (!ctx->agent)
|
||||
return out_text(r, 1, "error: task needs a running agent");
|
||||
const cst_agent_config_t *pcfg = cst_agent_config(ctx->agent);
|
||||
if (pcfg->depth > 0)
|
||||
return out_text(r, 1, "error: a sub-agent cannot start sub-agents; do "
|
||||
"the work yourself");
|
||||
const char *desc = arg(c, "description");
|
||||
const char *prompt = arg(c, "prompt");
|
||||
const char *want = arg(c, "agent");
|
||||
if (!want || !want[0])
|
||||
want = "general";
|
||||
if (!prompt || !prompt[0])
|
||||
return out_text(r, 1, "error: prompt is required");
|
||||
if (!desc || !desc[0])
|
||||
desc = want;
|
||||
|
||||
cst_agent_role_t *roles = calloc(CST_ROLES_MAX, sizeof(*roles));
|
||||
if (!roles)
|
||||
return out_text(r, 1, "error: out of memory");
|
||||
int nr = cst_roles_list(ctx->workspace_root, roles, CST_ROLES_MAX);
|
||||
int ri = find_role(roles, nr, want);
|
||||
if (ri < 0) {
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "error: no agent named \"%s\" (available:", want);
|
||||
for (int i = 0; i < nr; i++)
|
||||
cst_buf_printf(&b, "%s %s", i ? "," : "", roles[i].name);
|
||||
cst_buf_puts(&b, ")");
|
||||
cst_roles_free(roles, nr);
|
||||
free(roles);
|
||||
r->exit_code = 1;
|
||||
r->out_len = b.len;
|
||||
r->out = cst_buf_steal(&b);
|
||||
return 0;
|
||||
}
|
||||
const cst_agent_role_t *role = &roles[ri];
|
||||
|
||||
char pid[256], dir[OS_PATH_MAX], file[64], path[OS_PATH_MAX];
|
||||
cst_agent_session_id(ctx->agent, pid, sizeof(pid));
|
||||
if (sub_dir(ctx->workspace_root, pid, dir, sizeof(dir)) != 0 ||
|
||||
os_mkdir_p(dir, 0755) != 0 || next_file(dir, file, sizeof(file)) != 0 ||
|
||||
snprintf(path, sizeof(path), "%s/%s.jsonl", dir, file) >=
|
||||
(int)sizeof(path)) {
|
||||
cst_roles_free(roles, nr);
|
||||
free(roles);
|
||||
return out_text(r, 1, "error: cannot create the sub-agent transcript");
|
||||
}
|
||||
|
||||
cst_agent_config_t cc = *pcfg;
|
||||
cc.depth = pcfg->depth + 1;
|
||||
cc.plan_mode = 0;
|
||||
cc.read_only = role->read_only || pcfg->plan_mode;
|
||||
snprintf(cc.tools_only, sizeof(cc.tools_only), "%s", role->tools);
|
||||
cst_tool_set_disabled(&cc, "ask_user", 1);
|
||||
cc.role_prompt = role->prompt;
|
||||
if (role->model[0])
|
||||
snprintf(cc.model, sizeof(cc.model), "%s", role->model);
|
||||
if (role->max_steps > 0)
|
||||
cc.max_steps = role->max_steps;
|
||||
snprintf(cc.session_path, sizeof(cc.session_path), "%s", path);
|
||||
cst_agent_undo_key(ctx->agent, cc.undo_sid, sizeof(cc.undo_sid),
|
||||
cc.undo_tag, sizeof(cc.undo_tag));
|
||||
|
||||
volatile int *parent_cancel = os_get_cancel_flag();
|
||||
sub_run_t run;
|
||||
memset(&run, 0, sizeof(run));
|
||||
run.ph = ctx->hooks;
|
||||
run.pcall = c;
|
||||
run.role = role->name;
|
||||
run.ws = ctx->workspace_root;
|
||||
run.parent_cancel = parent_cancel;
|
||||
run.child = cst_agent_create(&cc);
|
||||
if (!run.child) {
|
||||
cst_roles_free(roles, nr);
|
||||
free(roles);
|
||||
return out_text(r, 1, "error: cannot start the sub-agent");
|
||||
}
|
||||
write_meta(dir, file, role->name, desc, "running", 0, 0, 0);
|
||||
|
||||
cst_agent_hooks_t h;
|
||||
memset(&h, 0, sizeof(h));
|
||||
h.on_tool_event = sub_tool_event;
|
||||
h.on_permission = sub_permission;
|
||||
h.userdata = &run;
|
||||
if (run.ph && run.ph->on_tool_event) {
|
||||
char line[160];
|
||||
snprintf(line, sizeof(line), "%s · starting", role->name);
|
||||
pthread_mutex_lock(&g_hook_mu);
|
||||
run.ph->on_tool_event("progress", c, line, run.ph->userdata);
|
||||
pthread_mutex_unlock(&g_hook_mu);
|
||||
}
|
||||
|
||||
pthread_t watcher;
|
||||
int watching = pthread_create(&watcher, NULL, watch_main, &run) == 0;
|
||||
double t0 = os_now_ms();
|
||||
char err[512] = "";
|
||||
int rc = cst_agent_run_turn_ex(run.child, prompt, &h, err, sizeof(err));
|
||||
double ms = os_now_ms() - t0;
|
||||
atomic_store(&run.done, 1);
|
||||
if (watching)
|
||||
pthread_join(watcher, NULL);
|
||||
int cancelled = parent_cancel && *parent_cancel;
|
||||
|
||||
/* the report is the last assistant text after the prompt */
|
||||
cst_session_t *s = cst_agent_session(run.child);
|
||||
int n = cst_session_count(s), steps = 0;
|
||||
const char *final = NULL;
|
||||
int seen_user = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cst_message_t *m = cst_session_get(s, i);
|
||||
if (m->role == CST_ROLE_USER && !m->tool_call_id[0] && !seen_user) {
|
||||
seen_user = 1;
|
||||
continue;
|
||||
}
|
||||
if (!seen_user || m->role != CST_ROLE_ASSISTANT)
|
||||
continue;
|
||||
steps++;
|
||||
if (m->content && m->content[0] && !m->tool_calls_json)
|
||||
final = m->content;
|
||||
}
|
||||
long tokens = cst_agent_last_prompt_tokens(run.child);
|
||||
cst_buf_t b = {0};
|
||||
if (rc != 0)
|
||||
cst_buf_printf(&b, "error: sub-agent %s: %s\n", cancelled ? "cancelled"
|
||||
: "failed",
|
||||
err[0] ? err : "unknown error");
|
||||
if (final) {
|
||||
while (*final == ' ' || *final == '\n')
|
||||
final++;
|
||||
cst_buf_puts(&b, final);
|
||||
} else if (rc == 0) {
|
||||
cst_buf_puts(&b, "(the sub-agent ended without a final report; its "
|
||||
"transcript has the details)");
|
||||
}
|
||||
while (b.len && (b.buf[b.len - 1] == '\n' || b.buf[b.len - 1] == ' '))
|
||||
b.buf[--b.len] = 0;
|
||||
char tok[32] = "";
|
||||
if (tokens > 0)
|
||||
snprintf(tok, sizeof(tok), " · %.1fk tokens", (double)tokens / 1000.0);
|
||||
cst_buf_printf(&b,
|
||||
"\n\n[task %s · %d step%s · %d tool call%s · %.1f s%s · "
|
||||
"transcript .agent/sessions/%s/%s.jsonl]",
|
||||
role->name, steps, steps == 1 ? "" : "s", run.ntools,
|
||||
run.ntools == 1 ? "" : "s", ms / 1000.0, tok, pid, file);
|
||||
write_meta(dir, file, role->name, desc,
|
||||
rc == 0 ? "done" : cancelled ? "cancelled" : "error", steps,
|
||||
run.ntools, ms);
|
||||
|
||||
cst_agent_destroy(run.child);
|
||||
/* run_turn and destroy re-pointed this thread's cancel flag */
|
||||
os_set_cancel_flag(parent_cancel);
|
||||
cst_roles_free(roles, nr);
|
||||
free(roles);
|
||||
r->exit_code = rc == 0 ? 0 : cancelled ? 130 : 1;
|
||||
r->out_len = b.len;
|
||||
r->out = cst_buf_steal(&b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Read-only roles may run side by side. */
|
||||
static int task_parallel(const ctl_tool_call_t *c, void *ud) {
|
||||
(void)ud;
|
||||
const char *want = arg(c, "agent");
|
||||
if (!want || !want[0])
|
||||
return 0; /* general */
|
||||
for (int i = 0; i < NBUILTIN; i++)
|
||||
if (strcmp(BUILTIN[i].name, want) == 0)
|
||||
return BUILTIN[i].read_only;
|
||||
cst_agent_role_t *v = calloc(CST_ROLES_MAX, sizeof(*v));
|
||||
if (!v)
|
||||
return 0;
|
||||
int n = cst_roles_list(g_ws, v, CST_ROLES_MAX);
|
||||
int i = find_role(v, n, want);
|
||||
int ro = i >= 0 && v[i].read_only;
|
||||
cst_roles_free(v, n);
|
||||
free(v);
|
||||
return ro;
|
||||
}
|
||||
|
||||
int cst_subagent_register_tool(const char *ws) {
|
||||
snprintf(g_ws, sizeof(g_ws), "%s", ws ? ws : "");
|
||||
if (cst_tool_find("task"))
|
||||
return 0;
|
||||
cst_agent_role_t *v = calloc(CST_ROLES_MAX, sizeof(*v));
|
||||
if (!v)
|
||||
return -1;
|
||||
int n = cst_roles_list(g_ws, v, CST_ROLES_MAX);
|
||||
cst_buf_t names = {0};
|
||||
for (int i = 0; i < n; i++)
|
||||
cst_buf_printf(&names, "%s%s", i ? ", " : "", v[i].name);
|
||||
cst_roles_free(v, n);
|
||||
free(v);
|
||||
cst_buf_t schema = {0};
|
||||
cst_buf_printf(
|
||||
&schema,
|
||||
"{\"type\":\"object\",\"properties\":{"
|
||||
"\"description\":{\"type\":\"string\",\"description\":\"3-6 word "
|
||||
"summary shown to the user\"},"
|
||||
"\"prompt\":{\"type\":\"string\",\"description\":\"the complete task: "
|
||||
"goal, context, constraints and what to report back\"},"
|
||||
"\"agent\":{\"type\":\"string\",\"description\":\"one of: %s "
|
||||
"(default general)\"}},"
|
||||
"\"required\":[\"description\",\"prompt\"]}",
|
||||
names.buf ? names.buf : "general");
|
||||
cst_buf_free(&names);
|
||||
cst_tool_def_t d;
|
||||
memset(&d, 0, sizeof(d));
|
||||
d.name = "task";
|
||||
d.desc = "Hand a self-contained task to a sub-agent with its own context; "
|
||||
"you get back only its final report. Use it for broad searches "
|
||||
"or investigations that would flood your context, and for "
|
||||
"independent subtasks. It sees none of this conversation, so put "
|
||||
"everything it needs in prompt. Read-only agents (explore, plan) "
|
||||
"called together in one step run in parallel.";
|
||||
d.schema_json = schema.buf;
|
||||
d.perm = CST_TP_ASK;
|
||||
d.run_ex = task_run;
|
||||
d.parallel = task_parallel;
|
||||
d.owner = "builtin:task";
|
||||
int rc = cst_tool_register(&d);
|
||||
cst_buf_free(&schema);
|
||||
return rc;
|
||||
}
|
||||
+333
-89
@@ -50,6 +50,93 @@ static int is_regular(const char *full) {
|
||||
|
||||
/* --- read_file ----------------------------------------------------------- */
|
||||
|
||||
#define READ_LINE_MAX 2000
|
||||
|
||||
/* Default-profile read: "path · lines a-b of N" header, `N|` numbers,
|
||||
* long lines cut, and the next offset when more remains. */
|
||||
static int read_compact(FILE *fp, const char *path, long offset, long limit,
|
||||
int numbers, cst_tool_result_t *r) {
|
||||
if (offset <= 0)
|
||||
offset = 1;
|
||||
if (limit <= 0)
|
||||
limit = RANGE_DEFAULT_LINES;
|
||||
cst_buf_t b = {0}, line = {0};
|
||||
long lineno = 0, shown = 0, cut_lines = 0;
|
||||
int full = 0, binary = 0, ch;
|
||||
for (;;) {
|
||||
line.len = 0;
|
||||
int got = 0, cr = 0;
|
||||
size_t len = 0;
|
||||
while ((ch = fgetc(fp)) != EOF) {
|
||||
got = 1;
|
||||
if (ch == 0)
|
||||
binary = 1;
|
||||
if (ch == '\n')
|
||||
break;
|
||||
len++;
|
||||
cr = ch == '\r';
|
||||
if (line.len < READ_LINE_MAX + 8) {
|
||||
char cc = (char)ch;
|
||||
cst_buf_append(&line, &cc, 1);
|
||||
}
|
||||
}
|
||||
if (!got || binary)
|
||||
break;
|
||||
lineno++;
|
||||
if (lineno < offset || shown >= limit || full)
|
||||
continue;
|
||||
len -= (size_t)cr;
|
||||
if (b.len + (len < READ_LINE_MAX ? len : READ_LINE_MAX) > RANGE_OUT_MAX) {
|
||||
full = 1;
|
||||
continue;
|
||||
}
|
||||
if (numbers)
|
||||
cst_buf_printf(&b, "%ld|", lineno);
|
||||
if (len > READ_LINE_MAX) {
|
||||
size_t keep = cst_utf8_floor(line.buf, READ_LINE_MAX);
|
||||
cst_buf_append(&b, line.buf, keep);
|
||||
cst_buf_printf(&b, " … [line cut, +%zu bytes]\n", len - keep);
|
||||
cut_lines++;
|
||||
} else {
|
||||
if (len)
|
||||
cst_buf_append(&b, line.buf, len);
|
||||
cst_buf_puts(&b, "\n");
|
||||
}
|
||||
shown++;
|
||||
}
|
||||
cst_buf_free(&line);
|
||||
if (binary) {
|
||||
cst_buf_free(&b);
|
||||
return tool_set_resultf(r, 1,
|
||||
"error: %.200s is a binary file; inspect it with "
|
||||
"run_shell (file, xxd | head)",
|
||||
path);
|
||||
}
|
||||
long last = offset + shown - 1;
|
||||
cst_buf_t out = {0};
|
||||
if (lineno == 0)
|
||||
cst_buf_printf(&out, "%s · empty file\n", path);
|
||||
else if (shown == 0)
|
||||
cst_buf_printf(&out, "%s · %ld line%s; offset %ld is past the end\n", path,
|
||||
lineno, lineno == 1 ? "" : "s", offset);
|
||||
else if (offset == 1 && last == lineno)
|
||||
cst_buf_printf(&out, "%s · %ld line%s\n", path, lineno,
|
||||
lineno == 1 ? "" : "s");
|
||||
else
|
||||
cst_buf_printf(&out, "%s · lines %ld-%ld of %ld\n", path, offset, last,
|
||||
lineno);
|
||||
if (b.buf)
|
||||
cst_buf_append(&out, b.buf, b.len);
|
||||
cst_buf_free(&b);
|
||||
if (shown && last < lineno)
|
||||
cst_buf_printf(&out, "… %ld more line%s; continue with offset=%ld\n",
|
||||
lineno - last, lineno - last == 1 ? "" : "s", last + 1);
|
||||
if (cut_lines)
|
||||
cst_buf_printf(&out, "(%ld long line%s cut at %d bytes)\n", cut_lines,
|
||||
cut_lines == 1 ? "" : "s", READ_LINE_MAX);
|
||||
return tool_take_buf(r, &out, 0);
|
||||
}
|
||||
|
||||
int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
const char *path = tool_arg(c, "path");
|
||||
@@ -70,8 +157,13 @@ int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
|
||||
long offset = tool_arg_long(c, "offset", 0);
|
||||
long limit = tool_arg_long(c, "limit", 0);
|
||||
int numbers = tool_arg_bool(c, "line_numbers", 0);
|
||||
int numbers = tool_arg_bool(c, "line_numbers", ctx->compact);
|
||||
|
||||
if (ctx->compact) {
|
||||
int rc = read_compact(fp, path, offset, limit, numbers, r);
|
||||
fclose(fp);
|
||||
return rc;
|
||||
}
|
||||
if (offset <= 0 && limit <= 0 && !numbers) {
|
||||
fclose(fp);
|
||||
size_t n = 0;
|
||||
@@ -168,6 +260,87 @@ static size_t count_occ(const char *hay, size_t hn, const char *needle,
|
||||
return cnt;
|
||||
}
|
||||
|
||||
/* "12, 40, 77" — line numbers of the first few occurrences. */
|
||||
static void occ_lines(const char *hay, size_t hn, const char *nd, size_t nn,
|
||||
char *out, size_t out_sz) {
|
||||
out[0] = 0;
|
||||
size_t used = 0;
|
||||
long line = 1;
|
||||
size_t last = 0;
|
||||
int k = 0;
|
||||
for (size_t i = 0; nn && i + nn <= hn && k < 6;) {
|
||||
const char *p = memchr(hay + i, nd[0], hn - i - nn + 1);
|
||||
if (!p)
|
||||
break;
|
||||
i = (size_t)(p - hay);
|
||||
if (memcmp(p, nd, nn) != 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
for (size_t j = last; j < i; j++)
|
||||
line += hay[j] == '\n';
|
||||
last = i;
|
||||
int w = snprintf(out + used, out_sz - used, "%s%ld", k ? ", " : "", line);
|
||||
if (w < 0 || (size_t)w >= out_sz - used)
|
||||
break;
|
||||
used += (size_t)w;
|
||||
k++;
|
||||
i += nn;
|
||||
}
|
||||
}
|
||||
|
||||
static int trimmed_eq(const char *a, size_t an, const char *b, size_t bn) {
|
||||
while (an && isspace((unsigned char)*a))
|
||||
a++, an--;
|
||||
while (an && isspace((unsigned char)a[an - 1]))
|
||||
an--;
|
||||
while (bn && isspace((unsigned char)*b))
|
||||
b++, bn--;
|
||||
while (bn && isspace((unsigned char)b[bn - 1]))
|
||||
bn--;
|
||||
return an && an == bn && memcmp(a, b, an) == 0;
|
||||
}
|
||||
|
||||
/* old_string was not found: lines whose trimmed text equals old_string's
|
||||
* first non-blank line, as "42, 97" (empty when none). */
|
||||
static void near_lines(const char *hay, size_t hn, const char *old, size_t on,
|
||||
char *out, size_t out_sz) {
|
||||
out[0] = 0;
|
||||
const char *f = old;
|
||||
size_t fl = 0;
|
||||
for (size_t i = 0; i < on;) {
|
||||
const char *e = memchr(old + i, '\n', on - i);
|
||||
size_t end = e ? (size_t)(e - old) : on;
|
||||
int blank = 1;
|
||||
for (size_t j = i; j < end; j++)
|
||||
if (!isspace((unsigned char)old[j]))
|
||||
blank = 0;
|
||||
if (!blank) {
|
||||
f = old + i;
|
||||
fl = end - i;
|
||||
break;
|
||||
}
|
||||
i = end + 1;
|
||||
}
|
||||
if (!fl)
|
||||
return;
|
||||
size_t used = 0;
|
||||
long line = 1;
|
||||
int k = 0;
|
||||
for (size_t i = 0; i < hn && k < 5; line++) {
|
||||
const char *e = memchr(hay + i, '\n', hn - i);
|
||||
size_t end = e ? (size_t)(e - hay) : hn;
|
||||
if (trimmed_eq(hay + i, end - i, f, fl)) {
|
||||
int w = snprintf(out + used, out_sz - used, "%s%ld", k ? ", " : "", line);
|
||||
if (w < 0 || (size_t)w >= out_sz - used)
|
||||
break;
|
||||
used += (size_t)w;
|
||||
k++;
|
||||
}
|
||||
i = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static char *replace_all(const char *hay, size_t hn, const char *old,
|
||||
size_t on, const char *nw, size_t nwn, int all,
|
||||
size_t *out_len) {
|
||||
@@ -272,17 +445,29 @@ static int plan_edit(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
}
|
||||
int rc = 0;
|
||||
if (cnt == 0) {
|
||||
snprintf(err, err_sz,
|
||||
"error: old_string not found in %.200s; it must match the file "
|
||||
"exactly (whitespace and indentation included) — re-read the "
|
||||
"file and copy the text",
|
||||
path);
|
||||
char near[64];
|
||||
near_lines(pl->oldc, pl->olen, old, on, near, sizeof(near));
|
||||
if (near[0])
|
||||
snprintf(err, err_sz,
|
||||
"error: old_string not found in %.200s, but its first line "
|
||||
"appears (whitespace aside) at line %s — read_file around "
|
||||
"there and copy the exact text, indentation included",
|
||||
path, near);
|
||||
else
|
||||
snprintf(err, err_sz,
|
||||
"error: old_string not found in %.200s; it must match the file "
|
||||
"exactly (whitespace and indentation included) — re-read the "
|
||||
"file (or grep for a distinctive word) and copy the text",
|
||||
path);
|
||||
rc = -1;
|
||||
} else if (cnt > 1 && !all) {
|
||||
char where[64];
|
||||
occ_lines(pl->oldc, pl->olen, old, on, where, sizeof(where));
|
||||
snprintf(err, err_sz,
|
||||
"error: old_string matches %zu places in %.200s; include more "
|
||||
"surrounding lines to make it unique, or set replace_all=true",
|
||||
cnt, path);
|
||||
"error: old_string matches %zu places in %.200s (lines %s%s); "
|
||||
"include more surrounding lines to make it unique, or set "
|
||||
"replace_all=true",
|
||||
cnt, path, where, cnt > 6 ? ", …" : "");
|
||||
rc = -1;
|
||||
} else {
|
||||
pl->newc = replace_all(pl->oldc, pl->olen, old, on, nw, nwn, all,
|
||||
@@ -665,29 +850,54 @@ static int start_rel(const cst_tool_ctx_t *ctx, const char *path,
|
||||
}
|
||||
|
||||
/* list_dir */
|
||||
#define LIST_COUNT_MAX 100000
|
||||
|
||||
typedef struct {
|
||||
cst_buf_t b;
|
||||
int count;
|
||||
int base_depth;
|
||||
int truncated;
|
||||
int more; /* entries past LIST_MAX (counted, not shown) */
|
||||
int truncated; /* stopped counting */
|
||||
} list_ud_t;
|
||||
|
||||
/* Entries directly inside a directory (for the depth boundary). */
|
||||
static int dir_entries(const char *full) {
|
||||
DIR *d = opendir(full);
|
||||
if (!d)
|
||||
return -1;
|
||||
int n = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0)
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
return n;
|
||||
}
|
||||
|
||||
static int list_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
(void)full;
|
||||
list_ud_t *u = w->ud;
|
||||
if (u->count >= LIST_MAX) {
|
||||
u->truncated = 1;
|
||||
return 1;
|
||||
if (++u->more >= LIST_COUNT_MAX) {
|
||||
u->truncated = 1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const char *name = strrchr(rel, '/');
|
||||
name = name ? name + 1 : rel;
|
||||
for (int i = 0; i < depth; i++)
|
||||
cst_buf_puts(&u->b, " ");
|
||||
if (S_ISDIR(st->st_mode))
|
||||
cst_buf_printf(&u->b, "%s/%s\n", name,
|
||||
skip_dir_name(name) ? " (skipped)" : "");
|
||||
else if (S_ISLNK(st->st_mode))
|
||||
if (S_ISDIR(st->st_mode)) {
|
||||
int k = -1;
|
||||
if (skip_dir_name(name))
|
||||
cst_buf_printf(&u->b, "%s/ (skipped)\n", name);
|
||||
else if (depth + 1 >= w->max_depth && (k = dir_entries(full)) > 0)
|
||||
cst_buf_printf(&u->b, "%s/ (%d entr%s)\n", name, k,
|
||||
k == 1 ? "y" : "ies");
|
||||
else
|
||||
cst_buf_printf(&u->b, "%s/%s\n", name, k == 0 ? " (empty)" : "");
|
||||
} else if (S_ISLNK(st->st_mode))
|
||||
cst_buf_printf(&u->b, "%s@\n", name);
|
||||
else
|
||||
cst_buf_printf(&u->b, "%s (%lld B)\n", name, (long long)st->st_size);
|
||||
@@ -726,9 +936,10 @@ int cst_tool_list_dir(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
free(w.ig.v);
|
||||
if (u.count == 0)
|
||||
cst_buf_puts(&u.b, "(empty)\n");
|
||||
if (u.truncated)
|
||||
cst_buf_printf(&u.b, "... (stopped after %d entries; list a subdirectory)\n",
|
||||
LIST_MAX);
|
||||
if (u.more)
|
||||
cst_buf_printf(&u.b, "… %s%d more entries not shown (list a subdirectory "
|
||||
"or use a smaller depth)\n",
|
||||
u.truncated ? "over " : "", u.more);
|
||||
return tool_take_buf(r, &u.b, 0);
|
||||
}
|
||||
|
||||
@@ -824,15 +1035,16 @@ static int glob_cb(walker_t *w, const char *rel, const char *full,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static glob_ud_t *g_sort_ud;
|
||||
static int idx_by_mtime(const void *a, const void *b) {
|
||||
int ia = *(const int *)a, ib = *(const int *)b;
|
||||
double d = g_sort_ud->mtimes[ib] - g_sort_ud->mtimes[ia];
|
||||
if (d > 0)
|
||||
return 1;
|
||||
if (d < 0)
|
||||
return -1;
|
||||
return strcmp(g_sort_ud->hits[ia], g_sort_ud->hits[ib]);
|
||||
typedef struct {
|
||||
const char *path;
|
||||
double mtime;
|
||||
} glob_hit_t;
|
||||
|
||||
static int hit_by_mtime(const void *a, const void *b) {
|
||||
const glob_hit_t *x = a, *y = b;
|
||||
if (x->mtime != y->mtime)
|
||||
return x->mtime < y->mtime ? 1 : -1;
|
||||
return strcmp(x->path, y->path);
|
||||
}
|
||||
|
||||
int cst_tool_glob(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
@@ -865,22 +1077,24 @@ int cst_tool_glob(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
free(w.ig.v);
|
||||
|
||||
cst_buf_t b = {0};
|
||||
int *idx = malloc((size_t)(u.n ? u.n : 1) * sizeof(int));
|
||||
if (idx) {
|
||||
for (int i = 0; i < u.n; i++)
|
||||
idx[i] = i;
|
||||
g_sort_ud = &u;
|
||||
qsort(idx, (size_t)u.n, sizeof(int), idx_by_mtime);
|
||||
g_sort_ud = NULL;
|
||||
glob_hit_t *hs = malloc((size_t)(u.n ? u.n : 1) * sizeof(*hs));
|
||||
if (hs) {
|
||||
for (int i = 0; i < u.n; i++) {
|
||||
hs[i].path = u.hits[i];
|
||||
hs[i].mtime = u.mtimes[i];
|
||||
}
|
||||
qsort(hs, (size_t)u.n, sizeof(*hs), hit_by_mtime);
|
||||
for (int i = 0; i < u.n && i < GLOB_MAX; i++)
|
||||
cst_buf_printf(&b, "%s\n", u.hits[idx[i]]);
|
||||
cst_buf_printf(&b, "%s\n", hs[i].path);
|
||||
}
|
||||
free(idx);
|
||||
free(hs);
|
||||
if (u.n == 0)
|
||||
cst_buf_printf(&b, "(no files match %s)\n", pattern);
|
||||
else if (u.n > GLOB_MAX || u.truncated)
|
||||
cst_buf_printf(&b, "... (%s%d matches; showing the %d newest)\n",
|
||||
u.truncated ? "over " : "", u.n, GLOB_MAX);
|
||||
cst_buf_printf(&b, "… %s%d more files (showing the %d newest of %s%d; "
|
||||
"narrow the pattern or path)\n",
|
||||
u.truncated ? "over " : "", u.n - GLOB_MAX, GLOB_MAX,
|
||||
u.truncated ? "over " : "", u.n);
|
||||
for (int i = 0; i < u.n; i++)
|
||||
free(u.hits[i]);
|
||||
free(u.hits);
|
||||
@@ -930,19 +1144,24 @@ int cst_workspace_files(const char *ws, char ***out, int max) {
|
||||
return u.n;
|
||||
}
|
||||
|
||||
/* grep */
|
||||
/* grep: matches grouped by file, "N matches in F files" summary. */
|
||||
#define GREP_PER_FILE 20
|
||||
#define GREP_COUNT_MAX 5000
|
||||
|
||||
typedef struct {
|
||||
regex_t re;
|
||||
const char *glob;
|
||||
int context;
|
||||
cst_buf_t b;
|
||||
int matches;
|
||||
int files;
|
||||
int truncated;
|
||||
int matches; /* counted (up to GREP_COUNT_MAX) */
|
||||
int shown;
|
||||
int files; /* text files searched */
|
||||
int hit_files; /* files with a match */
|
||||
int truncated; /* stopped counting */
|
||||
} grep_ud_t;
|
||||
|
||||
static void put_line(cst_buf_t *b, const char *rel, long ln, char sep,
|
||||
const char *s, size_t n) {
|
||||
static void put_line(cst_buf_t *b, long ln, char sep, const char *s,
|
||||
size_t n) {
|
||||
if (n && s[n - 1] == '\n')
|
||||
n--;
|
||||
if (n && s[n - 1] == '\r')
|
||||
@@ -952,11 +1171,23 @@ static void put_line(cst_buf_t *b, const char *rel, long ln, char sep,
|
||||
n = cst_utf8_floor(s, GREP_LINE_MAX);
|
||||
cut = 1;
|
||||
}
|
||||
cst_buf_printf(b, "%s%c%ld%c", rel, sep, ln, sep);
|
||||
cst_buf_printf(b, "%ld%c", ln, sep);
|
||||
cst_buf_append(b, s, n);
|
||||
cst_buf_puts(b, cut ? " …\n" : "\n");
|
||||
}
|
||||
|
||||
static int line_hit(grep_ud_t *u, const char *s, size_t n) {
|
||||
char tmp[GREP_LINE_MAX * 8];
|
||||
size_t m = n < sizeof(tmp) - 1 ? n : sizeof(tmp) - 1;
|
||||
memcpy(tmp, s, m);
|
||||
tmp[m] = 0;
|
||||
if (m && tmp[m - 1] == '\n')
|
||||
tmp[--m] = 0;
|
||||
if (m && tmp[m - 1] == '\r')
|
||||
tmp[--m] = 0;
|
||||
return regexec(&u->re, tmp, 0, NULL, 0) == 0;
|
||||
}
|
||||
|
||||
static void grep_file(grep_ud_t *u, const char *rel, const char *full) {
|
||||
size_t n = 0;
|
||||
char *buf = tool_slurp(full, &n, READ_FILE_MAX);
|
||||
@@ -996,45 +1227,48 @@ static void grep_file(grep_ud_t *u, const char *rel, const char *full) {
|
||||
nl++;
|
||||
i = end;
|
||||
}
|
||||
int last_printed = -1;
|
||||
for (int i = 0; i < nl && !u->truncated; i++) {
|
||||
char tmp[GREP_LINE_MAX * 8];
|
||||
size_t m = ln[i] < sizeof(tmp) - 1 ? ln[i] : sizeof(tmp) - 1;
|
||||
memcpy(tmp, ls[i], m);
|
||||
tmp[m] = 0;
|
||||
if (m && tmp[m - 1] == '\n')
|
||||
tmp[--m] = 0;
|
||||
if (regexec(&u->re, tmp, 0, NULL, 0) != 0)
|
||||
cst_buf_t fb = {0};
|
||||
int fm = 0, fshown = 0, last_printed = -1;
|
||||
for (int i = 0; i < nl; i++) {
|
||||
if (!line_hit(u, ls[i], ln[i]))
|
||||
continue;
|
||||
if (u->matches >= GREP_MAX_MATCHES) {
|
||||
fm++;
|
||||
if (++u->matches >= GREP_COUNT_MAX)
|
||||
u->truncated = 1;
|
||||
if (fshown < GREP_PER_FILE && u->shown < GREP_MAX_MATCHES) {
|
||||
int from = i - u->context < 0 ? 0 : i - u->context;
|
||||
if (from <= last_printed)
|
||||
from = last_printed + 1;
|
||||
else if (u->context && last_printed >= 0)
|
||||
cst_buf_puts(&fb, "--\n");
|
||||
for (int k = from; k < i; k++)
|
||||
put_line(&fb, k + 1, '-', ls[k], ln[k]);
|
||||
put_line(&fb, i + 1, ':', ls[i], ln[i]);
|
||||
last_printed = i;
|
||||
for (int k = i + 1; k <= i + u->context && k < nl; k++) {
|
||||
if (line_hit(u, ls[k], ln[k]))
|
||||
break; /* printed by the main loop */
|
||||
put_line(&fb, k + 1, '-', ls[k], ln[k]);
|
||||
last_printed = k;
|
||||
}
|
||||
fshown++;
|
||||
u->shown++;
|
||||
}
|
||||
if (u->truncated)
|
||||
break;
|
||||
}
|
||||
int from = i - u->context < 0 ? 0 : i - u->context;
|
||||
if (from <= last_printed)
|
||||
from = last_printed + 1;
|
||||
else if (u->context && last_printed >= 0)
|
||||
cst_buf_puts(&u->b, "--\n");
|
||||
for (int k = from; k < i; k++)
|
||||
put_line(&u->b, rel, k + 1, '-', ls[k], ln[k]);
|
||||
put_line(&u->b, rel, i + 1, ':', ls[i], ln[i]);
|
||||
last_printed = i;
|
||||
u->matches++;
|
||||
int to = i + u->context;
|
||||
for (int k = i + 1; k <= to && k < nl; k++) {
|
||||
/* a later matching line is printed by the main loop */
|
||||
char t2[GREP_LINE_MAX * 8];
|
||||
size_t m2 = ln[k] < sizeof(t2) - 1 ? ln[k] : sizeof(t2) - 1;
|
||||
memcpy(t2, ls[k], m2);
|
||||
t2[m2] = 0;
|
||||
if (m2 && t2[m2 - 1] == '\n')
|
||||
t2[--m2] = 0;
|
||||
if (regexec(&u->re, t2, 0, NULL, 0) == 0)
|
||||
break;
|
||||
put_line(&u->b, rel, k + 1, '-', ls[k], ln[k]);
|
||||
last_printed = k;
|
||||
}
|
||||
}
|
||||
if (fm) {
|
||||
u->hit_files++;
|
||||
if (fm == 1)
|
||||
cst_buf_printf(&u->b, "%s\n", rel);
|
||||
else
|
||||
cst_buf_printf(&u->b, "%s (%d matches)\n", rel, fm);
|
||||
if (fb.buf)
|
||||
cst_buf_append(&u->b, fb.buf, fb.len);
|
||||
if (fm > fshown)
|
||||
cst_buf_printf(&u->b, "… %d more in this file\n", fm - fshown);
|
||||
}
|
||||
cst_buf_free(&fb);
|
||||
free(ls);
|
||||
free(ln);
|
||||
free(buf);
|
||||
@@ -1137,14 +1371,24 @@ int cst_tool_grep(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
free(w.ig.v);
|
||||
}
|
||||
regfree(&u.re);
|
||||
if (u.matches == 0)
|
||||
if (u.matches == 0) {
|
||||
cst_buf_free(&u.b);
|
||||
cst_buf_printf(&u.b, "(no matches in %d file%s)\n", u.files,
|
||||
u.files == 1 ? "" : "s");
|
||||
else if (u.truncated)
|
||||
cst_buf_printf(&u.b, "... (stopped at %d matches; narrow the pattern or "
|
||||
"path)\n",
|
||||
GREP_MAX_MATCHES);
|
||||
return tool_take_buf(r, &u.b, 0);
|
||||
return tool_take_buf(r, &u.b, 0);
|
||||
}
|
||||
cst_buf_t out = {0};
|
||||
cst_buf_printf(&out, "%s%d match%s in %d file%s", u.truncated ? "over " : "",
|
||||
u.matches, u.matches == 1 ? "" : "es", u.hit_files,
|
||||
u.hit_files == 1 ? "" : "s");
|
||||
if (u.shown < u.matches)
|
||||
cst_buf_printf(&out, " (showing %d; narrow with path/glob or a tighter "
|
||||
"pattern)",
|
||||
u.shown);
|
||||
cst_buf_puts(&out, "\n");
|
||||
cst_buf_append(&out, u.b.buf, u.b.len);
|
||||
cst_buf_free(&u.b);
|
||||
return tool_take_buf(r, &out, 0);
|
||||
}
|
||||
|
||||
#else /* _WIN32: directory tools are not implemented natively */
|
||||
|
||||
+328
-59
@@ -2,9 +2,13 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_tools.h"
|
||||
@@ -25,116 +29,135 @@ static int run_apply_patch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
}
|
||||
static int run_shell(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
return cst_tool_run_shell_ex(ctx->workspace_root, c, r, ctx->sandbox);
|
||||
int rc = cst_tool_run_shell_ex(ctx->workspace_root, c, r, ctx->sandbox);
|
||||
if (rc == 0 && ctx->compact && r->out) {
|
||||
size_t n = 0;
|
||||
char *s = cst_shell_compact(r->out, r->out_len, &n);
|
||||
if (s) {
|
||||
free(r->out);
|
||||
r->out = s;
|
||||
r->out_len = n;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
#define S "string"
|
||||
#define I "integer"
|
||||
#define B "boolean"
|
||||
#define NO_DYN NULL, NULL, NULL, NULL, NULL
|
||||
|
||||
static const cst_tool_def_t TOOLS[] = {
|
||||
{"read_file",
|
||||
"Read a UTF-8 text file. For big files pass offset (first line, "
|
||||
"1-based) and limit (number of lines); line_numbers=true prefixes each "
|
||||
"line with its number.",
|
||||
"Read a text file. Output starts with `path · lines a-b of N` and "
|
||||
"prefixes lines with `N|` (not part of the file — never copy it into "
|
||||
"edits). Returns up to 2000 lines; continue with the offset it prints.",
|
||||
"Read a UTF-8 text file under the workspace.",
|
||||
{{"path", S, 1, "file path relative to the workspace"},
|
||||
{"offset", I, 0, "first line to return (1-based)"},
|
||||
{"limit", I, 0, "maximum number of lines"},
|
||||
{"line_numbers", B, 0, "prefix lines with their numbers"}},
|
||||
CST_TP_READ, 1, run_read_file, NULL},
|
||||
{"line_numbers", B, 0, "`N|` line prefixes (default true)"}},
|
||||
CST_TP_READ, 1, run_read_file, NULL, NO_DYN},
|
||||
{"run_shell",
|
||||
"Run a non-interactive shell command in the workspace root (stdin is "
|
||||
"empty; long-running commands are killed). Returns stdout+stderr.",
|
||||
"Run a non-interactive shell command in the workspace root for builds, "
|
||||
"tests, git and other CLIs (stdin is empty; long runs are killed). "
|
||||
"Returns stdout+stderr with colors stripped and long output cut to head "
|
||||
"+ tail. Prefer read_file/grep/glob/list_dir over cat/grep/find.",
|
||||
"Run a shell command in the workspace root (non-interactive, stdin is "
|
||||
"empty, killed after a timeout). Returns combined stdout+stderr.",
|
||||
{{"cmd", S, 1, "shell command"}},
|
||||
CST_TP_SHELL, 1, run_shell, NULL},
|
||||
CST_TP_SHELL, 1, run_shell, NULL, NO_DYN},
|
||||
{"write_file",
|
||||
"Create or overwrite a whole file (parent directories are created).",
|
||||
"Create a file or replace a whole file (parent directories are "
|
||||
"created). For changes to existing files prefer edit_file.",
|
||||
"Create or overwrite a whole file under the workspace (parent "
|
||||
"directories are created).",
|
||||
{{"path", S, 1, "file path"}, {"content", S, 1, "complete new content"}},
|
||||
CST_TP_WRITE, 1, run_write_file, cst_preview_write_file},
|
||||
CST_TP_WRITE, 1, run_write_file, cst_preview_write_file, NO_DYN},
|
||||
{"apply_patch",
|
||||
"Apply a unified diff (---/+++ headers, @@ -a,b +c,d @@ hunks with 3 "
|
||||
"context lines) to one or more files; all hunks apply or nothing "
|
||||
"changes.",
|
||||
"Apply a unified diff (---/+++ headers, @@ -a,b +c,d @@ hunks, 3 "
|
||||
"context lines) across one or more files; all hunks apply or nothing "
|
||||
"changes. Use for multi-hunk or multi-file edits.",
|
||||
"Apply a unified diff (---/+++ headers, @@ -a,b +c,d @@ hunks with "
|
||||
"context lines) to one or more files. All hunks must apply or nothing "
|
||||
"changes.",
|
||||
{{"patch", S, 1, "unified diff text"}},
|
||||
CST_TP_WRITE, 1, run_apply_patch, cst_preview_apply_patch},
|
||||
CST_TP_WRITE, 1, run_apply_patch, cst_preview_apply_patch, NO_DYN},
|
||||
{"edit_file",
|
||||
"Replace an exact snippet in a file. old_string must match the file "
|
||||
"byte-for-byte (including indentation) and be unique unless "
|
||||
"replace_all=true. Best for small edits.",
|
||||
"Replace an exact snippet in a file — the default for small edits. "
|
||||
"old_string must match byte-for-byte (indentation included, without "
|
||||
"read_file's `N|` prefixes) and be unique unless replace_all=true.",
|
||||
NULL,
|
||||
{{"path", S, 1, "file path"},
|
||||
{"old_string", S, 1, "exact text to replace"},
|
||||
{"new_string", S, 1, "replacement text"},
|
||||
{"replace_all", B, 0, "replace every occurrence"}},
|
||||
CST_TP_WRITE, 0, cst_tool_edit_file, cst_preview_edit_file},
|
||||
CST_TP_WRITE, 0, cst_tool_edit_file, cst_preview_edit_file, NO_DYN},
|
||||
{"list_dir",
|
||||
"List a directory as a tree (directories end with /). Skips .git, "
|
||||
"node_modules, .agent and build output.",
|
||||
"Show a directory tree (dirs end with /, files show sizes, dirs at the "
|
||||
"depth limit show entry counts). Skips .git, node_modules and "
|
||||
".gitignore'd paths.",
|
||||
NULL,
|
||||
{{"path", S, 0, "directory (default: workspace root)"},
|
||||
{"depth", I, 0, "levels to descend (default 2, max 6)"}},
|
||||
CST_TP_READ, 0, cst_tool_list_dir, NULL},
|
||||
CST_TP_READ, 0, cst_tool_list_dir, NULL, NO_DYN},
|
||||
{"glob",
|
||||
"Find files by name pattern, newest first. `*` and `?` stay within one "
|
||||
"path segment, `**` spans directories (`src/**/*.c`); a pattern "
|
||||
"without / matches file names at any depth.",
|
||||
"Find files by name, newest first (max 200). `*`/`?` stay within one "
|
||||
"path segment, `**` spans directories (`src/**/*.c`); a pattern without "
|
||||
"/ matches file names at any depth.",
|
||||
NULL,
|
||||
{{"pattern", S, 1, "glob relative to path"},
|
||||
{"path", S, 0, "directory to search (default: workspace root)"}},
|
||||
CST_TP_READ, 0, cst_tool_glob, NULL},
|
||||
CST_TP_READ, 0, cst_tool_glob, NULL, NO_DYN},
|
||||
{"grep",
|
||||
"Search file contents with a POSIX extended regex. Returns "
|
||||
"file:line:text matches.",
|
||||
"Search file contents with an extended regex (\\d \\w \\s work). "
|
||||
"Results are grouped by file as `line:text` under each path, with a "
|
||||
"total count first; narrow with path/glob when it is cut.",
|
||||
NULL,
|
||||
{{"pattern", S, 1, "extended regular expression"},
|
||||
{"path", S, 0, "file or directory (default: workspace root)"},
|
||||
{"glob", S, 0, "only files whose name matches, e.g. *.c"},
|
||||
{"ignore_case", B, 0, "case-insensitive"},
|
||||
{"context", I, 0, "lines of context around each match (max 5)"}},
|
||||
CST_TP_READ, 0, cst_tool_grep, NULL},
|
||||
CST_TP_READ, 0, cst_tool_grep, NULL, NO_DYN},
|
||||
{"delete_file",
|
||||
"Delete a file (not directories).",
|
||||
"Delete one file (not directories). Undoable with /undo.",
|
||||
NULL,
|
||||
{{"path", S, 1, "file path"}},
|
||||
CST_TP_WRITE, 0, cst_tool_delete_file, cst_preview_delete_file},
|
||||
CST_TP_WRITE, 0, cst_tool_delete_file, cst_preview_delete_file, NO_DYN},
|
||||
{"move_file",
|
||||
"Move or rename a file; fails if the destination exists.",
|
||||
NULL,
|
||||
{{"from", S, 1, "existing file"}, {"to", S, 1, "new path"}},
|
||||
CST_TP_WRITE, 0, cst_tool_move_file, cst_preview_move_file},
|
||||
CST_TP_WRITE, 0, cst_tool_move_file, cst_preview_move_file, NO_DYN},
|
||||
{"todo_write",
|
||||
"Replace your task list for multi-step work. One item per line: "
|
||||
"`[ ] todo`, `[~] in progress`, `[x] done`.",
|
||||
"Replace your task list for work with 3+ steps; keep exactly one item "
|
||||
"in progress. One item per line: `[ ] todo`, `[~] in progress`, `[x] "
|
||||
"done`.",
|
||||
NULL,
|
||||
{{"todos", S, 1, "the whole list, one item per line"}},
|
||||
CST_TP_META, 0, cst_tool_todo_write, NULL},
|
||||
CST_TP_META, 0, cst_tool_todo_write, NULL, NO_DYN},
|
||||
{"web_fetch",
|
||||
"Fetch an http(s) URL and return its text (HTML is reduced to text).",
|
||||
"Fetch an http(s) URL (docs, issues, APIs) and return its text; HTML "
|
||||
"is reduced to readable text.",
|
||||
NULL,
|
||||
{{"url", S, 1, "http:// or https:// URL"},
|
||||
{"max_bytes", I, 0, "maximum bytes to return (default 65536)"}},
|
||||
CST_TP_NET, 0, cst_tool_web_fetch, NULL},
|
||||
CST_TP_NET, 0, cst_tool_web_fetch, NULL, NO_DYN},
|
||||
{"ask_user",
|
||||
"Ask the user a question and wait for the answer. Use only for "
|
||||
"decisions that are truly the user's.",
|
||||
"Ask the user and wait for the answer. Only for decisions that are "
|
||||
"truly theirs (preferences, credentials, destructive choices).",
|
||||
NULL,
|
||||
{{"question", S, 1, "the question"},
|
||||
{"options", S, 0, "suggested answers, one per line"}},
|
||||
CST_TP_ASK, 0, cst_tool_ask_user, NULL},
|
||||
CST_TP_ASK, 0, cst_tool_ask_user, NULL, NO_DYN},
|
||||
};
|
||||
#define NTOOLS ((int)(sizeof(TOOLS) / sizeof(TOOLS[0])))
|
||||
|
||||
#undef S
|
||||
#undef I
|
||||
#undef B
|
||||
#undef NO_DYN
|
||||
|
||||
int cst_tool_profile_parse(const char *s, int *out) {
|
||||
if (!s || !out)
|
||||
@@ -154,17 +177,38 @@ const char *cst_tool_profile_name(int p) {
|
||||
return p == CST_TOOLS_MINIMAL ? "minimal" : "default";
|
||||
}
|
||||
|
||||
int cst_tool_count(void) { return NTOOLS; }
|
||||
/* Dynamic tools: published append-only into a fixed table so readers on
|
||||
* other threads never see a half-built entry. */
|
||||
#define DYN_MAX 256
|
||||
static cst_tool_def_t *g_dyn[DYN_MAX];
|
||||
static atomic_int g_ndyn;
|
||||
#ifndef _WIN32
|
||||
static pthread_mutex_t g_dyn_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define DYN_LOCK() pthread_mutex_lock(&g_dyn_mu)
|
||||
#define DYN_UNLOCK() pthread_mutex_unlock(&g_dyn_mu)
|
||||
#else
|
||||
#define DYN_LOCK() ((void)0)
|
||||
#define DYN_UNLOCK() ((void)0)
|
||||
#endif
|
||||
|
||||
int cst_tool_count(void) { return NTOOLS + atomic_load(&g_ndyn); }
|
||||
|
||||
const cst_tool_def_t *cst_tool_at(int i) {
|
||||
return i >= 0 && i < NTOOLS ? &TOOLS[i] : NULL;
|
||||
if (i < 0)
|
||||
return NULL;
|
||||
if (i < NTOOLS)
|
||||
return &TOOLS[i];
|
||||
i -= NTOOLS;
|
||||
return i < atomic_load(&g_ndyn) ? g_dyn[i] : NULL;
|
||||
}
|
||||
|
||||
int cst_tool_index(const char *name) {
|
||||
if (!name)
|
||||
return -1;
|
||||
for (int i = 0; i < NTOOLS; i++) {
|
||||
if (strcmp(TOOLS[i].name, name) == 0)
|
||||
int n = cst_tool_count();
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cst_tool_def_t *t = cst_tool_at(i);
|
||||
if (t && strcmp(t->name, name) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
@@ -176,6 +220,109 @@ const cst_tool_def_t *cst_tool_find(const char *name) {
|
||||
|
||||
int cst_tool_known(const char *name) { return cst_tool_find(name) != NULL; }
|
||||
|
||||
void cst_tool_sanitize_name(const char *in, char *out, size_t out_sz) {
|
||||
size_t o = 0;
|
||||
for (size_t i = 0; in && in[i] && o + 1 < out_sz && o < 63; i++) {
|
||||
char c = in[i];
|
||||
int ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-';
|
||||
out[o++] = ok ? c : '_';
|
||||
}
|
||||
if (out_sz)
|
||||
out[o] = 0;
|
||||
}
|
||||
|
||||
static char *dup_or_null(const char *s) { return s ? strdup(s) : NULL; }
|
||||
|
||||
static void dyn_free(cst_tool_def_t *t) {
|
||||
if (!t)
|
||||
return;
|
||||
free((char *)t->name);
|
||||
free((char *)t->desc);
|
||||
free((char *)t->schema_json);
|
||||
free((char *)t->owner);
|
||||
free(t);
|
||||
}
|
||||
|
||||
int cst_tool_register(const cst_tool_def_t *def) {
|
||||
if (!def || !def->name || !def->name[0] || (!def->run && !def->run_ex))
|
||||
return -1;
|
||||
DYN_LOCK();
|
||||
int rc = -1;
|
||||
int n = atomic_load(&g_ndyn);
|
||||
if (n < DYN_MAX && cst_tool_index(def->name) < 0) {
|
||||
cst_tool_def_t *t = calloc(1, sizeof(*t));
|
||||
if (t) {
|
||||
*t = *def;
|
||||
t->min_desc = NULL;
|
||||
t->minimal = 0;
|
||||
t->name = dup_or_null(def->name);
|
||||
t->desc = dup_or_null(def->desc ? def->desc : "");
|
||||
t->schema_json = dup_or_null(def->schema_json);
|
||||
t->owner = dup_or_null(def->owner);
|
||||
if (t->name && t->desc) {
|
||||
g_dyn[n] = t;
|
||||
atomic_store(&g_ndyn, n + 1);
|
||||
rc = 0;
|
||||
} else {
|
||||
dyn_free(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
DYN_UNLOCK();
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cst_tool_unregister_owner(const char *owner) {
|
||||
if (!owner)
|
||||
return 0;
|
||||
DYN_LOCK();
|
||||
int n = atomic_load(&g_ndyn), k = 0, removed = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (g_dyn[i]->owner && strcmp(g_dyn[i]->owner, owner) == 0) {
|
||||
dyn_free(g_dyn[i]);
|
||||
removed++;
|
||||
} else {
|
||||
g_dyn[k++] = g_dyn[i];
|
||||
}
|
||||
}
|
||||
for (int i = k; i < n; i++)
|
||||
g_dyn[i] = NULL;
|
||||
atomic_store(&g_ndyn, k);
|
||||
DYN_UNLOCK();
|
||||
return removed;
|
||||
}
|
||||
|
||||
int cst_tool_disabled(const cst_agent_config_t *cfg, const char *name) {
|
||||
if (!cfg || !name || !cfg->tools_off[0])
|
||||
return 0;
|
||||
char key[CTL_MAX_NAME + 2];
|
||||
snprintf(key, sizeof(key), ",%s,", name);
|
||||
return strstr(cfg->tools_off, key) != NULL;
|
||||
}
|
||||
|
||||
void cst_tool_set_disabled(cst_agent_config_t *cfg, const char *name,
|
||||
int off) {
|
||||
if (!cfg || !name || !name[0])
|
||||
return;
|
||||
char key[CTL_MAX_NAME + 2];
|
||||
snprintf(key, sizeof(key), ",%s,", name);
|
||||
char *hit = strstr(cfg->tools_off, key);
|
||||
if (off && !hit) {
|
||||
size_t n = strlen(cfg->tools_off);
|
||||
if (n == 0)
|
||||
snprintf(cfg->tools_off, sizeof(cfg->tools_off), "%s", key);
|
||||
else if (n + strlen(key) < sizeof(cfg->tools_off))
|
||||
snprintf(cfg->tools_off + n, sizeof(cfg->tools_off) - n, "%s", key + 1);
|
||||
} else if (!off && hit) {
|
||||
/* drop ",name" keeping the trailing comma */
|
||||
size_t kl = strlen(key) - 1;
|
||||
memmove(hit, hit + kl, strlen(hit + kl) + 1);
|
||||
if (strcmp(cfg->tools_off, ",") == 0)
|
||||
cfg->tools_off[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int cst_tool_is_shell(const char *name) {
|
||||
const cst_tool_def_t *t = cst_tool_find(name);
|
||||
return t && t->perm == CST_TP_SHELL;
|
||||
@@ -193,10 +340,80 @@ int cst_tool_active(const cst_agent_config_t *cfg, const cst_tool_def_t *t) {
|
||||
return 1;
|
||||
if (cfg->tool_profile == CST_TOOLS_MINIMAL && !t->minimal)
|
||||
return 0;
|
||||
if (cfg->plan_mode && (t->perm == CST_TP_WRITE || t->perm == CST_TP_SHELL))
|
||||
if ((cfg->plan_mode || cfg->read_only) &&
|
||||
(t->perm == CST_TP_WRITE || t->perm == CST_TP_SHELL))
|
||||
return 0;
|
||||
int i = (int)(t - TOOLS);
|
||||
return !(cfg->tools_off & (1ULL << i));
|
||||
if (cfg->depth > 0 && strcmp(t->name, "task") == 0)
|
||||
return 0;
|
||||
if (cfg->tools_only[0]) {
|
||||
char key[CTL_MAX_NAME + 2];
|
||||
snprintf(key, sizeof(key), ",%s,", t->name);
|
||||
if (!strstr(cfg->tools_only, key))
|
||||
return 0;
|
||||
}
|
||||
return !cst_tool_disabled(cfg, t->name);
|
||||
}
|
||||
|
||||
/* Parsed schema of a dynamic tool, or NULL. */
|
||||
static cJSON *tool_schema(const cst_tool_def_t *t) {
|
||||
if (!t->schema_json)
|
||||
return NULL;
|
||||
cJSON *s = cJSON_Parse(t->schema_json);
|
||||
if (s && !cJSON_IsObject(s)) {
|
||||
cJSON_Delete(s);
|
||||
return NULL;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static int schema_required(const cJSON *schema, const char *key) {
|
||||
const cJSON *req = cJSON_GetObjectItemCaseSensitive(schema, "required");
|
||||
const cJSON *it = NULL;
|
||||
cJSON_ArrayForEach(it, req) {
|
||||
if (cJSON_IsString(it) && strcmp(it->valuestring, key) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *cst_tool_args_json(const cst_tool_def_t *t, const ctl_tool_call_t *c) {
|
||||
cJSON *schema = t ? tool_schema(t) : NULL;
|
||||
const cJSON *props = cJSON_GetObjectItemCaseSensitive(schema, "properties");
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
for (int i = 0; i < c->nargs; i++) {
|
||||
const char *k = c->args[i].key;
|
||||
const char *v = c->args[i].value ? c->args[i].value : "";
|
||||
const char *type = NULL;
|
||||
const cJSON *pp = cJSON_GetObjectItemCaseSensitive(props, k);
|
||||
const cJSON *ty = cJSON_GetObjectItemCaseSensitive(pp, "type");
|
||||
if (cJSON_IsString(ty))
|
||||
type = ty->valuestring;
|
||||
for (int j = 0; !type && t && j < CST_TOOL_MAX_PARAMS && t->params[j].name;
|
||||
j++) {
|
||||
if (strcmp(t->params[j].name, k) == 0)
|
||||
type = t->params[j].type;
|
||||
}
|
||||
cJSON *val = NULL;
|
||||
if (!type || strcmp(type, "string") != 0) {
|
||||
const char *p = v;
|
||||
while (*p == ' ' || *p == '\n')
|
||||
p++;
|
||||
/* untyped: only structured values are taken as JSON */
|
||||
if (type || *p == '{' || *p == '[')
|
||||
val = cJSON_Parse(v);
|
||||
if (val && type && strcmp(type, "integer") == 0 && !cJSON_IsNumber(val)) {
|
||||
cJSON_Delete(val);
|
||||
val = NULL;
|
||||
}
|
||||
}
|
||||
if (!val)
|
||||
val = cJSON_CreateString(v);
|
||||
cJSON_AddItemToObject(o, k, val);
|
||||
}
|
||||
char *s = cJSON_PrintUnformatted(o);
|
||||
cJSON_Delete(o);
|
||||
cJSON_Delete(schema);
|
||||
return s;
|
||||
}
|
||||
|
||||
static int is_minimal(const cst_agent_config_t *cfg) {
|
||||
@@ -208,9 +425,10 @@ void *cst_tools_schema_cfg(const cst_agent_config_t *cfg) {
|
||||
if (!arr)
|
||||
return NULL;
|
||||
int legacy = is_minimal(cfg);
|
||||
for (int i = 0; i < NTOOLS; i++) {
|
||||
const cst_tool_def_t *t = &TOOLS[i];
|
||||
if (!cst_tool_active(cfg, t))
|
||||
int n = cst_tool_count();
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cst_tool_def_t *t = cst_tool_at(i);
|
||||
if (!t || !cst_tool_active(cfg, t))
|
||||
continue;
|
||||
cJSON *tool = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(tool, "type", "function");
|
||||
@@ -218,6 +436,17 @@ void *cst_tools_schema_cfg(const cst_agent_config_t *cfg) {
|
||||
cJSON_AddStringToObject(fn, "name", t->name);
|
||||
cJSON_AddStringToObject(fn, "description",
|
||||
legacy && t->min_desc ? t->min_desc : t->desc);
|
||||
cJSON *schema = tool_schema(t);
|
||||
if (t->schema_json) {
|
||||
if (!schema) {
|
||||
schema = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(schema, "type", "object");
|
||||
cJSON_AddObjectToObject(schema, "properties");
|
||||
}
|
||||
cJSON_AddItemToObject(fn, "parameters", schema);
|
||||
cJSON_AddItemToArray(arr, tool);
|
||||
continue;
|
||||
}
|
||||
cJSON *params = cJSON_AddObjectToObject(fn, "parameters");
|
||||
cJSON_AddStringToObject(params, "type", "object");
|
||||
cJSON *props = cJSON_AddObjectToObject(params, "properties");
|
||||
@@ -243,25 +472,47 @@ void *cst_tools_schema(void) { return cst_tools_schema_cfg(NULL); }
|
||||
char *cst_tools_prompt(const cst_agent_config_t *cfg) {
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, "Tools:\n");
|
||||
for (int i = 0; i < NTOOLS; i++) {
|
||||
const cst_tool_def_t *t = &TOOLS[i];
|
||||
if (!cst_tool_active(cfg, t))
|
||||
int n = cst_tool_count();
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cst_tool_def_t *t = cst_tool_at(i);
|
||||
if (!t || !cst_tool_active(cfg, t))
|
||||
continue;
|
||||
cst_buf_printf(&b, "- %s(", t->name);
|
||||
for (int k = 0; k < CST_TOOL_MAX_PARAMS && t->params[k].name; k++)
|
||||
if (t->schema_json) {
|
||||
cJSON *schema = tool_schema(t);
|
||||
const cJSON *props =
|
||||
cJSON_GetObjectItemCaseSensitive(schema, "properties");
|
||||
const cJSON *it = NULL;
|
||||
int k = 0;
|
||||
cJSON_ArrayForEach(it, props) {
|
||||
cst_buf_printf(&b, "%s%s%s", k++ ? ", " : "", it->string,
|
||||
schema_required(schema, it->string) ? "" : "?");
|
||||
}
|
||||
cJSON_Delete(schema);
|
||||
}
|
||||
for (int k = 0; !t->schema_json && k < CST_TOOL_MAX_PARAMS &&
|
||||
t->params[k].name;
|
||||
k++)
|
||||
cst_buf_printf(&b, "%s%s%s", k ? ", " : "", t->params[k].name,
|
||||
t->params[k].required ? "" : "?");
|
||||
cst_buf_printf(&b, "): %s\n", t->desc);
|
||||
/* first line of a long description is enough in the prompt */
|
||||
const char *d = t->desc ? t->desc : "";
|
||||
size_t dl = strlen(d);
|
||||
if (t->schema_json && dl > 300)
|
||||
dl = cst_utf8_floor(d, 300);
|
||||
cst_buf_printf(&b, "): %.*s%s\n", (int)dl, d,
|
||||
dl < strlen(d) ? "…" : "");
|
||||
}
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
char *cst_tools_names(const cst_agent_config_t *cfg) {
|
||||
cst_buf_t b = {0};
|
||||
int n = 0;
|
||||
for (int i = 0; i < NTOOLS; i++) {
|
||||
if (cst_tool_active(cfg, &TOOLS[i]))
|
||||
cst_buf_printf(&b, "%s%s", n++ ? ", " : "", TOOLS[i].name);
|
||||
int n = 0, cnt = cst_tool_count();
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
const cst_tool_def_t *t = cst_tool_at(i);
|
||||
if (t && cst_tool_active(cfg, t))
|
||||
cst_buf_printf(&b, "%s%s", n++ ? ", " : "", t->name);
|
||||
}
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
@@ -285,6 +536,22 @@ static int parse_int_value(const char *v, long *out) {
|
||||
|
||||
int cst_tool_validate(const cst_tool_def_t *t, const ctl_tool_call_t *call,
|
||||
char *err, size_t err_sz) {
|
||||
if (t->schema_json) {
|
||||
cJSON *schema = tool_schema(t);
|
||||
const cJSON *req = cJSON_GetObjectItemCaseSensitive(schema, "required");
|
||||
const cJSON *it = NULL;
|
||||
int rc = 0;
|
||||
cJSON_ArrayForEach(it, req) {
|
||||
if (cJSON_IsString(it) && !tool_arg(call, it->valuestring)) {
|
||||
snprintf(err, err_sz, "error: %s needs argument \"%s\"", t->name,
|
||||
it->valuestring);
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(schema);
|
||||
return rc;
|
||||
}
|
||||
for (int k = 0; k < CST_TOOL_MAX_PARAMS && t->params[k].name; k++) {
|
||||
const cst_tool_param_t *p = &t->params[k];
|
||||
const char *v = tool_arg(call, p->name);
|
||||
@@ -319,6 +586,8 @@ int cst_tool_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *call,
|
||||
char err[256];
|
||||
if (cst_tool_validate(t, call, err, sizeof(err)) != 0)
|
||||
return tool_set_result(r, err, 1);
|
||||
if (t->run_ex)
|
||||
return t->run_ex(ctx, call, r, t->ud);
|
||||
return t->run(ctx, call, r);
|
||||
}
|
||||
|
||||
|
||||
@@ -323,6 +323,170 @@ int cst_tool_run_shell_ex(const char *workspace_root,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define SHC_LINE_MAX 2000
|
||||
#define SHC_HEAD 80
|
||||
#define SHC_TAIL 120
|
||||
#define SHC_BYTES_MAX (40 * 1024)
|
||||
|
||||
/* One output line: escapes removed and only what follows the last lone
|
||||
* `\r` kept (what a terminal would finally show). */
|
||||
static void shc_clean_line(const char *s, size_t n, cst_buf_t *out) {
|
||||
size_t start = out->len;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
unsigned char ch = (unsigned char)s[i];
|
||||
if (ch == 0x1b && i + 1 < n) {
|
||||
unsigned char k = (unsigned char)s[i + 1];
|
||||
if (k == '[') {
|
||||
i += 2;
|
||||
while (i < n && ((unsigned char)s[i] < 0x40 || (unsigned char)s[i] > 0x7e))
|
||||
i++;
|
||||
} else if (k == ']') {
|
||||
i += 2;
|
||||
while (i < n && s[i] != 0x07 &&
|
||||
!(s[i] == 0x1b && i + 1 < n && s[i + 1] == '\\'))
|
||||
i++;
|
||||
if (i < n && s[i] == 0x1b)
|
||||
i++;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch == '\r') {
|
||||
if (i + 1 < n)
|
||||
out->len = start;
|
||||
continue;
|
||||
}
|
||||
if (ch == '\b') {
|
||||
if (out->len > start)
|
||||
out->len--;
|
||||
continue;
|
||||
}
|
||||
if (ch < 0x20 && ch != '\t')
|
||||
continue;
|
||||
cst_buf_append(out, (const char *)&s[i], 1);
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
size_t off, len;
|
||||
} shc_line_t;
|
||||
|
||||
static void shc_put(cst_buf_t *b, const char *s, size_t n) {
|
||||
if (n > SHC_LINE_MAX) {
|
||||
size_t keep = cst_utf8_floor(s, SHC_LINE_MAX);
|
||||
cst_buf_append(b, s, keep);
|
||||
cst_buf_printf(b, " … [+%zu bytes]\n", n - keep);
|
||||
return;
|
||||
}
|
||||
cst_buf_append(b, s, n);
|
||||
cst_buf_puts(b, "\n");
|
||||
}
|
||||
|
||||
char *cst_shell_compact(const char *s, size_t n, size_t *out_len) {
|
||||
cst_buf_t clean = {0};
|
||||
shc_line_t *lines = NULL;
|
||||
size_t nl = 0, cap = 0;
|
||||
for (size_t i = 0; i < n;) {
|
||||
const char *e = memchr(s + i, '\n', n - i);
|
||||
size_t end = e ? (size_t)(e - s) : n;
|
||||
size_t ln = end - i;
|
||||
if (ln && s[i + ln - 1] == '\r')
|
||||
ln--;
|
||||
if (nl == cap) {
|
||||
cap = cap ? cap * 2 : 256;
|
||||
shc_line_t *nv = realloc(lines, cap * sizeof(*nv));
|
||||
if (!nv)
|
||||
break;
|
||||
lines = nv;
|
||||
}
|
||||
size_t off = clean.len;
|
||||
shc_clean_line(s + i, ln, &clean);
|
||||
lines[nl].off = off;
|
||||
lines[nl].len = clean.len - off;
|
||||
nl++;
|
||||
i = e ? end + 1 : n;
|
||||
}
|
||||
/* Collapse runs of identical lines: keep one plus a count. */
|
||||
size_t m = 0;
|
||||
int *rep = calloc(nl ? nl : 1, sizeof(int));
|
||||
for (size_t i = 0; rep && i < nl;) {
|
||||
size_t j = i + 1;
|
||||
while (j < nl && lines[j].len == lines[i].len &&
|
||||
memcmp(clean.buf + lines[j].off, clean.buf + lines[i].off,
|
||||
lines[i].len) == 0)
|
||||
j++;
|
||||
lines[m] = lines[i];
|
||||
rep[m] = (int)(j - i);
|
||||
m++;
|
||||
if (j - i == 2) {
|
||||
lines[m] = lines[i];
|
||||
rep[m - 1] = rep[m] = 1;
|
||||
m++;
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
if (!rep)
|
||||
m = nl;
|
||||
size_t total = 0;
|
||||
for (size_t i = 0; i < m; i++)
|
||||
total += (lines[i].len < SHC_LINE_MAX ? lines[i].len : SHC_LINE_MAX) + 1;
|
||||
int cut = m > SHC_HEAD + SHC_TAIL + 20 || total > SHC_BYTES_MAX;
|
||||
size_t head = m, tail_from = m;
|
||||
if (cut) {
|
||||
head = m < SHC_HEAD ? m : SHC_HEAD;
|
||||
tail_from = m > SHC_TAIL ? m - SHC_TAIL : 0;
|
||||
if (tail_from < head)
|
||||
tail_from = head;
|
||||
/* byte budget: shrink the head first, keep the tail (errors live there) */
|
||||
size_t tb = 0;
|
||||
for (size_t i = tail_from; i < m; i++)
|
||||
tb += (lines[i].len < SHC_LINE_MAX ? lines[i].len : SHC_LINE_MAX) + 1;
|
||||
while (tb > SHC_BYTES_MAX * 3 / 4 && tail_from + 1 < m) {
|
||||
tb -= (lines[tail_from].len < SHC_LINE_MAX ? lines[tail_from].len
|
||||
: SHC_LINE_MAX) + 1;
|
||||
tail_from++;
|
||||
}
|
||||
size_t hb = 0, h = 0;
|
||||
while (h < head) {
|
||||
size_t l = (lines[h].len < SHC_LINE_MAX ? lines[h].len : SHC_LINE_MAX) + 1;
|
||||
if (hb + l + tb > SHC_BYTES_MAX)
|
||||
break;
|
||||
hb += l;
|
||||
h++;
|
||||
}
|
||||
head = h;
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
for (size_t i = 0; i < m; i++) {
|
||||
if (cut && i == head && head < tail_from) {
|
||||
size_t omitted = 0;
|
||||
for (size_t k = head; k < tail_from; k++)
|
||||
omitted += rep ? (size_t)rep[k] : 1;
|
||||
cst_buf_printf(&b, "… %zu lines omitted …\n", omitted);
|
||||
i = tail_from;
|
||||
if (i >= m)
|
||||
break;
|
||||
}
|
||||
shc_put(&b, clean.buf ? clean.buf + lines[i].off : "", lines[i].len);
|
||||
if (rep && rep[i] > 2)
|
||||
cst_buf_printf(&b, "… (previous line repeated %d more times)\n",
|
||||
rep[i] - 1);
|
||||
}
|
||||
/* Keep "no trailing newline" outputs that way. */
|
||||
if (n && s[n - 1] != '\n' && b.len && b.buf[b.len - 1] == '\n')
|
||||
b.len--;
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
else
|
||||
b.buf[b.len] = 0;
|
||||
free(rep);
|
||||
free(lines);
|
||||
cst_buf_free(&clean);
|
||||
*out_len = b.len;
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
/* --- apply_patch --------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
|
||||
Reference in New Issue
Block a user