1891 lines · 51.9 KB · C
RawDownload
// SPDX-License-Identifier: ISC#include <dirent.h>#include <errno.h>#include <stdbool.h>#include <stddef.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/queue.h>#include <sys/stat.h>#include <time.h> #include <lua.h>#include <lauxlib.h>#include <lualib.h> #include <cjson/cJSON.h> #include "clm/clm.h"#include "clm/cleanup.h"#include "clm/internal.h"#include "clm/lua_plugin.h"#include "clm/log.h"#include "useful.h"#include "banned.h" /* Forward declarations for modules registered into each plugin state. */int clm_lua_json_open(lua_State *L);void clm_lua_push_json_value(lua_State *L, cJSON *obj);int clm_lua_http_open(lua_State *L, struct clm_agent *agent);int clm_lua_agent_open(lua_State *L); /* Invocation-thread guard, consulted by the http bindings (lua_http.c). */void clm_lua_mark_invocation_thread(lua_State *L, lua_State *co, int on);int clm_lua_is_invocation_thread(lua_State *L); /* Clears the per-invocation registry stash (_clm_co_ref/_clm_tool_name/ * _clm_inv, set in lua_tool_invoke below) once a coroutine is truly done. * Called from both here and lua_http.c's async completion callbacks. */void clm_lua_clear_invocation_registry(lua_State *L); #define CLM_LUA_MEM_LIMIT (8 * 1024 * 1024) /* 8 MiB per plugin */#define CLM_LUA_EXEC_TIMEOUT_MS 30000u     /* default: 30s CPU timeout */#define CLM_LUA_LOAD_TIMEOUT_MS 500u       /* plugin load must be quick */#define CLM_LUA_HOOK_INTERVAL 10000        /* check deadline every N instructions */#define CLM_LUA_HTTP_MAX_INFLIGHT 8        /* max concurrent HTTP requests */#define CLM_LUA_HTTP_MAX_PER_CALL 128      /* max total HTTP requests per tool call */#define CLM_LUA_JSON_DECODE_MAX (2 * 1024 * 1024) /* 2 MiB max input to json.decode */ /* Forward declaration. */struct lua_tool_user; /* Per-plugin resource budget and peak tracking. */struct clm_lua_budget {	/* Limits (set from env defaults). */	size_t http_max_inflight;	size_t http_max_per_call;	size_t json_decode_max; 	/* Current counters (reset per tool call). */	size_t http_inflight;	size_t http_total;	uint64_t call_start_ns;   /* wall-clock start of current call */ 	/* Peak counters (lifetime of plugin, never reset). */	size_t peak_mem;	size_t peak_http_inflight;	size_t peak_http_total;    /* peak per single tool call */	uint64_t peak_call_ns;     /* longest tool call duration */}; /* Per-plugin state. */struct clm_lua_plugin {	TAILQ_ENTRY(clm_lua_plugin) entry;	lua_State *L;	struct clm_agent *agent;	size_t mem_used;	size_t mem_limit;	char *path;	struct lua_tool_user **tool_users; /* tracked for teardown */	size_t tool_user_count;	size_t tool_user_cap;	uint64_t deadline_ns; /* wall-clock deadline for current execution */	struct clm_lua_budget budget;}; TAILQ_HEAD(clm_lua_plugin_list, clm_lua_plugin); /* Top-level environment holding all plugins. */struct clm_lua_env {	struct clm_agent *agent;	struct clm_lua_plugin_list plugins;	uint64_t exec_timeout_ms; /* global execution timeout */	/* Global budget defaults. */	size_t http_max_inflight;	size_t http_max_per_call;	size_t json_decode_max;	/* Per-tool config (parsed JSON object, keyed by tool/plugin name). */	cJSON *tool_config;}; /* Budget helpers called from lua_http.c (defined below). */void clm_lua_http_done(struct clm_lua_plugin *plugin);int clm_lua_http_start(struct clm_lua_plugin *plugin);void clm_lua_budget_report(struct clm_lua_plugin *plugin, const char *name); /* ------------------------------------------------------------------ *//* Capped allocator                                                    *//* ------------------------------------------------------------------ */ static void *lua_capped_alloc(void *ud, void *ptr, size_t osize, size_t nsize){	struct clm_lua_plugin *p = ud; 	/* For a brand-new allocation (ptr == NULL), Lua 5.4 passes the	 * object's type tag in `osize`, not a real previous size -- e.g.	 * osize=8 for a coroutine, not "8 bytes previously allocated here".	 * Treating that as bytes-to-subtract silently corrupts mem_used on	 * every single new allocation (it ends up mem_used - tag too low).	 * Over thousands of allocations in a long-running session this	 * drifts mem_used further and further below the real heap size,	 * until the cap approves allocations the real system can no longer	 * satisfy -- realloc() then genuinely fails outside any protected	 * Lua call, which Lua has no way to recover from (its OOM error has	 * nowhere to unwind to, so the default panic handler aborts()).	 * Only ptr != NULL (an actual resize/free of a real prior block)	 * carries a real previous size worth accounting for. */	size_t real_osize = ptr != NULL ? osize : 0; 	if (nsize == 0) {		p->mem_used -= real_osize;		free(ptr);		return NULL;	}	if (p->mem_used - real_osize + nsize > p->mem_limit)		return NULL; /* refused; Lua raises OOM */	void *np = realloc(ptr, nsize);	if (np == NULL)		return NULL;	p->mem_used = p->mem_used - real_osize + nsize;	if (p->mem_used > p->budget.peak_mem)		p->budget.peak_mem = p->mem_used;	return np;} /* ------------------------------------------------------------------ *//* Invocation context userdata                                         *//* ------------------------------------------------------------------ */ #define CLM_LUA_CTX_META "clm_tool_ctx" struct lua_tool_ctx {	struct clm_tool_invocation *inv;	lua_State *main_L;  /* plugin's main state */	int co_ref;         /* registry ref keeping coroutine alive */	bool completed;}; static struct lua_tool_ctx *check_ctx(lua_State *L, int idx){	return luaL_checkudata(L, idx, CLM_LUA_CTX_META);} static intlua_ctx_complete(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	const char *result; 	if (ctx->completed)		return luaL_error(L, "tool already completed");	result = luaL_checkstring(L, 2);	ctx->completed = true;	clm_tool_complete(ctx->inv, result);	return 0;} static intlua_ctx_fail(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	const char *msg; 	if (ctx->completed)		return luaL_error(L, "tool already completed");	msg = luaL_checkstring(L, 2);	ctx->completed = true;	clm_tool_fail(ctx->inv, msg);	return 0;} static intlua_ctx_args_raw(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	const char *args = clm_tool_invocation_args(ctx->inv); 	if (args == NULL)		lua_pushstring(L, "{}");	else		lua_pushstring(L, args);	return 1;} static intlua_ctx_log(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	const char *msg = luaL_checkstring(L, 2); 	(void)ctx;	clm_debug("lua plugin: %s", msg);	return 0;} static const luaL_Reg ctx_methods[] = {	{"complete", lua_ctx_complete},	{"fail", lua_ctx_fail},	{"args_raw", lua_ctx_args_raw},	{"log", lua_ctx_log},	{NULL, NULL},}; static voidregister_ctx_meta(lua_State *L){	luaL_newmetatable(L, CLM_LUA_CTX_META);	lua_pushvalue(L, -1);	lua_setfield(L, -2, "__index");	luaL_setfuncs(L, ctx_methods, 0);	lua_pop(L, 1);} /* ------------------------------------------------------------------ *//* Tool invoke bridge                                                  *//* ------------------------------------------------------------------ */ /* * Stored per registered Lua tool: the plugin and the Lua registry reference * for the invoke function. */struct lua_tool_user {	struct clm_lua_plugin *plugin;	int invoke_ref; /* LUA_REGISTRYINDEX reference to the invoke fn */}; /* ------------------------------------------------------------------ *//* CPU timeout hook                                                    *//* ------------------------------------------------------------------ */ static uint64_tclock_ns(void){	struct timespec ts;	clock_gettime(CLOCK_MONOTONIC, &ts);	return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;} /* * lua_sethook count callback. Fires every CLM_LUA_HOOK_INTERVAL bytecode * instructions. Checks if the plugin's wall-clock deadline has been exceeded * and raises an error if so, unwinding to lua_resume. */static voidlua_exec_hook(lua_State *L, lua_Debug *ar){	(void)ar;	/* Retrieve the plugin pointer from the registry. */	lua_getfield(L, LUA_REGISTRYINDEX, "_clm_plugin");	struct clm_lua_plugin *p = lua_touserdata(L, -1);	lua_pop(L, 1); 	if (p == NULL || p->deadline_ns == 0)		return;	if (clock_ns() > p->deadline_ns)		luaL_error(L, "plugin exceeded execution time budget");} /* * Mark (on != 0) or clear (on == 0) `co` as a live tool-invocation coroutine, * keyed by its thread pointer in the registry. clm's async HTTP model requires * that http.get/post run directly on this coroutine, so their lua_yield unwinds * to clm's C-level lua_resume; the http bindings consult this marker to reject * calls from a nested coroutine or from load time (the main thread), which would * otherwise drive completion tracking out of band. Keyed per-thread, so * concurrent invocations of one plugin do not alias one another. */voidclm_lua_mark_invocation_thread(lua_State *L, lua_State *co, int on){	lua_pushlightuserdata(L, co);	if (on)		lua_pushboolean(L, 1);	else		lua_pushnil(L);	lua_settable(L, LUA_REGISTRYINDEX);} /* True if the running state L is a live tool-invocation coroutine. */intclm_lua_is_invocation_thread(lua_State *L){	int ok;	lua_pushlightuserdata(L, L);	lua_gettable(L, LUA_REGISTRYINDEX);	ok = lua_toboolean(L, -1);	lua_pop(L, 1);	return ok;} /* * lua_tool_invoke stashes _clm_co_ref/_clm_tool_name/_clm_inv in the * (shared, coroutine-and-main-state-wide) registry so http.get/post can * retrieve them from an async callback. They were only ever being * overwritten by the next invocation, never explicitly cleared -- so * between calls the registry held a stale _clm_inv lightuserdata pointing * at an already-freed clm_tool_invocation, and kept the previous call's * tool-name string rooted for no reason. Call this once a coroutine is * truly finished (same terminal points as the co_ref unref). */voidclm_lua_clear_invocation_registry(lua_State *L){	lua_pushnil(L);	lua_setfield(L, LUA_REGISTRYINDEX, "_clm_co_ref");	lua_pushnil(L);	lua_setfield(L, LUA_REGISTRYINDEX, "_clm_tool_name");	lua_pushnil(L);	lua_setfield(L, LUA_REGISTRYINDEX, "_clm_inv");	/* Nudge the collector now rather than waiting for its own pacing to	 * notice -- an incremental step is cheap and reclaims the coroutine	 * (now genuinely unreferenced) and its stack contents promptly	 * instead of letting several turns' worth of garbage pile up under	 * the plugin's tight memory cap before the GC gets around to it. */	lua_gc(L, LUA_GCSTEP, 0);} /* * Log resource peaks for this plugin after a tool call completes. */voidclm_lua_budget_report(struct clm_lua_plugin *plugin, const char *tool_name){	struct clm_lua_budget *b = &plugin->budget;	uint64_t elapsed_ns = clock_ns() - b->call_start_ns; 	/* Update peaks before reporting. */	if (elapsed_ns > b->peak_call_ns)		b->peak_call_ns = elapsed_ns;	if (b->http_total > b->peak_http_total)		b->peak_http_total = b->http_total; 	size_t elapsed_ms = (size_t)(elapsed_ns / 1000000ULL);	size_t peak_ms = (size_t)(b->peak_call_ns / 1000000ULL); 	clm_debug("lua budget [%s]: call=%zums mem_peak=%zu/%zu "	    "http=%zu http_peak_inflight=%zu peak_call=%zums",	    tool_name,	    elapsed_ms,	    b->peak_mem, plugin->mem_limit,	    b->http_total,	    b->peak_http_inflight,	    peak_ms);} /* * Called by lua_http.c when an HTTP request completes (success or error). * Decrements the in-flight counter. plugin may be NULL (defensive). */voidclm_lua_http_done(struct clm_lua_plugin *plugin){	if (plugin == NULL)		return;	if (plugin->budget.http_inflight > 0)		plugin->budget.http_inflight--;} /* * Called by lua_http.c before starting an HTTP request. Returns 0 if allowed, * -1 if the budget is exceeded (caller should raise an error). */intclm_lua_http_start(struct clm_lua_plugin *plugin){	struct clm_lua_budget *b; 	if (plugin == NULL)		return 0;	b = &plugin->budget;	if (b->http_inflight >= b->http_max_inflight)		return -1;	if (b->http_total >= b->http_max_per_call)		return -2;	b->http_inflight++;	b->http_total++;	if (b->http_inflight > b->peak_http_inflight)		b->peak_http_inflight = b->http_inflight;	return 0;} /* * clm_tool_fn callback: called by the agent framework when a Lua tool is * invoked. Runs the plugin's invoke function as a coroutine so that async * operations (HTTP) can yield. */static voidlua_tool_invoke(struct clm_tool_invocation *inv, void *user){	struct lua_tool_user *tu = user;	struct clm_lua_plugin *plugin = tu->plugin;	lua_State *L = plugin->L;	lua_State *co;	struct lua_tool_ctx *ctx;	int nres, rc; 	/* Reset per-call budget counters. */	plugin->budget.http_total = 0;	plugin->budget.call_start_ns = clock_ns(); 	/* lua_newthread() is a bare C-API call, not wrapped in lua_pcall (it	 * can't be -- there's no Lua frame to unwind into yet). If the	 * capped allocator refuses this allocation, Lua's OOM error has	 * nowhere protected to longjmp to, so its default panic handler	 * calls abort() and takes the whole process down -- lua_newthread()	 * does NOT return NULL on OOM the way the check below assumes; it	 * never returns at all. Refuse proactively instead, with a safety	 * margin covering what one invocation typically needs (the	 * coroutine object itself, its stack, the decoded args table, and	 * response bodies), so we fail this one tool call cleanly via	 * clm_tool_fail() instead of crashing every in-flight turn. */	if (plugin->mem_used + 262144 > plugin->mem_limit) {		clm_tool_fail(inv, "plugin is low on memory, refusing to start "		    "a new invocation to avoid crashing");		return;	} 	/* Create a coroutine for this invocation. */	co = lua_newthread(L);	if (co == NULL) {		clm_tool_fail(inv, "failed to create Lua coroutine");		return;	}	/* Keep coroutine rooted on the main stack while it runs. We'll pop	 * it once the tool completes (synchronously or after resume). */	int co_ref = luaL_ref(L, LUA_REGISTRYINDEX); 	/* Mark this coroutine as the live invocation thread so http.get/post can	 * verify they run directly on it (not a nested coroutine). Cleared on	 * every terminal path below, but left set across a yield so the resumed	 * coroutine may issue further http calls. */	clm_lua_mark_invocation_thread(L, co, 1); 	/* Push the invoke function onto the coroutine stack. */	lua_rawgeti(co, LUA_REGISTRYINDEX, tu->invoke_ref); 	/* Decode tool args directly in C (avoids unprotected lua_call and the	 * C->Lua->C round trip through json.decode). */	const char *args_str = clm_tool_invocation_args(inv);	cJSON *args_obj = cJSON_Parse(args_str ? args_str : "{}");	if (args_obj == NULL) {		clm_tool_fail(inv, "invalid tool arguments (malformed JSON)");		clm_lua_mark_invocation_thread(L, co, 0);		luaL_unref(L, LUA_REGISTRYINDEX, co_ref);		return;	}	clm_lua_push_json_value(co, args_obj);	cJSON_Delete(args_obj); 	/* Push ctx userdata. */	ctx = lua_newuserdatauv(co, sizeof(*ctx), 0);	ctx->inv = inv;	ctx->main_L = L;	ctx->co_ref = co_ref;	ctx->completed = false;	luaL_setmetatable(co, CLM_LUA_CTX_META); 	/* Store co_ref on the coroutine's registry so http functions	 * (which run on `co`) can retrieve it for the async callback. */	lua_pushinteger(co, co_ref);	lua_setfield(co, LUA_REGISTRYINDEX, "_clm_co_ref"); 	/* Store tool name for budget attribution in async callbacks. */	lua_pushstring(co, clm_tool_invocation_name(inv));	lua_setfield(co, LUA_REGISTRYINDEX, "_clm_tool_name"); 	/* Store invocation pointer for error reporting from HTTP callbacks. */	lua_pushlightuserdata(co, inv);	lua_setfield(co, LUA_REGISTRYINDEX, "_clm_inv"); 	/* Set execution deadline and install the count hook. The hook fires	 * every N instructions and raises if the deadline has passed. */	uint64_t timeout = clm_tool_invocation_timeout_ms(inv);	if (timeout == 0)		timeout = CLM_LUA_EXEC_TIMEOUT_MS;	plugin->deadline_ns = clock_ns() + timeout * 1000000ULL;	lua_sethook(co, lua_exec_hook, LUA_MASKCOUNT, CLM_LUA_HOOK_INTERVAL); 

Lines 1–500 of 1891.