// SPDX-License-Identifier: ISC#include <dirent.h>#include <errno.h>#include <setjmp.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 "lua_internal.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); /* 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 */ /* How far the heap may grow past what is live before the next cycle. Lua's * default of 200 means it doubles, which against an 8 MiB cap reaches the cap * on garbage alone once a plugin holds a few MiB. */#define CLM_LUA_GC_PAUSE 130#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 */}; TAILQ_HEAD(clm_lua_pending_list, clm_lua_pending); /* 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; struct clm_lua_pending_list pending; bool tearing_down; /* Unprotected-call recovery: see lua_plugin_panic. */ jmp_buf panic_jmp; bool panic_ready; bool dead;}; 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;}; intclm_lua_pending_add(struct clm_lua_plugin *plugin, struct clm_lua_pending *pending, clm_lua_pending_teardown_fn teardown){ if (plugin == NULL || pending == NULL || teardown == NULL) return -EINVAL; if (plugin->tearing_down) return -ECANCELED; pending->plugin = plugin; pending->teardown = teardown; TAILQ_INSERT_TAIL(&plugin->pending, pending, entry); return 0;} struct clm_lua_plugin *clm_lua_pending_remove(struct clm_lua_pending *pending){ struct clm_lua_plugin *plugin; if (pending == NULL || pending->plugin == NULL) return NULL; plugin = pending->plugin; TAILQ_REMOVE(&plugin->pending, pending, entry); pending->plugin = NULL; return plugin;} static voidclm_lua_pending_teardown_all(struct clm_lua_plugin *plugin){ struct clm_lua_pending *pending; plugin->tearing_down = true; while ((pending = TAILQ_FIRST(&plugin->pending)) != NULL) { (void)clm_lua_pending_remove(pending); pending->teardown(pending); }} /* 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);int clm_lua_resume_with_deadline(struct clm_lua_plugin *plugin, lua_State *co, lua_State *from, int nargs, int *nresults, uint64_t timeout_ms); /* ------------------------------------------------------------------ *//* 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; } /* Only growth is capped. A shrink already returns memory, and the * collector reallocates downward while it works, so refusing those * would make a plugin at its limit unable to get back under it. */ if (nsize > real_osize && 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;} /* ------------------------------------------------------------------ *//* Panic and warning handlers *//* ------------------------------------------------------------------ */ /* Where an error raised outside any protected call lands; without this Lua * aborts the process. Callers arm panic_ready around bare C API calls and * recover through panic_jmp. Nothing unwound the state, so the plugin is * then dead and L must never be touched again, lua_close included. */static intlua_plugin_panic(lua_State *L){ struct clm_lua_plugin *p = NULL; const char *msg = lua_tostring(L, -1); (void)lua_getallocf(L, (void **)&p); clm_debug("lua panic [%s]: %s", p && p->path ? p->path : "?", msg ? msg : "(unknown)"); if (p != NULL && p->panic_ready) { p->panic_ready = false; p->dead = true; longjmp(p->panic_jmp, 1); } return 0; /* returning from here aborts, as before */} /* Where a Lua warning goes; with no warn function Lua drops them. The one * that matters is an error inside a __gc handler: Lua reports a failing * finalizer through this path instead of raising it. */static voidlua_plugin_warn(void *ud, const char *msg, int tocont){ struct clm_lua_plugin *p = ud; (void)tocont; clm_debug("lua warning [%s]: %s", p && p->path ? p->path : "?", msg ? msg : "");} /* ------------------------------------------------------------------ *//* 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");} /* * resume a tool coroutine with a fresh execution deadline. the deadline is * active only while lua bytecode runs, so time suspended on async work does * not consume the execution budget. */intclm_lua_resume_with_deadline(struct clm_lua_plugin *plugin, lua_State *co, lua_State *from, int nargs, int *nresults, uint64_t timeout_ms){ int rc; if (timeout_ms == 0) timeout_ms = CLM_LUA_EXEC_TIMEOUT_MS; plugin->deadline_ns = clock_ns() + timeout_ms * 1000000ULL; lua_sethook(co, lua_exec_hook, LUA_MASKCOUNT, CLM_LUA_HOOK_INTERVAL); rc = lua_resume(co, from, nargs, nresults); plugin->deadline_ns = 0; lua_sethook(co, NULL, 0, 0); return rc;} /* * 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). */Lines 1–500 of 2160. Load more
// SPDX-License-Identifier: ISC #include <dirent.h> #include <errno.h> #include <setjmp.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 "lua_internal.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); /* 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 */ /* How far the heap may grow past what is live before the next cycle. Lua's * default of 200 means it doubles, which against an 8 MiB cap reaches the cap * on garbage alone once a plugin holds a few MiB. */ #define CLM_LUA_GC_PAUSE 130 #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 */ }; TAILQ_HEAD(clm_lua_pending_list, clm_lua_pending); /* 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; struct clm_lua_pending_list pending; bool tearing_down; /* Unprotected-call recovery: see lua_plugin_panic. */ jmp_buf panic_jmp; bool panic_ready; bool dead; }; 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; }; int clm_lua_pending_add(struct clm_lua_plugin *plugin, struct clm_lua_pending *pending, clm_lua_pending_teardown_fn teardown) { if (plugin == NULL || pending == NULL || teardown == NULL) return -EINVAL; if (plugin->tearing_down) return -ECANCELED; pending->plugin = plugin; pending->teardown = teardown; TAILQ_INSERT_TAIL(&plugin->pending, pending, entry); return 0; } struct clm_lua_plugin * clm_lua_pending_remove(struct clm_lua_pending *pending) { struct clm_lua_plugin *plugin; if (pending == NULL || pending->plugin == NULL) return NULL; plugin = pending->plugin; TAILQ_REMOVE(&plugin->pending, pending, entry); pending->plugin = NULL; return plugin; } static void clm_lua_pending_teardown_all(struct clm_lua_plugin *plugin) { struct clm_lua_pending *pending; plugin->tearing_down = true; while ((pending = TAILQ_FIRST(&plugin->pending)) != NULL) { (void)clm_lua_pending_remove(pending); pending->teardown(pending); } } /* 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); int clm_lua_resume_with_deadline(struct clm_lua_plugin *plugin, lua_State *co, lua_State *from, int nargs, int *nresults, uint64_t timeout_ms); /* ------------------------------------------------------------------ */ /* 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; } /* Only growth is capped. A shrink already returns memory, and the * collector reallocates downward while it works, so refusing those * would make a plugin at its limit unable to get back under it. */ if (nsize > real_osize && 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; } /* ------------------------------------------------------------------ */ /* Panic and warning handlers */ /* ------------------------------------------------------------------ */ /* Where an error raised outside any protected call lands; without this Lua * aborts the process. Callers arm panic_ready around bare C API calls and * recover through panic_jmp. Nothing unwound the state, so the plugin is * then dead and L must never be touched again, lua_close included. */ static int lua_plugin_panic(lua_State *L) { struct clm_lua_plugin *p = NULL; const char *msg = lua_tostring(L, -1); (void)lua_getallocf(L, (void **)&p); clm_debug("lua panic [%s]: %s", p && p->path ? p->path : "?", msg ? msg : "(unknown)"); if (p != NULL && p->panic_ready) { p->panic_ready = false; p->dead = true; longjmp(p->panic_jmp, 1); } return 0; /* returning from here aborts, as before */ } /* Where a Lua warning goes; with no warn function Lua drops them. The one * that matters is an error inside a __gc handler: Lua reports a failing * finalizer through this path instead of raising it. */ static void lua_plugin_warn(void *ud, const char *msg, int tocont) { struct clm_lua_plugin *p = ud; (void)tocont; clm_debug("lua warning [%s]: %s", p && p->path ? p->path : "?", msg ? msg : ""); } /* ------------------------------------------------------------------ */ /* 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"); } /* * resume a tool coroutine with a fresh execution deadline. the deadline is * active only while lua bytecode runs, so time suspended on async work does * not consume the execution budget. */ int clm_lua_resume_with_deadline(struct clm_lua_plugin *plugin, lua_State *co, lua_State *from, int nargs, int *nresults, uint64_t timeout_ms) { int rc; if (timeout_ms == 0) timeout_ms = CLM_LUA_EXEC_TIMEOUT_MS; plugin->deadline_ns = clock_ns() + timeout_ms * 1000000ULL; lua_sethook(co, lua_exec_hook, LUA_MASKCOUNT, CLM_LUA_HOOK_INTERVAL); rc = lua_resume(co, from, nargs, nresults); plugin->deadline_ns = 0; lua_sethook(co, NULL, 0, 0); return rc; } /* * 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). */