-- Ausfuehrbare Zusicherung fuer LMCP_TOOL_ALLOW. -- -- Hintergrund: eine lmcp-Instanz konnte ihren Werkzeugsatz nur ERWEITERN. -- tools.d-Dateien fuegen hinzu; der Grundstock aus server.lua bringt shell, -- write_file und Verwandte mit, und eine Plugin-Datei kann nichts wegnehmen. -- Am 2026-08-08 hatte damit jeder Agent mit dem Raum-Token eine Wurzelschale -- im Raum-Container -- nachgewiesen: uid=0(root), Schreibzugriff auf -- room.jsonl. Das ist keine Einbruchsluecke (eine Sicherheitsdomaene), aber -- es macht jede Aussage ueber Rollentrennung unbelegbar. -- -- Geprueft wird an der REGISTRIERUNG, nicht nachtraeglich loeschend: was nicht -- auf der Liste steht, entsteht gar nicht -- fuer Built-ins wie fuer Plugins, -- heute wie fuer alles, was spaeter dazukommt. -- -- Aufruf: lua5.4 tests/test_tool_allow.lua local hier = arg[0]:match('(.*/)') or './' -- VORNE anhaengen, nicht hinten. Sonst gewinnt die INSTALLIERTE Fassung unter -- /usr/share/lua/5.4/lmcp.lua, und der Test prueft nicht den Baum, in dem er -- liegt -- gemessen am 2026-08-08: der Test gab rot, obwohl der Code stimmte. package.path = hier .. '../?.lua;' .. package.path local fehler = 0 local function pruefe(name, bedingung, detail) if bedingung then print(string.format("[ok ] %s", name)) else fehler = fehler + 1 print(string.format("[FEHLER] %s%s", name, detail and (" -> " .. tostring(detail)) or "")) end end local function namen(server) local t = {} for n in pairs(server.tools) do t[#t + 1] = n end table.sort(t) return t end local function enthaelt(liste, wert) for _, v in ipairs(liste) do if v == wert then return true end end return false end -- lmcp frisch laden, damit die Umgebungsvariable beim Anlegen gilt. local function frisch() package.loaded['lmcp'] = nil return require('lmcp') end local leer = { type = "object" } local function nichts() return "x" end -- 1. Ohne die Variable aendert sich nichts (Rueckwaertsvertraeglichkeit). -- Nur im ELTERNLAUF: im Kind ist die Liste gesetzt, dort waere die Aussage -- falsch und der Test wuerde sich selbst widerlegen. if os.getenv("LMCP_TOOL_ALLOW") == nil then local lmcp = frisch() local s = lmcp.new("probe-offen", { port = 0 }) s:tool("room_say", "d", leer, nichts) s:tool("shell", "d", leer, nichts) local n = namen(s) pruefe("ohne LMCP_TOOL_ALLOW bleibt alles registriert", enthaelt(n, "room_say") and enthaelt(n, "shell"), table.concat(n, ",")) end -- Ab hier mit Liste. lmcp liest sie beim Anlegen der Instanz, also muss sie -- VOR lmcp.new() in der Umgebung stehen -- in Lua nur ueber einen Kindprozess -- setzbar, deshalb startet der Test sich selbst neu. if os.getenv("LMCP_TOOL_ALLOW") == nil then local eigen = arg[0] local rc = os.execute( 'LMCP_TOOL_ALLOW="room_say,room_read,lease_acquire" lua5.4 "' .. eigen .. '" --kind') local ok = (rc == true or rc == 0) pruefe("Teillauf mit gesetzter Liste besteht", ok, tostring(rc)) print(fehler .. " Pruefungen fehlgeschlagen") os.exit(fehler == 0 and 0 or 1) end -- --- Kindlauf: LMCP_TOOL_ALLOW ist gesetzt ----------------------------------- do local lmcp = frisch() local s = lmcp.new("probe-eng", { port = 0 }) -- erlaubt s:tool("room_say", "d", leer, nichts) s:tool("room_read", "d", leer, nichts) s:tool("lease_acquire", "d", leer, nichts) -- nicht erlaubt: genau die, die die Wurzelschale ausmachten s:tool("shell", "d", leer, nichts) s:tool("shell_bg", "d", leer, nichts) s:tool("write_file", "d", leer, nichts) s:tool("edit_file", "d", leer, nichts) s:tool("read_file", "d", leer, nichts) local n = namen(s) pruefe("erlaubte Werkzeuge sind da", enthaelt(n, "room_say") and enthaelt(n, "room_read") and enthaelt(n, "lease_acquire"), table.concat(n, ",")) pruefe("shell ist NICHT registriert", not enthaelt(n, "shell")) pruefe("shell_bg ist NICHT registriert", not enthaelt(n, "shell_bg")) pruefe("write_file ist NICHT registriert", not enthaelt(n, "write_file")) pruefe("edit_file ist NICHT registriert", not enthaelt(n, "edit_file")) pruefe("read_file ist NICHT registriert", not enthaelt(n, "read_file")) pruefe("genau drei Werkzeuge uebrig", #n == 3, table.concat(n, ",")) -- Der Kern: `tools/list` und `tools/call` lesen DASSELBE Register. Ein -- nicht registriertes Werkzeug ist also nicht bloss unsichtbar, es ist -- nicht rufbar. Waere es nur aus der Liste gefiltert, bliebe es erreichbar. pruefe("verweigertes Werkzeug ist auch nicht aufrufbar", s.tools["shell"] == nil) -- Die Registrierung darf nicht werfen: ein Plugin, das ein gesperrtes -- Werkzeug anbietet, soll weiterlaufen, nicht abstuerzen. local ok = pcall(function() s:tool("shell", "d", leer, nichts) end) pruefe("Registrierung eines gesperrten Werkzeugs wirft nicht", ok) -- Verkettung muss erhalten bleiben (tool() gibt self zurueck). local zurueck = s:tool("shell", "d", leer, nichts) pruefe("tool() liefert weiterhin self (verkettbar)", zurueck == s) end print(fehler .. " Pruefungen fehlgeschlagen") os.exit(fehler == 0 and 0 or 1)