// 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 "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); #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; struct clm_lua_pending_list pending; uint64_t next_call_serial; bool tearing_down; /* every live tool coroutine, grouped by clm_lua_call. */ struct clm_lua_coroutine_list coroutines;}; 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, lua_State *co, struct clm_lua_pending *pending, clm_lua_pending_teardown_fn teardown){ struct clm_lua_coroutine *lco; if (plugin == NULL || co == NULL || pending == NULL || teardown == NULL) return -EINVAL; if (plugin->tearing_down) return -ECANCELED; lco = clm_lua_coroutine_find(co); if (lco == NULL || lco->plugin != plugin || lco->call->completed) return -ECANCELED; pending->plugin = plugin; pending->coroutine = lco; pending->teardown = teardown; TAILQ_INSERT_TAIL(&plugin->pending, pending, plugin_entry); TAILQ_INSERT_TAIL(&lco->pendings, pending, coroutine_entry); return 0;} struct clm_lua_plugin *clm_lua_pending_remove(struct clm_lua_pending *pending){ struct clm_lua_plugin *plugin; struct clm_lua_coroutine *lco; if (pending == NULL || pending->plugin == NULL) return NULL; plugin = pending->plugin; lco = pending->coroutine; TAILQ_REMOVE(&plugin->pending, pending, plugin_entry); if (lco != NULL) TAILQ_REMOVE(&lco->pendings, pending, coroutine_entry); pending->plugin = NULL; pending->coroutine = 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; } 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"#define CLM_LUA_TASK_META "clm_task" enum clm_lua_task_state { CLM_LUA_TASK_PENDING, CLM_LUA_TASK_DONE, CLM_LUA_TASK_ERROR, CLM_LUA_TASK_CANCELLED,}; struct clm_lua_wait; struct clm_lua_task { struct clm_lua_call *call; uint64_t call_serial; struct clm_lua_coroutine *lco; struct clm_lua_wait *waiter; enum clm_lua_task_state state; int self_ref; int results_ref; int nresults; char *error; bool error_observed;}; struct clm_lua_wait { struct clm_lua_pending pending; struct clm_lua_task **tasks; size_t ntasks; struct clm_agent *agent; struct clm_timer *timer; bool any;}; struct lua_tool_ctx { struct clm_lua_call *call;}; static void lua_call_result(struct clm_lua_call *call, bool failed, const char *text, struct clm_lua_coroutine *current); 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); struct clm_lua_coroutine *lco = clm_lua_coroutine_find(L); const char *result; if (ctx->call->completed) return luaL_error(L, "tool already completed"); if (lco == NULL || !lco->is_main) return luaL_error(L, "ctx:complete may only be called from the main " "tool coroutine"); result = luaL_checkstring(L, 2); if (ctx->call->active_coroutines > 1) { lua_call_result(ctx->call, true, "tool completed with unawaited tasks", lco); return 0; } if (ctx->call->unobserved_errors != 0) { lua_call_result(ctx->call, true, ctx->call->child_error != NULL ? ctx->call->child_error : "task failed without its result being observed", lco); return 0; } lua_call_result(ctx->call, false, result, lco); return 0;} static intlua_ctx_fail(lua_State *L){ struct lua_tool_ctx *ctx = check_ctx(L, 1); struct clm_lua_coroutine *lco = clm_lua_coroutine_find(L); const char *msg; if (ctx->call->completed) return luaL_error(L, "tool already completed"); if (lco == NULL || !lco->is_main) return luaL_error(L, "ctx:fail may only be called from the main " "tool coroutine"); msg = luaL_checkstring(L, 2); lua_call_result(ctx->call, true, msg, lco); return 0;} static intlua_ctx_args_raw(lua_State *L){ struct lua_tool_ctx *ctx = check_ctx(L, 1); const char *args = ctx->call->inv != NULL ? clm_tool_invocation_args(ctx->call->inv) : NULL; 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);} static int lua_task_wait(lua_State *L);static int lua_task_result(lua_State *L);static int lua_task_cancel_method(lua_State *L); static struct clm_lua_task *check_task(lua_State *L, int idx){ return luaL_checkudata(L, idx, CLM_LUA_TASK_META);} static intlua_task_gc(lua_State *L){ struct clm_lua_task *task = check_task(L, 1); if (task->results_ref != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, task->results_ref); task->results_ref = LUA_NOREF; } free(task->error); task->error = NULL; return 0;} static const luaL_Reg task_methods[] = { {"wait", lua_task_wait}, {"result", lua_task_result}, {"cancel", lua_task_cancel_method}, {NULL, NULL},}; static voidregister_task_meta(lua_State *L){ luaL_newmetatable(L, CLM_LUA_TASK_META); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); luaL_setfuncs(L, task_methods, 0); lua_pushcfunction(L, lua_task_gc); lua_setfield(L, -2, "__gc"); lua_pushstring(L, "clm task"); lua_setfield(L, -2, "__name"); 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; struct clm_lua_coroutine *lco = clm_lua_coroutine_find(co); uint64_t previous_deadline = plugin->deadline_ns; plugin->deadline_ns = clock_ns() + timeout_ms * 1000000ULL; lua_sethook(co, lua_exec_hook, LUA_MASKCOUNT, CLM_LUA_HOOK_INTERVAL); if (lco != NULL) lco->running = true; rc = lua_resume(co, from, nargs, nresults); if (lco != NULL) lco->running = false; plugin->deadline_ns = previous_deadline; lua_sethook(co, NULL, 0, 0); return rc;} static void lua_task_cancel_internal(struct clm_lua_task *task, const char *reason);static void lua_wait_task_done(struct clm_lua_task *task); /* register a coroutine while its thread object is on owner's stack. */struct clm_lua_coroutine *clm_lua_coroutine_register(struct clm_lua_plugin *plugin, struct clm_lua_call *call, lua_State *owner, lua_State *co, bool is_main){ struct clm_lua_coroutine *lco; if (plugin == NULL || call == NULL || owner == NULL || co == NULL || lua_tothread(owner, -1) != co) return NULL; lco = calloc(1, sizeof(*lco)); if (lco == NULL) return NULL; lco->plugin = plugin; lco->call = call; lco->co = co; lco->main_L = plugin->L; lco->agent = plugin->agent; lco->is_main = is_main; TAILQ_INIT(&lco->pendings); /* lua_newthread left the thread on owner; copy and root that value. */ lua_pushvalue(owner, -1); lco->ref = luaL_ref(owner, LUA_REGISTRYINDEX); TAILQ_INSERT_TAIL(&plugin->coroutines, lco, entry); call->active_coroutines++; return lco;} voidclm_lua_coroutine_unregister(struct clm_lua_coroutine *lco){ struct clm_lua_call *call; if (lco == NULL) return; while (!TAILQ_EMPTY(&lco->pendings)) { struct clm_lua_pending *pending = TAILQ_FIRST(&lco->pendings); (void)clm_lua_pending_remove(pending); pending->teardown(pending); } if (lco->task != NULL) { lua_task_cancel_internal(lco->task, "task cancelled");Lines 1–500 of 2759. 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 "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); #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; struct clm_lua_pending_list pending; uint64_t next_call_serial; bool tearing_down; /* every live tool coroutine, grouped by clm_lua_call. */ struct clm_lua_coroutine_list coroutines; }; 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, lua_State *co, struct clm_lua_pending *pending, clm_lua_pending_teardown_fn teardown) { struct clm_lua_coroutine *lco; if (plugin == NULL || co == NULL || pending == NULL || teardown == NULL) return -EINVAL; if (plugin->tearing_down) return -ECANCELED; lco = clm_lua_coroutine_find(co); if (lco == NULL || lco->plugin != plugin || lco->call->completed) return -ECANCELED; pending->plugin = plugin; pending->coroutine = lco; pending->teardown = teardown; TAILQ_INSERT_TAIL(&plugin->pending, pending, plugin_entry); TAILQ_INSERT_TAIL(&lco->pendings, pending, coroutine_entry); return 0; } struct clm_lua_plugin * clm_lua_pending_remove(struct clm_lua_pending *pending) { struct clm_lua_plugin *plugin; struct clm_lua_coroutine *lco; if (pending == NULL || pending->plugin == NULL) return NULL; plugin = pending->plugin; lco = pending->coroutine; TAILQ_REMOVE(&plugin->pending, pending, plugin_entry); if (lco != NULL) TAILQ_REMOVE(&lco->pendings, pending, coroutine_entry); pending->plugin = NULL; pending->coroutine = 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; } 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" #define CLM_LUA_TASK_META "clm_task" enum clm_lua_task_state { CLM_LUA_TASK_PENDING, CLM_LUA_TASK_DONE, CLM_LUA_TASK_ERROR, CLM_LUA_TASK_CANCELLED, }; struct clm_lua_wait; struct clm_lua_task { struct clm_lua_call *call; uint64_t call_serial; struct clm_lua_coroutine *lco; struct clm_lua_wait *waiter; enum clm_lua_task_state state; int self_ref; int results_ref; int nresults; char *error; bool error_observed; }; struct clm_lua_wait { struct clm_lua_pending pending; struct clm_lua_task **tasks; size_t ntasks; struct clm_agent *agent; struct clm_timer *timer; bool any; }; struct lua_tool_ctx { struct clm_lua_call *call; }; static void lua_call_result(struct clm_lua_call *call, bool failed, const char *text, struct clm_lua_coroutine *current); 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); struct clm_lua_coroutine *lco = clm_lua_coroutine_find(L); const char *result; if (ctx->call->completed) return luaL_error(L, "tool already completed"); if (lco == NULL || !lco->is_main) return luaL_error(L, "ctx:complete may only be called from the main " "tool coroutine"); result = luaL_checkstring(L, 2); if (ctx->call->active_coroutines > 1) { lua_call_result(ctx->call, true, "tool completed with unawaited tasks", lco); return 0; } if (ctx->call->unobserved_errors != 0) { lua_call_result(ctx->call, true, ctx->call->child_error != NULL ? ctx->call->child_error : "task failed without its result being observed", lco); return 0; } lua_call_result(ctx->call, false, result, lco); return 0; } static int lua_ctx_fail(lua_State *L) { struct lua_tool_ctx *ctx = check_ctx(L, 1); struct clm_lua_coroutine *lco = clm_lua_coroutine_find(L); const char *msg; if (ctx->call->completed) return luaL_error(L, "tool already completed"); if (lco == NULL || !lco->is_main) return luaL_error(L, "ctx:fail may only be called from the main " "tool coroutine"); msg = luaL_checkstring(L, 2); lua_call_result(ctx->call, true, msg, lco); return 0; } static int lua_ctx_args_raw(lua_State *L) { struct lua_tool_ctx *ctx = check_ctx(L, 1); const char *args = ctx->call->inv != NULL ? clm_tool_invocation_args(ctx->call->inv) : NULL; 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); } static int lua_task_wait(lua_State *L); static int lua_task_result(lua_State *L); static int lua_task_cancel_method(lua_State *L); static struct clm_lua_task * check_task(lua_State *L, int idx) { return luaL_checkudata(L, idx, CLM_LUA_TASK_META); } static int lua_task_gc(lua_State *L) { struct clm_lua_task *task = check_task(L, 1); if (task->results_ref != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, task->results_ref); task->results_ref = LUA_NOREF; } free(task->error); task->error = NULL; return 0; } static const luaL_Reg task_methods[] = { {"wait", lua_task_wait}, {"result", lua_task_result}, {"cancel", lua_task_cancel_method}, {NULL, NULL}, }; static void register_task_meta(lua_State *L) { luaL_newmetatable(L, CLM_LUA_TASK_META); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); luaL_setfuncs(L, task_methods, 0); lua_pushcfunction(L, lua_task_gc); lua_setfield(L, -2, "__gc"); lua_pushstring(L, "clm task"); lua_setfield(L, -2, "__name"); 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; struct clm_lua_coroutine *lco = clm_lua_coroutine_find(co); uint64_t previous_deadline = plugin->deadline_ns; plugin->deadline_ns = clock_ns() + timeout_ms * 1000000ULL; lua_sethook(co, lua_exec_hook, LUA_MASKCOUNT, CLM_LUA_HOOK_INTERVAL); if (lco != NULL) lco->running = true; rc = lua_resume(co, from, nargs, nresults); if (lco != NULL) lco->running = false; plugin->deadline_ns = previous_deadline; lua_sethook(co, NULL, 0, 0); return rc; } static void lua_task_cancel_internal(struct clm_lua_task *task, const char *reason); static void lua_wait_task_done(struct clm_lua_task *task); /* register a coroutine while its thread object is on owner's stack. */ struct clm_lua_coroutine * clm_lua_coroutine_register(struct clm_lua_plugin *plugin, struct clm_lua_call *call, lua_State *owner, lua_State *co, bool is_main) { struct clm_lua_coroutine *lco; if (plugin == NULL || call == NULL || owner == NULL || co == NULL || lua_tothread(owner, -1) != co) return NULL; lco = calloc(1, sizeof(*lco)); if (lco == NULL) return NULL; lco->plugin = plugin; lco->call = call; lco->co = co; lco->main_L = plugin->L; lco->agent = plugin->agent; lco->is_main = is_main; TAILQ_INIT(&lco->pendings); /* lua_newthread left the thread on owner; copy and root that value. */ lua_pushvalue(owner, -1); lco->ref = luaL_ref(owner, LUA_REGISTRYINDEX); TAILQ_INSERT_TAIL(&plugin->coroutines, lco, entry); call->active_coroutines++; return lco; } void clm_lua_coroutine_unregister(struct clm_lua_coroutine *lco) { struct clm_lua_call *call; if (lco == NULL) return; while (!TAILQ_EMPTY(&lco->pendings)) { struct clm_lua_pending *pending = TAILQ_FIRST(&lco->pendings); (void)clm_lua_pending_remove(pending); pending->teardown(pending); } if (lco->task != NULL) { lua_task_cancel_internal(lco->task, "task cancelled");