1479 lines · 39.1 KB · C
RawDownload
// SPDX-License-Identifier: ISC#include <errno.h>#include <fnmatch.h>#include <signal.h>#include <stdbool.h>#include <stdio.h>#include <stdlib.h>#include <string.h> #include <dirent.h>#include <fcntl.h>#include <sys/stat.h> #include <cjson/cJSON.h> #include "clm/tools.h"#include "clm/internal.h"#include "clm/log.h"#include "clm/cleanup.h"#include "useful.h"#include "banned.h" #define CLM_TOOL_OUTPUT_CAP_DEFAULT (64 * 1024)    /* bytes returned to model */#define CLM_TOOL_OUTPUT_CAP_MAX (1024 * 1024)      /* ceiling for overrides */#define CLM_TOOL_TIMEOUT_MAX_MS 600000u            /* 10 minutes */#define CLM_READ_DEFAULT_LIMIT 200                 /* lines */ /* * One in-flight tool call. Owned by its batch. Completed exactly once via * clm_tool_complete/clm_tool_fail (or the timeout path), which records a tool * result into history and decrements the batch's pending count. */struct clm_tool_invocation; /* Opaque to the frontend; references the parked invocation it authorizes. */struct clm_permission_req {	struct clm_tool_invocation *inv;}; static void run_invoke(struct clm_tool_invocation *inv); struct clm_tool_invocation {	struct clm_tool_batch *batch;	const struct clm_tool *def;   /* resolved tool, or NULL if unknown */	char *id;	char *name;	char *args;                   /* raw JSON args string */ 	size_t output_cap;            /* effective for this call */	uint64_t timeout_ms;          /* effective for this call */ 	struct clm_timer *timer;      /* per-call timeout, via host; NULL if none */	bool timed_out;	bool completed; 	void (*cancel)(struct clm_tool_invocation *, void *);	void *cancel_user; 	/* Permission gate: while awaiting a decision the invocation is parked *	 * (on_permission fired, invoke deferred). perm_req is the opaque handle *	 * that the frontend answers against; it points back to this invocation. */	bool awaiting_perm;	struct clm_permission_req perm_req; 	/* Rate-limit deferral: when the token bucket is empty, the invocation *	 * is parked on rl_timer and dispatched when tokens refill. */	struct clm_timer *rl_timer;}; /* A batch of tool calls dispatched together for one assistant message. */struct clm_tool_batch {	struct clm_agent *agent;	struct clm_tool_invocation *inv;	size_t n;	size_t pending;   /* outstanding completions + a dispatch hold */	size_t done;      /* completed calls, for progress reporting */	int status;       /* first hard error encountered, else 0 */ 	/* Set by clm_tools_detach() when the owning agent is torn down while	 * this batch still has invocations genuinely in flight. Once set,	 * `agent` is a dead pointer: nothing on the completion path may read	 * through it. See inv_finalize(). */	bool detached;}; /* ------------------------------------------------------------------ *//* Argument helpers                                                    *//* ------------------------------------------------------------------ */ static char *arg_string(cJSON *args, const char *key){	cJSON *v = NULL;	if (!(v = cJSON_GetObjectItemCaseSensitive(args, key)))		return NULL;	if (!cJSON_IsString(v))		return NULL;	return strdup(v->valuestring);} /* * Expand a leading "~/" to "$HOME/". Always returns a new allocation * (caller frees). Returns strdup(path) if no expansion needed. */static char *expand_tilde(const char *path){	const char *home;	size_t hlen, plen;	char *out; 	if (path == NULL)		return NULL;	if (path[0] != '~' || path[1] != '/')		return strdup(path);	home = getenv("HOME");	if (home == NULL || home[0] == '\0')		return strdup(path);	hlen = strlen(home);	plen = strlen(path + 1); /* includes the '/' */	out = malloc(hlen + plen + 1);	if (out == NULL)		return strdup(path);	memcpy(out, home, hlen);	memcpy(out + hlen, path + 1, plen + 1); /* copy '/' + rest + NUL */	return out;} static intarg_int(cJSON *args, const char *key, int dflt){	cJSON *v = NULL;	if (!(v = cJSON_GetObjectItemCaseSensitive(args, key)))		return dflt;	if (!cJSON_IsNumber(v))		return dflt;	return (int)v->valuedouble;} /* ------------------------------------------------------------------ *//* Registration                                                        *//* ------------------------------------------------------------------ */ /* * A "removed" node is a zombie kept alive only until its last invocation * finishes (see the struct clm_tool comment in clm/tools.h); it must never be * resolved again, so lookups skip it. */static const struct clm_tool *find_tool(const struct clm_agent *agent, const char *name){	const struct clm_tool *t;	TAILQ_FOREACH(t, &agent->tools, entries) {		if (!t->removed && strcmp(t->name, name) == 0)			return t;	}	return NULL;} intclm_tool_add(struct clm_agent *agent, const struct clm_tool_def *def){	struct clm_tool *t; 	ASSERT_RETURN(agent != NULL, -EINVAL);	ASSERT_RETURN(def != NULL, -EINVAL);	ASSERT_RETURN(def->name != NULL, -EINVAL);	ASSERT_RETURN(def->invoke != NULL, -EINVAL); 	if (find_tool(agent, def->name) != NULL)		return -EEXIST; 	t = calloc(1, sizeof(*t));	if (t == NULL)		return -ENOMEM; 	t->name = strdup(def->name);	t->description = def->description ? strdup(def->description) : NULL;	t->params_schema = def->params_schema ? strdup(def->params_schema) : NULL;	if (t->name == NULL ||	    (def->description != NULL && t->description == NULL) ||	    (def->params_schema != NULL && t->params_schema == NULL)) {		free(t->name);		free(t->description);		free(t->params_schema);		free(t);		return -ENOMEM;	}	t->invoke = def->invoke;	t->detach = def->detach;	t->user = def->user;	t->output_cap = def->output_cap;	t->timeout_ms = def->timeout_ms;	t->flags = def->flags; 	TAILQ_INSERT_TAIL(&agent->tools, t, entries);	agent->tool_count++;	return 0;} static voidtool_detach(struct clm_tool *t){	if (t->detach == NULL)		return;	t->detach(t->user);	t->detach = NULL;} /* Free a node's owned strings and itself. Does not unlink or touch pending * invocations; callers must already know it is safe (unlinked + inflight==0, * or agent teardown). */static voidtool_node_free(struct clm_tool *t){	tool_detach(t);	free(t->name);	free(t->description);	free(t->params_schema);	free(t);} intclm_tool_remove(struct clm_agent *agent, const char *name){	struct clm_tool *t; 	ASSERT_RETURN(agent != NULL, -EINVAL);	ASSERT_RETURN(name != NULL, -EINVAL); 	TAILQ_FOREACH(t, &agent->tools, entries) {		if (t->removed || strcmp(t->name, name) != 0)			continue;		tool_detach(t);		TAILQ_REMOVE(&agent->tools, t, entries);		agent->tool_count--;		if (t->inflight == 0)			tool_node_free(t);		else			t->removed = true; /* freed once the last invocation finalizes */		return 0;	}	return -ENOENT;} voidclm_tools_free_registry(struct clm_tool_list *tools){	struct clm_tool *t, *tmp;	if (tools == NULL)		return;	t = TAILQ_FIRST(tools);	while (t != NULL) {		tmp = TAILQ_NEXT(t, entries);		TAILQ_REMOVE(tools, t, entries);		tool_node_free(t);		t = tmp;	}} /* ------------------------------------------------------------------ *//* Schema construction                                                 *//* ------------------------------------------------------------------ */ static cJSON *int_prop(const char *desc){	cJSON *obj = cJSON_CreateObject();	if (obj == NULL)		return NULL;	cJSON_AddItemToObject(obj, "type", cJSON_CreateString("integer"));	cJSON_AddItemToObject(obj, "description", cJSON_CreateString(desc));	return obj;} static voidinject_prop(cJSON *props, const char *name, const char *desc){	cJSON *exist = cJSON_GetObjectItemCaseSensitive(props, name);	if (exist != NULL) {		clm_debug("schema: skip reserved param '%s' (tool defines it)", name);		return;	}	cJSON *p = int_prop(desc);	if (p != NULL)		cJSON_AddItemToObject(props, name, p);} static cJSON *tool_schema(const struct clm_tool *t){	cJSON *tool = cJSON_CreateObject();	cJSON *func, *params, *props; 	if (tool == NULL)		return NULL; 	cJSON_AddItemToObject(tool, "type", cJSON_CreateString("function")); 	func = cJSON_CreateObject();	if (func == NULL) {		cJSON_Delete(tool);		return NULL;	}	cJSON_AddItemToObject(tool, "function", func); 	cJSON_AddItemToObject(func, "name", cJSON_CreateString(t->name));	cJSON_AddItemToObject(func, "description", cJSON_CreateString(t->description ? t->description : "")); 	/* Parse the tool's parameters schema, or synthesize an empty object. */	params = t->params_schema ? cJSON_Parse(t->params_schema) : NULL;	if (params == NULL || cJSON_GetObjectItem(params, "type") == NULL) {		if (params != NULL)			cJSON_Delete(params);		params = cJSON_CreateObject();		if (params == NULL) {			cJSON_Delete(tool);			return NULL;		}	}	cJSON_AddItemToObject(func, "parameters", params); 	if (cJSON_GetObjectItemCaseSensitive(params, "type") == NULL)		cJSON_AddItemToObject(params, "type", cJSON_CreateString("object")); 	props = cJSON_GetObjectItemCaseSensitive(params, "properties");	if (props == NULL) {		props = cJSON_CreateObject();		if (props == NULL) {			cJSON_Delete(tool);			return NULL;		}		cJSON_AddItemToObject(params, "properties", props);	} 	if (t->flags & CLM_TOOL_TIMEOUT_OVERRIDABLE)		inject_prop(props, "timeout_ms", "optional: max milliseconds before this call is aborted");	if (t->flags & CLM_TOOL_OUTPUT_CAP_OVERRIDABLE)		inject_prop(props, "output_cap", "optional: max bytes of output to return"); 	return tool;} cJSON *clm_tools_build_schema(const struct clm_agent *agent){	cJSON *arr = cJSON_CreateArray();	const struct clm_tool *t; 	if (agent == NULL)		return NULL; 	if (arr == NULL)		return NULL; 	TAILQ_FOREACH(t, &agent->tools, entries) {		if (t->removed || (t->flags & CLM_TOOL_HIDDEN))			continue;		cJSON *sch = tool_schema(t);		if (sch != NULL)			cJSON_AddItemToArray(arr, sch); /* steals reference */	} 	return arr;} /* ------------------------------------------------------------------ *//* Invocation accessors                                                *//* ------------------------------------------------------------------ */ const char *clm_tool_invocation_name(const struct clm_tool_invocation *inv){	return inv ? inv->name : NULL;} const char *clm_tool_invocation_args(const struct clm_tool_invocation *inv){	return inv ? inv->args : NULL;} void *clm_tool_invocation_loop(const struct clm_tool_invocation *inv){	struct clm_host *host = inv ? inv->batch->agent->host : NULL;	return host ? host->native_loop : NULL;} const char *clm_permission_req_name(const struct clm_permission_req *req){	return (req && req->inv) ? req->inv->name : NULL;} const char *clm_permission_req_args(const struct clm_permission_req *req){	return (req && req->inv) ? req->inv->args : NULL;} /* Mutable lookup of a registered tool by name (for recording decisions). */static struct clm_tool *find_tool_mut(struct clm_agent *agent, const char *name){	struct clm_tool *t;	TAILQ_FOREACH(t, &agent->tools, entries) {		if (!t->removed && strcmp(t->name, name) == 0)			return t;	}	return NULL;} intclm_tool_permission_respond(struct clm_agent *agent,    const struct clm_permission_req *req, enum clm_permission_decision decision){	struct clm_tool_invocation *inv;	bool allow; 	ASSERT_RETURN(agent != NULL, -EINVAL);	ASSERT_RETURN(req != NULL && req->inv != NULL, -EINVAL); 	inv = req->inv;	if (!inv->awaiting_perm)		return -EINVAL; /* already answered, or not parked */	inv->awaiting_perm = false; 	allow = (decision == CLM_PERM_ALLOW_ONCE ||	         decision == CLM_PERM_ALLOW_ALWAYS); 	/* Remember _ALWAYS decisions for the rest of the session, per tool. */	if (decision == CLM_PERM_ALLOW_ALWAYS ||	    decision == CLM_PERM_DENY_ALWAYS) {		struct clm_tool *t = find_tool_mut(agent, inv->name);		if (t != NULL) {			t->remembered = true;			t->remember_allow = allow;		}	} 	if (allow)		run_invoke(inv);	else		clm_tool_fail(inv, "denied by user");	return 0;} size_tclm_tool_invocation_output_cap(const struct clm_tool_invocation *inv){	return inv ? inv->output_cap : 0;} uint64_tclm_tool_invocation_timeout_ms(const struct clm_tool_invocation *inv){	return inv ? inv->timeout_ms : 0;} voidclm_tool_invocation_set_cancel(struct clm_tool_invocation *inv,    void (*cancel)(struct clm_tool_invocation *, void *), void *user){	if (inv == NULL)		return;	inv->cancel = cancel;	inv->cancel_user = user;} /* ------------------------------------------------------------------ *//* Batch teardown                                                      *//* ------------------------------------------------------------------ */ static voidbatch_really_free(struct clm_tool_batch *batch){	size_t i;	for (i = 0; i < batch->n; i++) {		free(batch->inv[i].id);		free(batch->inv[i].name);		free(batch->inv[i].args);	}	free(batch->inv);	free(batch);} static voidbatch_finalize(struct clm_tool_batch *batch){	struct clm_agent *agent = batch->agent;	int status = batch->status;	size_t i; 	if (agent->active_batch == batch)		agent->active_batch = NULL; 	/* Advance the turn (or end it on error). This may start a new,	 * independent batch asynchronously; this one is torn down below. */	clm_agent_tools_done(agent, status);

Lines 1–500 of 1479.