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
+378 -3
View File
@@ -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