611a047bef
Eine lmcp-Instanz konnte ihren Werkzeugsatz bisher nur ERWEITERN.
tools.d-Dateien fuegen hinzu; der Grundstock aus server.lua bringt
shell, shell_bg, write_file, edit_file, read_file, fetch und web_search
mit, und eine Plugin-Datei kann nichts wegnehmen.
Gemessen am 2026-08-08 im bullpen: jeder Agent-Container haelt den
Raum-Token, also hatte JEDER von ihnen eine Wurzelschale im
Raum-Container. Nachgewiesen aus dem foreman-Container -> uid=0(root),
hostname room, Schreibzugriff auf room.jsonl. Das ist keine
Einbruchsluecke (alle Container sind eine Sicherheitsdomaene), aber es
macht jede Aussage ueber Rollentrennung unbelegbar: der Orchestrator,
der laut Entwurf KEINE Schale haben darf ("Raumtext ist nicht
vertrauenswuerdig"), hat eine — und hat sie im ersten Durchlauf
unaufgefordert benutzt, um die Abnahme seiner eigenen Koordination
auszufuehren.
GEPRUEFT WIRD BEI DER REGISTRIERUNG, nicht nachtraeglich loeschend.
Die Alternative waere eine Plugin-Datei, die nach dem Laden aus
server.tools entfernt — die muss jeden kuenftigen Grundstock-Eintrag
kennen und ist damit wieder eine Liste, die jemand pflegen muss. Genau
solche Listen laufen auseinander (siehe PRIVILEGED, zwei Kopien in zwei
Dateien). An der Registrierung ist die Regel EINE Aussage: was nicht auf
der Liste steht, entsteht nicht — Built-ins wie Plugins, heute wie fuer
alles, was morgen dazukommt.
Rueckwaertsvertraeglich: ohne LMCP_TOOL_ALLOW aendert sich nichts.
Verweigern ist stumm und wirft nicht, damit ein Plugin, das ein
gesperrtes Werkzeug anbietet, weiterlaeuft statt abzustuerzen; tool()
bleibt verkettbar.
tests/test_tool_allow.lua weist es in beide Richtungen nach: Rueckgabe 0
gegen diese Fassung, Rueckgabe 1 mit sieben Fehlschlaegen gegen die
vorige. Der Test prueft ausdruecklich, dass ein verweigertes Werkzeug
auch nicht AUFRUFBAR ist — tools/list und tools/call lesen dasselbe
Register, ein reiner Anzeigefilter waere wertlos gewesen.
Zwei Fallen im Test selbst, als Kommentar festgehalten, weil sie mich
zwei Runden gekostet haben: package.path muss VORNE ergaenzt werden,
sonst gewinnt die installierte /usr/share/lua/5.4/lmcp.lua und der Test
misst den falschen Baum; und die Rueckwaerts-Pruefung darf nur im
Elternlauf laufen, im Kind ist die Liste gesetzt.
125 lines
5.2 KiB
Lua
125 lines
5.2 KiB
Lua
-- 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)
|