From 800b2c4b6115845f6bf15d90484b3e80e81a606f Mon Sep 17 00:00:00 2001 From: t Date: Wed, 1 Jul 2026 15:36:20 -0600 Subject: 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], pick a default with - \\ [defaults] model = ":", and gate tools/extensions - \\ with [tools]/[extensions] allow/deny globs. Model aliases (wire name, - \\ reasoning, max_tokens, pricing) live in models.toml. + \\ [defaults] model = ":", 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. @@ -267,6 +273,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` // --------------------------------------------------------------------------- -- cgit v1.3