// 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. Load more
// 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 int lua_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 int lua_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 int lua_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 int lua_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 void register_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_t clock_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 void lua_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. */ void clm_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. */ int clm_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). */ void clm_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. */ void clm_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). */ void clm_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). */ int clm_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 void lua_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);