//! Extension discovery: walk well-known directories (plus any extra //! `paths` and installed `rocks`), evaluate each Lua source to learn what //! extension *entries* it declares, then activate the surviving ones into a //! long-lived `LuaRuntime`. //! //! Sources, in precedence order. Cross-layer: project > user > base. //! Within a layer: `rocks < paths < dir` (the canonical dir is most //! authoritative locally, a rock least). The scanned dirs are: //! //! /agent/{extensions,tools}/ ("base") //! ${XDG_CONFIG_HOME:-$HOME/.config}/panto/{extensions,tools}/ ("user") //! ./.panto/{extensions,tools}/ ("project") //! //! The `base` layer is staged at bootstrap from files embedded into the //! binary (see `build/gen_agent_embed.zig`). `extensions/` and `tools/` are //! no longer distinct namespaces — both just contribute entries. //! //! Layout per directory: //! - `.lua` -- single-file source. //! - `/init.lua` -- directory source; the directory is added to //! `package.path` so it can `require` siblings. //! //! Each source is eval'd (side-effect-free) and must return an *entry* //! `{ name, activate }`, the sugar tool form `{ name, handler, schema, ... }`, //! or a list of those. Identity is the declared `name`, not the filename. //! //! Resolution (two passes): //! 1. Availability — eval every source, collect `name → entry`. Later //! (higher-precedence) source wins for the same name; same-precedence //! duplicate is an error. //! 2. Activation — for every permitted name (per the `[extensions]` //! policy), call `entry.activate()`. //! //! Symlinks: followed normally. Dotfiles and `_`-prefixed files: skipped. const std = @import("std"); const panto = @import("panto"); const lua_runtime = @import("lua_runtime.zig"); const config_file = @import("config_file.zig"); const Allocator = std.mem.Allocator; const Io = std.Io; const LuaRuntime = lua_runtime.LuaRuntime; const Entry = LuaRuntime.Entry; pub const Layer = enum(u8) { base = 0, user = 1, project = 2 }; /// Within-layer origin, ordered least → most authoritative. pub const Origin = enum(u8) { rocks = 0, paths = 1, dir = 2 }; /// Where a source came from, and thus its precedence. Higher `rank()` wins /// when two sources declare the same entry name. pub const Source = struct { layer: Layer, origin: Origin, pub fn rank(self: Source) u8 { return (@as(u8, @intFromEnum(self.layer)) << 2) | @intFromEnum(self.origin); } pub fn label(self: Source) []const u8 { return switch (self.layer) { .base => "base", .user => "user", .project => "project", }; } }; /// One directory to scan, tagged with its source precedence. pub const ScanDir = struct { path: []const u8, source: Source, }; /// An installed rock to load as an extension source: the Lua module name to /// `require`, tagged with its source precedence. pub const RockSource = struct { module: []const u8, source: Source, }; /// A discovered Lua source file before evaluation. Owns its strings. const Found = struct { script_path: []u8, /// For directory-style entries, the directory added to `package.path`. package_root: ?[]u8, source: Source, pub fn deinit(self: *Found, allocator: Allocator) void { allocator.free(self.script_path); if (self.package_root) |p| allocator.free(p); } }; /// A runtime `Entry` paired with the source it came from, for shadowing and /// policy resolution. const Candidate = struct { entry: Entry, source: Source, script_path: []const u8, // borrowed from a `Found`, for diagnostics }; fn policyPermits(p: ?*const config_file.Policy, name: []const u8) bool { const pol = p orelse return true; return pol.permits(name); } /// Log at `err` in production but `warn` under test — the Zig test runner /// fails any test that logs an error, and several tests deliberately /// exercise error paths. fn logConflict(comptime fmt: []const u8, args: anytype) void { if (@import("builtin").is_test) std.log.warn(fmt, args) else std.log.err(fmt, args); } /// Discover and load every extension into `runtime`, returning the number of /// registered tools this call added. /// /// `base_agent_dir`, when non-null, is where embedded base sources have been /// staged (typically `/agent/`); pass `null` to skip base. /// `environ_map` is consulted for `HOME`/`XDG_CONFIG_HOME`; project dirs are /// `cwd()/.panto/{extensions,tools}`. pub fn discoverAndLoad( allocator: Allocator, io: Io, environ_map: *const std.process.Environ.Map, base_agent_dir: ?[]const u8, runtime: *LuaRuntime, policy: ?*const config_file.Policy, /// Extra dirs from `extensions.paths`, each tagged with its config layer. extra_paths: []const config_file.LayeredStr, /// Installed rocks from `extensions.rocks` (raw dependency specs), each /// tagged with its config layer. The Lua module `require`d is the spec's /// first whitespace-delimited token (the rock name). Installation happens /// upstream (see `main`); this only loads already-installed modules. rocks: []const config_file.LayeredStr, ) !usize { var dirs: std.array_list.Managed(ScanDir) = .init(allocator); defer { for (dirs.items) |d| allocator.free(d.path); dirs.deinit(); } const cwd = try std.process.currentPathAlloc(io, allocator); defer allocator.free(cwd); // base: /agent/{extensions,tools} if (base_agent_dir) |d| { try dirs.append(.{ .path = try std.fs.path.join(allocator, &.{ d, "extensions" }), .source = .{ .layer = .base, .origin = .dir } }); try dirs.append(.{ .path = try std.fs.path.join(allocator, &.{ d, "tools" }), .source = .{ .layer = .base, .origin = .dir } }); } // user: $XDG_CONFIG_HOME/panto/{extensions,tools} if (try userConfigDir(allocator, environ_map)) |base| { defer allocator.free(base); try dirs.append(.{ .path = try std.fs.path.join(allocator, &.{ base, "extensions" }), .source = .{ .layer = .user, .origin = .dir } }); try dirs.append(.{ .path = try std.fs.path.join(allocator, &.{ base, "tools" }), .source = .{ .layer = .user, .origin = .dir } }); } // project: cwd/.panto/{extensions,tools} try dirs.append(.{ .path = try std.fs.path.join(allocator, &.{ cwd, ".panto", "extensions" }), .source = .{ .layer = .project, .origin = .dir } }); try dirs.append(.{ .path = try std.fs.path.join(allocator, &.{ cwd, ".panto", "tools" }), .source = .{ .layer = .project, .origin = .dir } }); // extensions.paths: extra dirs at their config layer's precedence, // origin=paths (below the canonical dir, above rocks). Relative paths // resolve against cwd. for (extra_paths) |p| { const path = if (std.fs.path.isAbsolute(p.value)) try allocator.dupe(u8, p.value) else try std.fs.path.join(allocator, &.{ cwd, p.value }); try dirs.append(.{ .path = path, .source = .{ .layer = configLayer(p.layer), .origin = .paths } }); } // extensions.rocks: require each rock's module (its name = first token). var rock_srcs: std.array_list.Managed(RockSource) = .init(allocator); defer rock_srcs.deinit(); for (rocks) |r| { var it = std.mem.tokenizeAny(u8, r.value, " \t"); const module = it.next() orelse continue; try rock_srcs.append(.{ .module = module, .source = .{ .layer = configLayer(r.layer), .origin = .rocks } }); } return loadFromDirs(allocator, io, runtime, dirs.items, rock_srcs.items, policy); } /// Map a config layer index (base=0, user=1, project=2, local=3) to a loader /// precedence layer. The `local` layer shares `project` precedence. fn configLayer(layer: u16) Layer { return switch (layer) { 0 => .base, 1 => .user, else => .project, }; } /// Lower-level entry point: scan an explicit list of dirs (each tagged with /// its source precedence), then eval → shadow → filter → activate. pub fn loadFromDirs( allocator: Allocator, io: Io, runtime: *LuaRuntime, dirs: []const ScanDir, rocks: []const RockSource, policy: ?*const config_file.Policy, ) !usize { var found: std.array_list.Managed(Found) = .init(allocator); defer { for (found.items) |*f| f.deinit(allocator); found.deinit(); } for (dirs) |d| try scanDir(allocator, io, d.path, d.source, &found); // ---- Pass 1: eval every source, collect candidate entries. ---- var cands: std.array_list.Managed(Candidate) = .init(allocator); defer { // Any candidate still here at scope exit was neither activated nor // dropped (an error path); reclaim its Lua ref + name. for (cands.items) |cnd| runtime.dropEntry(cnd.entry); cands.deinit(); } for (found.items) |f| { var entries: std.array_list.Managed(Entry) = .init(allocator); defer entries.deinit(); try runtime.evalEntries(f.script_path, f.package_root, &entries); for (entries.items) |e| { try cands.append(.{ .entry = e, .source = f.source, .script_path = f.script_path }); } } // Rocks: `require` each installed module. A rock that fails to load // (not installed, bad shape) is logged and skipped rather than aborting // startup — one bad optional rock should not kill the REPL. for (rocks) |r| { var entries: std.array_list.Managed(Entry) = .init(allocator); defer entries.deinit(); runtime.evalEntriesFromModule(r.module, &entries) catch |err| { logConflict("rock '{s}' failed to load: {t}", .{ r.module, err }); for (entries.items) |e| runtime.dropEntry(e); continue; }; for (entries.items) |e| { try cands.append(.{ .entry = e, .source = r.source, .script_path = r.module }); } } // ---- Shadowing: keep the highest-precedence entry per name. ---- try applyShadowing(allocator, runtime, &cands); // ---- Pass 2: activate permitted survivors. ---- const before = runtime.toolCount(); var i: usize = 0; while (i < cands.items.len) : (i += 1) { const cnd = cands.items[i]; if (!policyPermits(policy, cnd.entry.name)) { std.log.debug("extension: '{s}' denied by policy", .{cnd.entry.name}); runtime.dropEntry(cnd.entry); continue; } runtime.activateEntry(cnd.entry) catch |err| { logConflict( "extension '{s}' ({s}: {s}) failed to activate: {t}", .{ cnd.entry.name, cnd.source.label(), cnd.script_path, err }, ); // activateEntry consumed the entry already. Drop the rest. var j = i + 1; while (j < cands.items.len) : (j += 1) runtime.dropEntry(cands.items[j].entry); cands.clearRetainingCapacity(); return err; }; std.log.debug("extension: activated '{s}' ({s})", .{ cnd.entry.name, cnd.source.label() }); } cands.clearRetainingCapacity(); return runtime.toolCount() - before; } // --------------------------------------------------------------------------- // Path resolution // --------------------------------------------------------------------------- /// `${XDG_CONFIG_HOME:-$HOME/.config}/panto`, or null if neither is set. fn userConfigDir(allocator: Allocator, environ_map: *const std.process.Environ.Map) !?[]u8 { if (environ_map.get("XDG_CONFIG_HOME")) |xdg| { return try std.fs.path.join(allocator, &.{ xdg, "panto" }); } if (environ_map.get("HOME")) |home| { return try std.fs.path.join(allocator, &.{ home, ".config", "panto" }); } return null; } // --------------------------------------------------------------------------- // Directory scanning // --------------------------------------------------------------------------- fn scanDir( allocator: Allocator, io: Io, dir_path: []const u8, source: Source, out: *std.array_list.Managed(Found), ) !void { var dir = Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound, error.NotDir => return, else => |e| return e, }; defer dir.close(io); var iter = dir.iterate(); while (try iter.next(io)) |entry| { if (entry.name.len == 0 or entry.name[0] == '.') continue; const maybe_found: ?Found = switch (entry.kind) { .file, .sym_link => try classifyFile(allocator, dir_path, entry.name, source), .directory => try classifyDirectory(allocator, io, dir, dir_path, entry.name, source), else => null, }; if (maybe_found) |f| try out.append(f); } } fn classifyFile( allocator: Allocator, dir_path: []const u8, entry_name: []const u8, source: Source, ) !?Found { if (!std.mem.endsWith(u8, entry_name, ".lua")) return null; const base = entry_name[0 .. entry_name.len - ".lua".len]; if (base.len == 0) return null; if (base[0] == '_') return null; const script_path = try std.fs.path.join(allocator, &.{ dir_path, entry_name }); errdefer allocator.free(script_path); const package_root = try allocator.dupe(u8, dir_path); errdefer allocator.free(package_root); return Found{ .script_path = script_path, .package_root = package_root, .source = source }; } fn classifyDirectory( allocator: Allocator, io: Io, parent: Io.Dir, dir_path: []const u8, entry_name: []const u8, source: Source, ) !?Found { var sub = parent.openDir(io, entry_name, .{}) catch return null; defer sub.close(io); sub.access(io, "init.lua", .{}) catch return null; const package_root = try std.fs.path.join(allocator, &.{ dir_path, entry_name }); errdefer allocator.free(package_root); const script_path = try std.fs.path.join(allocator, &.{ package_root, "init.lua" }); errdefer allocator.free(script_path); return Found{ .script_path = script_path, .package_root = package_root, .source = source }; } // --------------------------------------------------------------------------- // Shadowing // --------------------------------------------------------------------------- /// Keep the highest-precedence candidate per declared name; drop the rest /// (their Lua refs are reclaimed). Two candidates with the same name AND the /// same source precedence are an ambiguous duplicate — an error. fn applyShadowing( allocator: Allocator, runtime: *LuaRuntime, cands: *std.array_list.Managed(Candidate), ) !void { // name -> index of current winner in `cands`. var winners: std.StringHashMap(usize) = .init(allocator); defer winners.deinit(); const keep = try allocator.alloc(bool, cands.items.len); defer allocator.free(keep); @memset(keep, true); for (cands.items, 0..) |cnd, i| { const gop = try winners.getOrPut(cnd.entry.name); if (!gop.found_existing) { gop.value_ptr.* = i; continue; } const prev = gop.value_ptr.*; const prev_rank = cands.items[prev].source.rank(); const cur_rank = cnd.source.rank(); if (cur_rank == prev_rank) { logConflict( "extension name '{s}' declared by two same-precedence sources: {s} and {s}", .{ cnd.entry.name, cands.items[prev].script_path, cnd.script_path }, ); return error.DuplicateExtensionName; } else if (cur_rank > prev_rank) { keep[prev] = false; std.log.debug("extension: '{s}' from {s} shadowed by {s}", .{ cnd.entry.name, cands.items[prev].source.label(), cnd.source.label() }); gop.value_ptr.* = i; } else { keep[i] = false; std.log.debug("extension: '{s}' from {s} shadowed by {s}", .{ cnd.entry.name, cnd.source.label(), cands.items[prev].source.label() }); } } var write: usize = 0; for (cands.items, keep) |cnd, k| { if (k) { cands.items[write] = cnd; write += 1; } else { runtime.dropEntry(cnd.entry); } } cands.shrinkRetainingCapacity(write); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- const testing = std.testing; /// Test helper: first text part of a tool result. fn xokText(result: panto.ToolCallResult) []const u8 { switch (result) { .ok => |parts| { for (parts.items) |p| { if (p == .text) return p.text; } return ""; }, .err => return "", } } /// Test helper: free a results slice (parts on `.ok`). fn xfreeResults(results: []panto.ToolCallResult) void { for (results) |r| switch (r) { .ok => |b| b.deinit(testing.allocator), .err => {}, }; } fn writeFile(dir: Io.Dir, sub_path: []const u8, content: []const u8) !void { try dir.writeFile(testing.io, .{ .sub_path = sub_path, .data = content }); } fn makeDir(dir: Io.Dir, sub_path: []const u8) !void { try dir.createDirPath(testing.io, sub_path); } /// Resolve an absolute path for a subdirectory of the tmp dir. fn realDir(tmp: *testing.TmpDir, sub: []const u8, buf: []u8) ![]const u8 { const n = try tmp.dir.realPathFile(testing.io, sub, buf); return buf[0..n]; } fn invokeOne(rt: *LuaRuntime, name: []const u8, input: []const u8) !panto.ToolCallResult { var src = rt.toolSource(); const calls = [_]panto.ToolCall{.{ .tool_name = name, .input = input }}; var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }}; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); return results[0]; } test "loadFromDirs: activates a sugar tool and an entry extension" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try makeDir(tmp.dir, "d"); // Sugar tool form (return a table with handler). try writeFile(tmp.dir, "d/echo.lua", \\return { \\ name = "echo", description = "e", \\ schema = { type = "object", properties = { m = { type = "string" } } }, \\ handler = function(input) return "echo: " .. input.m end, \\} ); // Entry form with deferred activate(). try writeFile(tmp.dir, "d/greet.lua", \\local panto = require("panto") \\return { name = "greet", activate = function() \\ panto.ext.register_tool { \\ name = "greet", description = "g", \\ schema = { type = "object" }, \\ handler = function(input) return "hi" end, \\ } \\end } ); var buf: [std.fs.max_path_bytes]u8 = undefined; const d = try realDir(&tmp, "d", &buf); var rt = try LuaRuntime.create(testing.allocator); defer rt.deinit(); const n = try loadFromDirs(testing.allocator, testing.io, rt, &.{ .{ .path = d, .source = .{ .layer = .project, .origin = .dir } }, }, &.{}, null); try testing.expectEqual(@as(usize, 2), n); const r = try invokeOne(rt, "echo", "{\"m\":\"hi\"}"); defer xfreeResults(@constCast(&[_]panto.ToolCallResult{r})); try testing.expectEqualStrings("echo: hi", xokText(r)); } test "loadFromDirs: higher precedence shadows same name" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try makeDir(tmp.dir, "u"); try makeDir(tmp.dir, "p"); try writeFile(tmp.dir, "u/greet.lua", \\return { name = "greet", description = "u", schema = { type = "object" }, \\ handler = function(input) return "USER" end } ); try writeFile(tmp.dir, "p/greet.lua", \\return { name = "greet", description = "p", schema = { type = "object" }, \\ handler = function(input) return "PROJECT" end } ); var ubuf: [std.fs.max_path_bytes]u8 = undefined; var pbuf: [std.fs.max_path_bytes]u8 = undefined; const u = try realDir(&tmp, "u", &ubuf); const p = try realDir(&tmp, "p", &pbuf); var rt = try LuaRuntime.create(testing.allocator); defer rt.deinit(); const n = try loadFromDirs(testing.allocator, testing.io, rt, &.{ .{ .path = u, .source = .{ .layer = .user, .origin = .dir } }, .{ .path = p, .source = .{ .layer = .project, .origin = .dir } }, }, &.{}, null); try testing.expectEqual(@as(usize, 1), n); const r = try invokeOne(rt, "greet", "{}"); defer xfreeResults(@constCast(&[_]panto.ToolCallResult{r})); try testing.expectEqualStrings("PROJECT", xokText(r)); } test "loadFromDirs: within a layer, dir shadows paths shadows rocks" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try makeDir(tmp.dir, "rock"); try makeDir(tmp.dir, "path"); try makeDir(tmp.dir, "dir"); inline for (.{ .{ "rock", "ROCK" }, .{ "path", "PATH" }, .{ "dir", "DIR" } }) |row| { try writeFile(tmp.dir, row[0] ++ "/w.lua", "return { name = \"w\", description = \"d\", schema = { type = \"object\" }," ++ " handler = function(input) return \"" ++ row[1] ++ "\" end }"); } var b1: [std.fs.max_path_bytes]u8 = undefined; var b2: [std.fs.max_path_bytes]u8 = undefined; var b3: [std.fs.max_path_bytes]u8 = undefined; const rock = try realDir(&tmp, "rock", &b1); const path = try realDir(&tmp, "path", &b2); const dir = try realDir(&tmp, "dir", &b3); var rt = try LuaRuntime.create(testing.allocator); defer rt.deinit(); // All at the same layer; only within-layer origin differs. const n = try loadFromDirs(testing.allocator, testing.io, rt, &.{ .{ .path = rock, .source = .{ .layer = .user, .origin = .rocks } }, .{ .path = path, .source = .{ .layer = .user, .origin = .paths } }, .{ .path = dir, .source = .{ .layer = .user, .origin = .dir } }, }, &.{}, null); try testing.expectEqual(@as(usize, 1), n); const r = try invokeOne(rt, "w", "{}"); defer xfreeResults(@constCast(&[_]panto.ToolCallResult{r})); try testing.expectEqualStrings("DIR", xokText(r)); } test "loadFromDirs: same-precedence duplicate name is an error" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try makeDir(tmp.dir, "a"); try makeDir(tmp.dir, "b"); try writeFile(tmp.dir, "a/x.lua", \\return { name = "dup", description = "a", schema = { type = "object" }, \\ handler = function(input) return "a" end } ); try writeFile(tmp.dir, "b/y.lua", \\return { name = "dup", description = "b", schema = { type = "object" }, \\ handler = function(input) return "b" end } ); var ab: [std.fs.max_path_bytes]u8 = undefined; var bb: [std.fs.max_path_bytes]u8 = undefined; const a = try realDir(&tmp, "a", &ab); const b = try realDir(&tmp, "b", &bb); var rt = try LuaRuntime.create(testing.allocator); defer rt.deinit(); // Same layer AND same origin → same rank → ambiguous. const result = loadFromDirs(testing.allocator, testing.io, rt, &.{ .{ .path = a, .source = .{ .layer = .project, .origin = .dir } }, .{ .path = b, .source = .{ .layer = .project, .origin = .dir } }, }, &.{}, null); try testing.expectError(error.DuplicateExtensionName, result); } test "loadFromDirs: tool-name collision across entries errors" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try makeDir(tmp.dir, "d"); // Two distinct entry names, but both register the same tool name. try writeFile(tmp.dir, "d/a.lua", \\local panto = require("panto") \\return { name = "a", activate = function() \\ panto.ext.register_tool { name = "clash", description = "a", \\ schema = { type = "object" }, handler = function() return "a" end } \\end } ); try writeFile(tmp.dir, "d/b.lua", \\local panto = require("panto") \\return { name = "b", activate = function() \\ panto.ext.register_tool { name = "clash", description = "b", \\ schema = { type = "object" }, handler = function() return "b" end } \\end } ); var buf: [std.fs.max_path_bytes]u8 = undefined; const d = try realDir(&tmp, "d", &buf); var rt = try LuaRuntime.create(testing.allocator); defer rt.deinit(); const result = loadFromDirs(testing.allocator, testing.io, rt, &.{ .{ .path = d, .source = .{ .layer = .project, .origin = .dir } }, }, &.{}, null); try testing.expectError(error.DuplicateTool, result); } test "loadFromDirs: policy denies a name — its activate() never runs" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try makeDir(tmp.dir, "d"); // activate() would crash if it ran; the deny must prevent activation. // eval (the top-level return) is always safe. try writeFile(tmp.dir, "d/danger.lua", \\return { name = "danger", activate = function() \\ error("activate must not run") \\end } ); try writeFile(tmp.dir, "d/ok.lua", \\return { name = "ok", description = "o", schema = { type = "object" }, \\ handler = function(input) return "ok" end } ); var buf: [std.fs.max_path_bytes]u8 = undefined; const d = try realDir(&tmp, "d", &buf); var rt = try LuaRuntime.create(testing.allocator); defer rt.deinit(); var rules = try testing.allocator.alloc(config_file.Rule, 1); rules[0] = .{ .pattern = try testing.allocator.dupe(u8, "danger"), .verdict = .deny, .layer = 0, .spec = @import("glob.zig").specificity("danger") }; const policy: config_file.Policy = .{ .rules = rules }; defer policy.deinit(testing.allocator); const n = try loadFromDirs(testing.allocator, testing.io, rt, &.{ .{ .path = d, .source = .{ .layer = .project, .origin = .dir } }, }, &.{}, &policy); // Only "ok" activates; "danger" is denied (its activate never runs). try testing.expectEqual(@as(usize, 1), n); } test "loadFromDirs: a source returning a list registers multiple entries" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); try makeDir(tmp.dir, "d"); try writeFile(tmp.dir, "d/multi.lua", \\local panto = require("panto") \\local function tool(nm) \\ return { name = nm, activate = function() \\ panto.ext.register_tool { name = nm, description = nm, \\ schema = { type = "object" }, handler = function() return nm end } \\ end } \\end \\return { tool("agent.skills"), tool("agent.rules") } ); var buf: [std.fs.max_path_bytes]u8 = undefined; const d = try realDir(&tmp, "d", &buf); var rt = try LuaRuntime.create(testing.allocator); defer rt.deinit(); // Deny one of the two namespaces; the other still activates. var rules = try testing.allocator.alloc(config_file.Rule, 1); rules[0] = .{ .pattern = try testing.allocator.dupe(u8, "agent.rules"), .verdict = .deny, .layer = 0, .spec = @import("glob.zig").specificity("agent.rules") }; const policy: config_file.Policy = .{ .rules = rules }; defer policy.deinit(testing.allocator); const n = try loadFromDirs(testing.allocator, testing.io, rt, &.{ .{ .path = d, .source = .{ .layer = .project, .origin = .dir } }, }, &.{}, &policy); try testing.expectEqual(@as(usize, 1), n); const r = try invokeOne(rt, "agent.skills", "{}"); defer xfreeResults(@constCast(&[_]panto.ToolCallResult{r})); try testing.expectEqualStrings("agent.skills", xokText(r)); }