summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/extension_loader.zig165
-rw-r--r--src/luarocks_runtime.zig37
-rw-r--r--src/main.zig10
-rw-r--r--src/panto_home.zig11
4 files changed, 210 insertions, 13 deletions
diff --git a/src/extension_loader.zig b/src/extension_loader.zig
index 8a871c9..2fd86ae 100644
--- a/src/extension_loader.zig
+++ b/src/extension_loader.zig
@@ -1,17 +1,26 @@
//! Extension and tool discovery: walk well-known directories, locate Lua
//! files, and load each into a long-lived `LuaRuntime`.
//!
-//! Two parallel namespaces are scanned, each at user and project scope:
+//! Two parallel namespaces are scanned, each at three scopes — system,
+//! user, and project. Project shadows user shadows system.
//!
//! Extensions (full-featured; call `panto.register_tool` from a script
//! that may register many tools):
-//! 1. `${XDG_CONFIG_HOME:-$HOME/.config}/panto/extensions/` ("user")
-//! 2. `./.panto/extensions/` ("project")
+//! 1. `$PANTO_HOME/agent/extensions/` ("system")
+//! 2. `${XDG_CONFIG_HOME:-$HOME/.config}/panto/extensions/` ("user")
+//! 3. `./.panto/extensions/` ("project")
//!
//! Tools (ergonomic single-tool form; the script returns one table
//! shaped like the argument to `panto.register_tool`):
-//! 1. `${XDG_CONFIG_HOME:-$HOME/.config}/panto/tools/` ("user")
-//! 2. `./.panto/tools/` ("project")
+//! 1. `$PANTO_HOME/agent/tools/` ("system")
+//! 2. `${XDG_CONFIG_HOME:-$HOME/.config}/panto/tools/` ("user")
+//! 3. `./.panto/tools/` ("project")
+//!
+//! The `system` layer is populated at bootstrap from files embedded
+//! into the panto binary (see `build/gen_agent_embed.zig` and
+//! `luarocks_runtime.stageAgentTree`). It is panto's "batteries" tier:
+//! ships in the binary, but every entry is individually shadowable
+//! from the user or project layer.
//!
//! Layout per directory (identical for extensions and tools):
//! - `<name>.lua` -- single-file entry; the logical name is the
@@ -24,10 +33,11 @@
//! Conflict rules:
//! - Within one directory, two entries with the same logical name
//! are an error.
-//! - Project shadows user *within the same kind* (extension or tool).
-//! Extensions and tools live in distinct namespaces for shadowing;
-//! the registered *tool names* still share one global namespace
-//! across both, and collisions there are still an error.
+//! - Project shadows user shadows system *within the same kind*
+//! (extension or tool). Extensions and tools live in distinct
+//! namespaces for shadowing; the registered *tool names* still
+//! share one global namespace across both, and collisions there
+//! are still an error.
//!
//! Symlinks: followed normally. Dotfiles: skipped.
@@ -60,11 +70,15 @@ const Found = struct {
};
pub const Source = enum {
+ /// Staged at bootstrap from files embedded in the panto binary.
+ /// Lowest priority — shadowed by user and project.
+ system,
user,
project,
pub fn label(self: Source) []const u8 {
return switch (self) {
+ .system => "system",
.user => "user",
.project => "project",
};
@@ -87,14 +101,31 @@ pub const Kind = enum {
/// paths into `runtime`. Returns the number of registered tools added
/// to the runtime by this call.
///
+/// `system_agent_dir`, when non-null, is the path under which embedded
+/// system tools/extensions have been staged — typically
+/// `$PANTO_HOME/agent/`. Pass `null` to skip the system layer entirely
+/// (mostly useful for tests).
+///
/// `environ_map` is consulted for `HOME` and `XDG_CONFIG_HOME`. The
/// project directories are always `cwd()/.panto/{extensions,tools}`.
pub fn discoverAndLoad(
allocator: Allocator,
io: Io,
environ_map: *const std.process.Environ.Map,
+ system_agent_dir: ?[]const u8,
runtime: *LuaRuntime,
) !usize {
+ const sys_ext = if (system_agent_dir) |d|
+ try std.fs.path.join(allocator, &.{ d, kindSubdir(.extension) })
+ else
+ null;
+ defer if (sys_ext) |d| allocator.free(d);
+ const sys_tool = if (system_agent_dir) |d|
+ try std.fs.path.join(allocator, &.{ d, kindSubdir(.tool) })
+ else
+ null;
+ defer if (sys_tool) |d| allocator.free(d);
+
const user_ext = try userKindDir(allocator, environ_map, .extension);
defer if (user_ext) |d| allocator.free(d);
const user_tool = try userKindDir(allocator, environ_map, .tool);
@@ -110,8 +141,10 @@ pub fn discoverAndLoad(
io,
runtime,
.{
+ .system_extensions = sys_ext,
.user_extensions = user_ext,
.project_extensions = project_ext,
+ .system_tools = sys_tool,
.user_tools = user_tool,
.project_tools = project_tool,
},
@@ -120,9 +153,15 @@ pub fn discoverAndLoad(
/// Set of search paths consumed by `loadFromDirs`. Any field may be
/// null; missing directories on disk are silently skipped.
+///
+/// Scan order is system → user → project. `applyShadowing` keeps the
+/// *last* occurrence of each (kind, name), so project entries win,
+/// then user, then system.
pub const DirSet = struct {
+ system_extensions: ?[]const u8 = null,
user_extensions: ?[]const u8 = null,
project_extensions: ?[]const u8 = null,
+ system_tools: ?[]const u8 = null,
user_tools: ?[]const u8 = null,
project_tools: ?[]const u8 = null,
};
@@ -141,8 +180,10 @@ pub fn loadFromDirs(
found.deinit();
}
+ if (dirs.system_extensions) |d| try scanDir(allocator, io, d, .system, .extension, &found);
if (dirs.user_extensions) |d| try scanDir(allocator, io, d, .user, .extension, &found);
if (dirs.project_extensions) |d| try scanDir(allocator, io, d, .project, .extension, &found);
+ if (dirs.system_tools) |d| try scanDir(allocator, io, d, .system, .tool, &found);
if (dirs.user_tools) |d| try scanDir(allocator, io, d, .user, .tool, &found);
if (dirs.project_tools) |d| try scanDir(allocator, io, d, .project, .tool, &found);
@@ -708,6 +749,112 @@ test "loadFromDirs: project tool shadows user tool of the same name" {
try testing.expectEqualStrings("PROJECT", results[0].ok);
}
+test "loadFromDirs: project tool shadows user shadows system" {
+ var tmp = testing.tmpDir(.{ .iterate = true });
+ defer tmp.cleanup();
+
+ try makeDir(tmp.dir, "system_tools");
+ try makeDir(tmp.dir, "user_tools");
+ try makeDir(tmp.dir, "project_tools");
+
+ try writeFile(tmp.dir, "system_tools/greet.lua",
+ \\return {
+ \\ name = "greet", description = "system version",
+ \\ schema = { type = "object" },
+ \\ handler = function(input) return "SYSTEM" end,
+ \\}
+ );
+ try writeFile(tmp.dir, "user_tools/greet.lua",
+ \\return {
+ \\ name = "greet", description = "user version",
+ \\ schema = { type = "object" },
+ \\ handler = function(input) return "USER" end,
+ \\}
+ );
+ try writeFile(tmp.dir, "project_tools/greet.lua",
+ \\return {
+ \\ name = "greet", description = "project version",
+ \\ schema = { type = "object" },
+ \\ handler = function(input) return "PROJECT" end,
+ \\}
+ );
+
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const sys_len = try tmp.dir.realPathFile(testing.io, "system_tools", &path_buf);
+ const sys_path = try testing.allocator.dupe(u8, path_buf[0..sys_len]);
+ defer testing.allocator.free(sys_path);
+ const user_len = try tmp.dir.realPathFile(testing.io, "user_tools", &path_buf);
+ const user_path = try testing.allocator.dupe(u8, path_buf[0..user_len]);
+ defer testing.allocator.free(user_path);
+ const proj_len = try tmp.dir.realPathFile(testing.io, "project_tools", &path_buf);
+ const proj_path = try testing.allocator.dupe(u8, path_buf[0..proj_len]);
+ defer testing.allocator.free(proj_path);
+
+ var rt = try LuaRuntime.create(testing.allocator);
+ defer rt.deinit();
+
+ const n_tools = try loadFromDirs(testing.allocator, testing.io, rt, .{
+ .system_tools = sys_path,
+ .user_tools = user_path,
+ .project_tools = proj_path,
+ });
+ try testing.expectEqual(@as(usize, 1), n_tools);
+
+ var src = rt.toolSource();
+ const calls = [_]panto.ToolCall{.{ .tool_name = "greet", .input = "{}" }};
+ var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
+ try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
+ defer testing.allocator.free(results[0].ok);
+ try testing.expectEqualStrings("PROJECT", results[0].ok);
+}
+
+test "loadFromDirs: user tool shadows system tool when no project entry" {
+ var tmp = testing.tmpDir(.{ .iterate = true });
+ defer tmp.cleanup();
+
+ try makeDir(tmp.dir, "system_tools");
+ try makeDir(tmp.dir, "user_tools");
+
+ try writeFile(tmp.dir, "system_tools/greet.lua",
+ \\return {
+ \\ name = "greet", description = "system",
+ \\ schema = { type = "object" },
+ \\ handler = function(input) return "SYSTEM" end,
+ \\}
+ );
+ try writeFile(tmp.dir, "user_tools/greet.lua",
+ \\return {
+ \\ name = "greet", description = "user",
+ \\ schema = { type = "object" },
+ \\ handler = function(input) return "USER" end,
+ \\}
+ );
+
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const sys_len = try tmp.dir.realPathFile(testing.io, "system_tools", &path_buf);
+ const sys_path = try testing.allocator.dupe(u8, path_buf[0..sys_len]);
+ defer testing.allocator.free(sys_path);
+ const user_len = try tmp.dir.realPathFile(testing.io, "user_tools", &path_buf);
+ const user_path = try testing.allocator.dupe(u8, path_buf[0..user_len]);
+ defer testing.allocator.free(user_path);
+
+ var rt = try LuaRuntime.create(testing.allocator);
+ defer rt.deinit();
+
+ const n_tools = try loadFromDirs(testing.allocator, testing.io, rt, .{
+ .system_tools = sys_path,
+ .user_tools = user_path,
+ });
+ try testing.expectEqual(@as(usize, 1), n_tools);
+
+ var src = rt.toolSource();
+ const calls = [_]panto.ToolCall{.{ .tool_name = "greet", .input = "{}" }};
+ var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
+ try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
+ defer testing.allocator.free(results[0].ok);
+ try testing.expectEqualStrings("USER", results[0].ok);
+}
+
test "loadFromDirs: extension and tool share a *file* name independently" {
// The shadow-key is (kind, name) so an extension named `foo` and a
// tool named `foo` coexist — as long as their *registered* tool names
diff --git a/src/luarocks_runtime.zig b/src/luarocks_runtime.zig
index b67a8ad..0114b64 100644
--- a/src/luarocks_runtime.zig
+++ b/src/luarocks_runtime.zig
@@ -37,6 +37,7 @@ const manifest = @import("manifest.zig");
const panto_home = @import("panto_home.zig");
const embedded_luarocks = @import("embedded_luarocks");
const embedded_lua_headers = @import("embedded_lua_headers");
+const embedded_agent = @import("embedded_agent");
const lua_bridge = @import("lua_bridge.zig");
const c = lua_bridge.c;
@@ -135,6 +136,7 @@ pub fn bootstrap(
try panto_home.ensureDirsExist(layout, io);
try stageLuaHeaders(allocator, io, layout);
+ try stageAgentTree(allocator, io, layout);
const lua_wrapper_path = try writeLuaWrapper(allocator, io, layout, panto_executable_path);
defer allocator.free(lua_wrapper_path);
try writeLuarocksConfig(allocator, io, layout, lua_wrapper_path);
@@ -199,6 +201,41 @@ fn stageLuaHeaders(
}
}
+// ---------------------------------------------------------------------------
+// Stage the embedded `agent/` tree
+// ---------------------------------------------------------------------------
+
+/// Materialize the binary-embedded `agent/` tree under
+/// `$PANTO_HOME/agent/`. Each entry's `path` is relative to the agent
+/// root and may include subdirectories (`tools/read.lua` etc.); the
+/// parent directory is created lazily as we encounter files inside it.
+///
+/// Uses the same `writeIfDifferent` discipline as `stageLuaHeaders`:
+/// unchanged files keep their on-disk mtime so downstream consumers
+/// (the extension loader's stat-cached searches, future-rocks builds
+/// against staged headers) don't see spurious invalidations.
+fn stageAgentTree(
+ allocator: Allocator,
+ io: Io,
+ layout: panto_home.Layout,
+) !void {
+ var root = try Io.Dir.cwd().openDir(io, layout.agent_dir, .{});
+ defer root.close(io);
+
+ for (embedded_agent.files) |entry| {
+ // Ensure the file's parent directory exists. `path` is
+ // forward-slash separated (the generator normalizes that),
+ // so we can split off the dirname directly.
+ if (std.fs.path.dirname(entry.path)) |parent| {
+ root.createDirPath(io, parent) catch |err| switch (err) {
+ error.PathAlreadyExists => {},
+ else => return err,
+ };
+ }
+ try writeIfDifferent(allocator, io, root, entry.path, entry.contents);
+ }
+}
+
/// Write `contents` into `dir/name` only if the existing file differs.
/// Creates the file if absent.
fn writeIfDifferent(
diff --git a/src/main.zig b/src/main.zig
index 3bc24e3..f95812d 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -399,14 +399,16 @@ pub fn main(init: std.process.Init) !void {
// chance to register tools that might want to yield.
try rt.installScheduler();
- // Discover Lua extensions from $XDG_CONFIG_HOME/panto/extensions (or
- // $HOME/.config/panto/extensions) and ./.panto/extensions. Project
- // entries shadow user entries with the same name; tool-name collisions
- // between extensions abort startup.
+ // Discover Lua extensions across three layers — system
+ // ($PANTO_HOME/agent), user ($XDG_CONFIG_HOME/panto or
+ // $HOME/.config/panto), and project (./.panto). Project shadows
+ // user shadows system; tool-name collisions across surviving
+ // entries abort startup.
const n_ext_tools = extension_loader.discoverAndLoad(
alloc,
io,
init.environ_map,
+ luarocks_rt.layout.agent_dir,
rt,
) catch |err| {
std.log.err("extension discovery failed: {t}", .{err});
diff --git a/src/panto_home.zig b/src/panto_home.zig
index b7c1799..353e54b 100644
--- a/src/panto_home.zig
+++ b/src/panto_home.zig
@@ -30,6 +30,11 @@ pub const Layout = struct {
allocator: Allocator,
/// `$PANTO_HOME` itself.
home: []u8,
+ /// `$PANTO_HOME/agent/` — the "system" extension/tool tree,
+ /// populated at bootstrap from files embedded into the panto
+ /// binary. Searched after user/project layers for tools and
+ /// extensions; project shadows user shadows system.
+ agent_dir: []u8,
/// `$PANTO_HOME/rocks/lua-<lua_version>/` — the versioned tree.
tree: []u8,
/// `<tree>/include/` — where Lua headers are staged.
@@ -52,6 +57,7 @@ pub const Layout = struct {
pub fn deinit(self: Layout) void {
const a = self.allocator;
a.free(self.home);
+ a.free(self.agent_dir);
a.free(self.tree);
a.free(self.include_dir);
a.free(self.share_lua_dir);
@@ -76,6 +82,9 @@ pub fn resolve(
const home = try resolveHome(allocator, environ_map);
errdefer allocator.free(home);
+ const agent_dir = try std.fs.path.join(allocator, &.{ home, "agent" });
+ errdefer allocator.free(agent_dir);
+
// `<home>/rocks/lua-<lua_version>`
const tree_subdir = try std.fmt.allocPrint(
allocator,
@@ -111,6 +120,7 @@ pub fn resolve(
return .{
.allocator = allocator,
.home = home,
+ .agent_dir = agent_dir,
.tree = tree,
.include_dir = include_dir,
.share_lua_dir = share_lua_dir,
@@ -143,6 +153,7 @@ fn resolveHome(
/// — existing directories are left alone.
pub fn ensureDirsExist(layout: Layout, io: Io) !void {
try makePathRecursive(io, layout.home);
+ try makePathRecursive(io, layout.agent_dir);
try makePathRecursive(io, layout.tree);
try makePathRecursive(io, layout.include_dir);
try makePathRecursive(io, layout.share_lua_dir);