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:
WHC
2026-10-05 14:55:31 +08:00
parent c81df42f0b
commit 7d3ec9fcb9
38 changed files with 8356 additions and 399 deletions
+20 -1
View File
@@ -76,7 +76,14 @@ typedef struct {
int approval; /* cst_approval_t (interactive sessions) */
int plan_mode; /* read-only tools + planning note */
int tool_profile; /* cst_tool_profile_t (CST_TOOLS) */
unsigned long long tools_off; /* bit i = registry tool i disabled */
char tools_off[1024]; /* ",name,name," tools switched off (/tools off) */
/* Sub-agents (task tool). */
int depth; /* 0 = top-level; sub-agents run at 1 */
int read_only; /* no write / shell tools */
char tools_only[1024]; /* ",a,b," allowlist; empty = every tool */
const char *role_prompt; /* role instructions (not owned) */
char undo_sid[128]; /* snapshot into this session's undo... */
char undo_tag[64]; /* ...under this turn tag (the parent's) */
} cst_agent_config_t;
/* ask: confirm per tool; auto: sandbox/rules decide, ask on doubt; yolo: never
@@ -299,6 +306,9 @@ int cst_agent_undo_available(const cst_agent_t *a);
struct cst_perms *cst_agent_perms(cst_agent_t *a);
/* /retry: remove the last user prompt and its replies; caller frees. */
char *cst_agent_rewind_last_turn(cst_agent_t *a);
/* Session id and turn tag that /undo snapshots of the running turn use. */
void cst_agent_undo_key(const cst_agent_t *a, char *sid, size_t sid_sz,
char *tag, size_t tag_sz);
/* Last prompt_tokens reported by the model (0 if unknown). */
long cst_agent_last_prompt_tokens(const cst_agent_t *a);
@@ -323,10 +333,15 @@ int cst_creds_prompt_console(const char *provider, char *out, size_t out_sz);
/* Last used provider, model per provider and effort, in
* <config>/settings.json. */
#define CST_SETTINGS_MAX_MODELS 16
#define CST_SETTINGS_MAX_TRUSTED 32
typedef struct {
char provider[32];
char effort[16];
char approval[8]; /* "" = not chosen yet */
int show_thinking; /* TUI: Thought blocks start expanded */
/* workspaces whose .agent/plugins and .agent/mcp.json may run */
int ntrusted;
char trusted[CST_SETTINGS_MAX_TRUSTED][512];
int nmodels;
struct {
char provider[32];
@@ -340,6 +355,10 @@ const char *cst_settings_model(const cst_settings_t *s, const char *provider);
int cst_settings_remember(const cst_agent_config_t *cfg);
/* Record an approval mode the user picked (CST_APPROVAL_*). */
int cst_settings_remember_approval(int approval);
int cst_settings_remember_thinking(int show_thinking);
/* Workspace trust for project plugins / MCP servers (by real path). */
int cst_settings_trusted(const char *workspace);
int cst_settings_trust(const char *workspace, int on);
/* JSON baseline parser for experiments (buffer until valid object). */
typedef struct {
+31
View File
@@ -0,0 +1,31 @@
#ifndef CST_EXT_H
#define CST_EXT_H
/* Startup / reload of everything that adds tools at runtime: the skill
* tool, plugin exec tools and MCP servers. */
#include "cst_agent.h"
#include <stddef.h>
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Load for cfg's workspace (no-op for `--tools minimal`). One-line summary
* in report, e.g. "2 skills · 1 plugin (3 tools) · mcp 1/2". */
void cst_extensions_load(const cst_agent_config_t *cfg, char *report,
size_t report_sz);
/* Summary of the last load (empty when nothing was found). */
const char *cst_extensions_report(void);
/* Headless: wait for MCP handshakes, then note servers that failed. */
void cst_extensions_wait(FILE *log);
/* Drop plugin/MCP tools and stop MCP servers. */
void cst_extensions_unload(void);
#ifdef __cplusplus
}
#endif
#endif
+134
View File
@@ -0,0 +1,134 @@
#ifndef CST_KEYMAP_H
#define CST_KEYMAP_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Key codes: a Unicode code point (control characters included: ctrl+a = 1),
* a CST_K_* special key, optionally OR'ed with CST_KEY_ALT. */
#define CST_KEY_SPECIAL 0x110000
#define CST_KEY_ALT 0x400000
enum {
CST_K_UP = CST_KEY_SPECIAL,
CST_K_DOWN,
CST_K_LEFT,
CST_K_RIGHT,
CST_K_HOME,
CST_K_END,
CST_K_PGUP,
CST_K_PGDN,
CST_K_BTAB, /* shift+tab */
CST_K_DEL,
CST_K_INS,
CST_K_CTRL_HOME,
CST_K_CTRL_END,
CST_K_CTRL_UP,
CST_K_CTRL_DOWN,
CST_K_ALT_UP,
CST_K_ALT_DOWN,
CST_K_F1, /* CST_K_F1 + n - 1 for f1..f12 */
CST_K_LAST = CST_K_F1 + 11
};
typedef enum {
CST_KA_NONE = 0,
/* general */
CST_KA_PALETTE,
CST_KA_KEYS,
CST_KA_MODE,
CST_KA_APPROVAL,
CST_KA_NEWLINE,
CST_KA_REDRAW,
CST_KA_QUIT,
/* view */
CST_KA_TOGGLE_TOOL,
CST_KA_TOGGLE_THOUGHT,
CST_KA_THINKING_ALL,
CST_KA_DETAILS,
CST_KA_TODO,
CST_KA_SIDEBAR,
CST_KA_SCROLL_UP,
CST_KA_SCROLL_DOWN,
CST_KA_LINE_UP,
CST_KA_LINE_DOWN,
CST_KA_TOP,
CST_KA_BOTTOM,
/* session */
CST_KA_NEW_SESSION,
CST_KA_SESSIONS,
CST_KA_UNDO,
CST_KA_RETRY,
CST_KA_COMPACT,
CST_KA_COPY,
CST_KA_EXPORT,
/* model */
CST_KA_MODEL,
CST_KA_PROVIDER,
CST_KA_EFFORT,
/* extensions */
CST_KA_AGENTS,
CST_KA_SKILLS,
CST_KA_PLUGINS,
CST_KA_COUNT
} cst_key_action_t;
typedef struct {
const char *name; /* keybinds.json key, e.g. "new_session" */
const char *group; /* "General", "View", ... */
const char *desc;
const char *slash; /* equivalent slash command, or NULL */
} cst_key_action_info_t;
const cst_key_action_info_t *cst_key_action_info(int action);
int cst_key_action_find(const char *name); /* CST_KA_NONE when unknown */
/* "ctrl+p", "shift+tab", "alt+enter", "f1", "pgup", "x" → code; -1 if bad. */
int cst_key_parse(const char *s);
/* code → canonical name ("ctrl+x", "tab", "alt+up", ...). */
const char *cst_key_name(int code, char *buf, size_t n);
#define CST_KEYMAP_MAX 128
typedef struct {
int action;
int keys[2];
int nkeys;
int user; /* from keybinds.json */
} cst_binding_t;
typedef struct {
int leader;
cst_binding_t b[CST_KEYMAP_MAX];
int n;
} cst_keymap_t;
void cst_keymap_defaults(cst_keymap_t *km);
/* Apply keybinds.json text over the defaults:
* {"leader":"ctrl+x", "palette":"ctrl+p", "new_session":["<leader> n","f2"],
* "todo":"none"}
* Returns 0, or -1 with a message (bad entries are skipped, the rest apply). */
int cst_keymap_apply_json(cst_keymap_t *km, const char *json, char *err,
size_t err_sz);
/* Defaults + <config>/keybinds.json when present. */
int cst_keymap_load(cst_keymap_t *km, char *err, size_t err_sz);
#define CST_KM_NONE 0
#define CST_KM_MATCH 1
#define CST_KM_PREFIX 2
/* seq of 1 or 2 keys → CST_KM_*; *action set on a match. */
int cst_keymap_lookup(const cst_keymap_t *km, const int *seq, int n,
int *action);
/* "ctrl+x n, f2" (or "" when unbound). */
void cst_keymap_describe(const cst_keymap_t *km, int action, char *buf,
size_t n);
/* One line per conflict ("ctrl+o: toggle_tool, toggle_thought"); returns the
* number of conflicts. */
int cst_keymap_conflicts(const cst_keymap_t *km, char *buf, size_t n);
#ifdef __cplusplus
}
#endif
#endif
+66
View File
@@ -0,0 +1,66 @@
#ifndef CST_MCP_H
#define CST_MCP_H
/*
* MCP (Model Context Protocol) client over stdio: newline-delimited
* JSON-RPC 2.0. Each server is spawned with its command/args/env, gets
* initialize + notifications/initialized + tools/list, and every tool is
* registered as mcp__<server>__<tool> (approved like network tools).
*
* Config, merged in this order (first name wins):
* <ws>/.agent/mcp.json (trusted workspaces only), <config>/mcp.json,
* plugin "mcpServers". Format: {"mcpServers": {"name": {"command": "npx",
* "args": ["-y", "pkg"], "env": {"K": "V"}, "cwd": "...",
* "disabled": false}}}
*/
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define CST_MCP_MAX 16
typedef enum {
CST_MCP_OFF = 0,
CST_MCP_STARTING,
CST_MCP_READY,
CST_MCP_FAILED,
CST_MCP_EXITED
} cst_mcp_state_t;
typedef struct {
char name[64];
char source[48]; /* "workspace" | "user" | "plugin:<name>" */
char command[256];
int state; /* cst_mcp_state_t */
int ntools;
char error[200];
} cst_mcp_info_t;
/* One server from a JSON object text ({"command":..., "args":[...], ...}).
* cwd: default working directory (NULL = workspace). Returns 0, or -1 when
* the name exists / the config is invalid. */
int cst_mcp_add_config_json(const char *name, const char *server_json,
const char *cwd, const char *source);
/* Read the mcp.json files for workspace (the workspace one only when
* trusted). Returns servers added; *untrusted counts skipped ones. */
int cst_mcp_load_configs(const char *workspace, int ws_trusted,
int *untrusted);
/* Start every configured server in the background; tools appear as each
* one finishes its handshake. */
void cst_mcp_start_all(const char *workspace);
/* Wait up to ms for servers still starting (headless runs). */
void cst_mcp_wait_ready(int ms);
/* Stop servers, unregister their tools and forget the configs. */
void cst_mcp_stop_all(void);
int cst_mcp_info(cst_mcp_info_t *out, int max);
/* Ready / configured counts (for the sidebar). */
void cst_mcp_counts(int *ready, int *total);
#ifdef __cplusplus
}
#endif
#endif
+51
View File
@@ -0,0 +1,51 @@
#ifndef CST_PLUGINS_H
#define CST_PLUGINS_H
/*
* Plugins: <config>/plugins/<dir>/plugin.json (always loaded) and
* <ws>/.agent/plugins/<dir>/plugin.json (only for trusted workspaces).
*
* {"name": "jira", "description": "...",
* "tools": [{"name": "jira_search", "description": "...",
* "parameters": {JSON Schema},
* "command": ["python3", "search.py"], (or a shell string)
* "perm": "read|write|shell|net", "timeout": 60}],
* "skills": "skills",
* "mcpServers": {"name": {"command": "...", "args": [], "env": {}}}}
*
* A tool call runs the command in the workspace with the arguments as JSON
* on stdin (CST_PLUGIN_DIR / CST_WORKSPACE in the environment); stdout +
* stderr is the result and the exit status its exit code. Relative argv
* entries that exist in the plugin directory are made absolute.
*/
#include "os_compat.h"
#ifdef __cplusplus
extern "C" {
#endif
#define CST_PLUGINS_MAX 32
typedef struct {
char name[64];
char dir[OS_PATH_MAX];
char source[16]; /* "user" | "workspace" */
char desc[200];
int ntools;
int nmcp;
int nskills_root; /* 1 when it adds a skills directory */
char error[200]; /* load problem, "" when fine */
} cst_plugin_info_t;
/* Load plugins; workspace ones only when ws_trusted. Returns the number of
* plugins loaded; *untrusted (optional) counts skipped workspace plugins. */
int cst_plugins_load(const char *workspace, int ws_trusted, int *untrusted);
void cst_plugins_unload(void);
int cst_plugins_info(cst_plugin_info_t *out, int max);
#ifdef __cplusplus
}
#endif
#endif
+62
View File
@@ -0,0 +1,62 @@
#ifndef CST_SKILLS_H
#define CST_SKILLS_H
/*
* Skills: directories with a SKILL.md (YAML frontmatter `name`,
* `description`, then instructions) plus any helper files. Found in
* <ws>/.agent/skills/<dir>/ <ws>/.claude/skills/<dir>/
* <config>/skills/<dir>/ plugin skill roots (cst_skills_add_root)
* The first skill of a name wins. The system prompt lists name +
* description; the `skill` tool loads the rest on demand.
*/
#include "os_compat.h"
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define CST_SKILLS_MAX 64
#define CST_SKILL_INDEX_MAX 4096
typedef struct {
char name[64];
char desc[600];
char dir[OS_PATH_MAX];
char source[48]; /* "workspace", "claude", "user", "plugin:<name>" */
} cst_skill_t;
/* Skills visible from workspace, sorted by name. Returns the count. */
int cst_skills_list(const char *workspace, cst_skill_t *out, int max);
/* Extra root (a directory of skill directories), e.g. from a plugin. */
void cst_skills_add_root(const char *dir, const char *source);
/* Forget the extra roots whose source starts with prefix ("" = all). */
void cst_skills_clear_roots(const char *source_prefix);
/* "Skills (...):\n- name: description\n" capped at CST_SKILL_INDEX_MAX, or
* NULL when there are none. malloc'd. */
char *cst_skills_index(const char *workspace);
/* SKILL.md body (or one file of the skill) as tool output; *exit_code is 0
* or 1. malloc'd. */
char *cst_skill_read(const char *workspace, const char *name,
const char *file, int *exit_code);
/* User message for `/skill name task`: the instructions inlined before the
* task. NULL when the skill does not exist. malloc'd. */
char *cst_skill_prompt(const char *workspace, const char *name,
const char *task);
/* Register the `skill` tool (idempotent). */
int cst_skills_register_tool(void);
/* --- YAML-ish frontmatter (also used for agent role files) --- */
/* Value of key: plain, quoted, folded (`>`/`|`), `[a, b]` or `- a` lists
* (lists come back comma-separated). Returns 1 when found. */
int cst_fm_get(const char *text, const char *key, char *out, size_t n);
/* Text after the closing `---` (the whole text when there is none). */
const char *cst_fm_body(const char *text);
#ifdef __cplusplus
}
#endif
#endif
+65
View File
@@ -0,0 +1,65 @@
#ifndef CST_SUBAGENT_H
#define CST_SUBAGENT_H
/*
* Sub-agents: the task(description, prompt, agent?) tool runs one turn of a
* child agent with its own context and returns its final report.
*
* Roles: built-in general / explore / plan, plus Markdown files in
* <ws>/.agent/agents/ and <config>/agents/ (the workspace wins, both win
* over built-ins). Frontmatter: name, description, model, tools (allowlist),
* max_steps, readonly; the body is the role's instructions.
*
* Transcripts: <ws>/.agent/sessions/<parent-id>/sub-<n>.jsonl, with a
* sub-<n>.json summary next to each (not listed as sessions).
*/
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define CST_ROLES_MAX 32
typedef struct {
char name[64];
char desc[256];
char source[16]; /* "builtin" | "workspace" | "user" */
char model[128]; /* empty = the parent's */
char tools[1024]; /* ",a,b," allowlist; empty = all but task */
int max_steps; /* 0 = default */
int read_only;
char *prompt; /* role instructions (malloc'd) */
} cst_agent_role_t;
/* Roles sorted built-ins first, then by name. Free with cst_roles_free. */
int cst_roles_list(const char *workspace, cst_agent_role_t *out, int max);
void cst_roles_free(cst_agent_role_t *v, int n);
/* System prompt section listing the roles (malloc'd), or NULL. */
char *cst_subagent_index(const char *workspace);
/* Register the task tool (owner "builtin:task") for workspace. */
int cst_subagent_register_tool(const char *workspace);
typedef struct {
char file[64]; /* "sub-3" */
char role[64];
char desc[160];
char status[32]; /* "done" | "error" | "cancelled" | "running" */
int steps, tools;
double ms;
long long mtime;
} cst_subagent_rec_t;
/* This parent session's sub-agent runs, newest first. */
int cst_subagent_list(const char *workspace, const char *parent_id,
cst_subagent_rec_t *out, int max);
/* Full path of a transcript (sub-<n>.jsonl). */
int cst_subagent_path(const char *workspace, const char *parent_id,
const char *file, char *out, size_t out_sz);
#ifdef __cplusplus
}
#endif
#endif
+40
View File
@@ -41,6 +41,13 @@ typedef struct cst_tool_ctx {
void (*set_todo)(const char *text, void *ud);
void *todo_ud;
int sandbox; /* run_shell: confine to the workspace (cst_sandbox.h) */
/* Information-dense output (read_file headers and line numbers, cleaned
* run_shell output). Off for `--tools minimal` so paper runs see the
* original bytes. */
int compact;
/* The calling agent and its turn hooks (task runs sub-agents). */
cst_agent_t *agent;
const cst_agent_hooks_t *hooks;
} cst_tool_ctx_t;
typedef int (*cst_tool_run_fn)(const cst_tool_ctx_t *ctx,
@@ -51,6 +58,10 @@ typedef int (*cst_tool_run_fn)(const cst_tool_ctx_t *ctx,
typedef int (*cst_tool_preview_fn)(const cst_tool_ctx_t *ctx,
const ctl_tool_call_t *call, char **out);
typedef int (*cst_tool_run_ex_fn)(const cst_tool_ctx_t *ctx,
const ctl_tool_call_t *call,
cst_tool_result_t *r, void *ud);
typedef struct {
const char *name;
const char *desc;
@@ -62,8 +73,32 @@ typedef struct {
int minimal; /* member of `--tools minimal` (the original four) */
cst_tool_run_fn run;
cst_tool_preview_fn preview;
/* Dynamic tools (skills, plugins, MCP, sub-agents). */
const char *schema_json; /* JSON Schema object for the arguments; when set
params[] is unused */
cst_tool_run_ex_fn run_ex;
void *ud;
const char *owner; /* "plugin:<name>", "mcp:<server>", ... */
/* Non-READ tools that may still join a parallel batch for this call. */
int (*parallel)(const ctl_tool_call_t *call, void *ud);
} cst_tool_def_t;
/* Add a tool at runtime (strings are copied, ud is not). Safe while turns
* run; returns 0, or -1 when the name is taken or the table is full. */
int cst_tool_register(const cst_tool_def_t *def);
/* Drop every dynamic tool of owner (call only while no turn runs). */
int cst_tool_unregister_owner(const char *owner);
/* Arguments of call as a JSON object text, typed by the tool's schema
* (numbers/booleans/objects stay JSON). malloc'd. */
char *cst_tool_args_json(const cst_tool_def_t *t, const ctl_tool_call_t *call);
/* Turn a name into a valid tool name ([A-Za-z0-9_-], at most 63 bytes). */
void cst_tool_sanitize_name(const char *in, char *out, size_t out_sz);
/* Per-config switches by name (/tools on|off). */
int cst_tool_disabled(const cst_agent_config_t *cfg, const char *name);
void cst_tool_set_disabled(cst_agent_config_t *cfg, const char *name,
int off);
typedef enum { CST_TOOLS_DEFAULT = 0, CST_TOOLS_MINIMAL } cst_tool_profile_t;
int cst_tool_profile_parse(const char *s, int *out);
@@ -95,6 +130,11 @@ int cst_tool_preview(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *call,
* Returns the count written into paths. */
int cst_tool_targets(const ctl_tool_call_t *call, char paths[][1024], int max);
/* Shell output cleaned for a model: ANSI escapes removed, `\r` progress
* redraws folded to their final state, runs of identical lines collapsed,
* very long output cut to head + tail. Returns malloc'd text. */
char *cst_shell_compact(const char *s, size_t n, size_t *out_len);
/* Unified diff (3 lines of context, GNU `diff -u` layout) with
* `--- a/<label>` / `+++ b/<label>` headers. Returns malloc'd text; "" when
* identical. */
+31 -1
View File
@@ -71,7 +71,37 @@ int os_run_shell_ex(const char *cwd, const char *cmd, size_t max_out,
* killed after the timeout (CST_SHELL_TIMEOUT seconds, default 120) or when
* *flag becomes non-zero. Exit code 124 = timeout, 130 = cancelled. */
void os_set_cancel_flag(volatile int *flag);
void os_set_run_timeout(int seconds);
volatile int *os_get_cancel_flag(void);
void os_set_run_timeout(int seconds); /* this thread; 0 = default */
int os_get_run_timeout(void);
/* Long-lived child with pipes on stdin/stdout (MCP servers). stderr goes
* to stderr_path (NULL = discarded). env_add: "KEY=VALUE" entries added to
* the inherited environment (NULL-terminated, may be NULL). */
typedef struct {
#ifdef _WIN32
void *proc, *in_w, *out_r;
#else
int pid, in_fd, out_fd;
#endif
} os_proc_t;
int os_proc_spawn(const char *cwd, const char *const argv[],
const char *const env_add[], const char *stderr_path,
os_proc_t *p);
int os_proc_write(os_proc_t *p, const char *data, size_t n); /* 0 / -1 */
/* Bytes read, 0 at EOF, -1 on error, -2 when nothing came within
* timeout_ms. */
long os_proc_read(os_proc_t *p, char *buf, size_t n, int timeout_ms);
void os_proc_close_stdin(os_proc_t *p);
/* Close the pipes, end the process (group) and reap it. */
void os_proc_kill(os_proc_t *p);
/* This thread's cancel flag (os_set_cancel_flag) is raised. */
int os_cancel_requested(void);
/* Run fn(args[i]) for every i on its own thread and wait for all of them
* (falls back to running inline when a thread cannot start). */
typedef void (*os_task_fn)(void *arg);
void os_run_parallel(os_task_fn fn, void *const args[], int n);
/* Monotonic milliseconds (arbitrary epoch). */
double os_now_ms(void);