forked from WHC/Hermit
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:
+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
|
||||
|
||||
Reference in New Issue
Block a user