summaryrefslogtreecommitdiff
path: root/src/subcommand.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-01 15:36:20 -0600
committert <t@tjp.lol>2026-07-01 15:37:04 -0600
commit800b2c4b6115845f6bf15d90484b3e80e81a606f (patch)
tree914981ad5c49e8941280d015923b7c3143463d64 /src/subcommand.zig
parentef7c37c3423ceb896f20676525307f15d6e927b4 (diff)
Load extensions via unified policy, entry/activate, rocks and paths
Replace the split [tools]/[extensions] config sections and the two-stage load gate with a single model: - [extensions] is now one policy resolved across all layers. Rules from every layer are kept (not clobbered) and resolved by last-match-wins after sorting by (layer, glob specificity, deny-last), so a base layer can carve one name out of an otherwise-denied group and a higher layer's broad rule still wins. Default is allow; whitelist via deny=["**"]. - Registration is deferred: a source returns an entry {name, activate} (or a list, or the sugar tool table), is always eval'd side-effect-free, and only permitted names get activate()d. Identity is the declared name, not the filename. Collapses the old pre/post-load two-stage gate. - The loader runs two passes (eval -> shadow -> filter -> activate) with precedence project>user>base and, within a layer, rocks<paths<dir. - extensions.paths adds extra scan dirs; extensions.rocks loads luarocks packages as extension sources (require-as-entries). Startup installs only missing rocks (no per-launch network hit); panto update force- (re)installs every configured rock. deny is a feature toggle, not a security boundary: rocks is where registry code enters, so the pin list is the trust boundary. Rock integrity (first-party GPG signing + --verify) is designed but not yet wired; until then rocks pins which version, not which bytes. Breaking: [tools] is removed; extensions must return entries. Built-in tools and the wc example keep working via the sugar tool form.
Diffstat (limited to 'src/subcommand.zig')
-rw-r--r--src/subcommand.zig70
1 files changed, 67 insertions, 3 deletions
diff --git a/src/subcommand.zig b/src/subcommand.zig
index 0c04ce8..1372d72 100644
--- a/src/subcommand.zig
+++ b/src/subcommand.zig
@@ -77,6 +77,10 @@ pub fn dispatch(
try runBootstrapSubcommand(allocator, io, environ_map, panto_executable_path, .{ .force = force });
return .done;
}
+ if (std.mem.eql(u8, sub, "update")) {
+ try runUpdateSubcommand(allocator, io, environ_map, panto_executable_path);
+ return .done;
+ }
if (std.mem.eql(u8, sub, "sessions")) {
try runSessionsSubcommand(allocator, io, environ_map);
return .done;
@@ -116,6 +120,7 @@ fn printHelp(io: Io) !void {
\\ Forget a stored OAuth token.
\\ panto bootstrap [--force]
\\ Run the luarocks bootstrap and exit.
+ \\ panto update Install/update the rocks in extensions.rocks.
\\ panto lua [args...] Drop into the embedded Lua interpreter.
\\ panto help Show this message.
\\
@@ -124,9 +129,10 @@ fn printHelp(io: Io) !void {
\\ $XDG_CONFIG_HOME/panto/config.toml (user)
\\ ./.panto/config.toml (project)
\\ Define providers under [providers.<name>], pick a default with
- \\ [defaults] model = "<provider>:<alias>", and gate tools/extensions
- \\ with [tools]/[extensions] allow/deny globs. Model aliases (wire name,
- \\ reasoning, max_tokens, pricing) live in models.toml.
+ \\ [defaults] model = "<provider>:<alias>", and gate extensions with
+ \\ [extensions] allow/deny globs (plus [extensions] paths/rocks to add
+ \\ sources). Model aliases (wire name, reasoning, max_tokens, pricing)
+ \\ live in models.toml.
\\
\\Environment:
\\ OPENAI_API_KEY, ANTHROPIC_API_KEY Consumed by the default providers.
@@ -268,6 +274,64 @@ fn runBootstrapSubcommand(
}
// ---------------------------------------------------------------------------
+// `panto update`
+// ---------------------------------------------------------------------------
+
+/// `panto update` — (re)install every rock listed in `extensions.rocks`,
+/// unconditionally. This is the one place we intentionally hit luarocks (and
+/// the network): startup only installs a rock that is missing, so changing a
+/// pin within an already-satisfied range, or forcing a re-resolve, is done
+/// here. Analogous to `bootstrap`, but for user rocks rather than batteries.
+fn runUpdateSubcommand(
+ allocator: Allocator,
+ io: Io,
+ environ_map: *const std.process.Environ.Map,
+ panto_executable_path: []const u8,
+) !void {
+ var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const cwd_n = try std.process.currentPath(io, &cwd_buf);
+ const cwd = cwd_buf[0..cwd_n];
+
+ var cfg = config_file.load(allocator, io, environ_map, cwd) catch |err| {
+ std.log.err("panto update: failed to load config ({t})", .{err});
+ return err;
+ };
+ defer cfg.deinit();
+
+ var out_buf: [1024]u8 = undefined;
+ var out_file = std.Io.File.stdout().writer(io, &out_buf);
+ const out = &out_file.interface;
+
+ if (cfg.ext_rocks.len == 0) {
+ try out.writeAll("panto update: no extensions.rocks configured\n");
+ try out_file.flush();
+ return;
+ }
+
+ const L = c.luaL_newstate() orelse return error.LuaInitFailed;
+ defer c.lua_close(L);
+ c.luaL_openlibs(L);
+ const rt = try luarocks_runtime.bootstrap(allocator, io, environ_map, L, panto_executable_path);
+ defer rt.deinit();
+
+ var installed: usize = 0;
+ var failed: usize = 0;
+ for (cfg.ext_rocks) |spec| {
+ std.log.info("panto update: installing '{s}'", .{spec.value});
+ luarocks_runtime.installRock(rt, allocator, spec.value) catch |err| {
+ std.log.err("panto update: failed to install '{s}': {t}", .{ spec.value, err });
+ failed += 1;
+ continue;
+ };
+ installed += 1;
+ }
+
+ try out.print("panto update: {d} rock(s) installed, {d} failed\n", .{ installed, failed });
+ try out_file.flush();
+ if (failed > 0) return error.LuarocksInstallFailed;
+}
+
+// ---------------------------------------------------------------------------
// `panto sessions`
// ---------------------------------------------------------------------------