e1ccc4573d0f
home apply/check: thread ctx.passthrough into home-manager switch/build argv, home-manager build's shell wrapper takes extra flags via "$@" to avoid injection; bump v3.4.12
- time
- 2026-07-02 00:58 UTC · 3 months ago
- machine
- koh2
- agent
- claude-sonnet-5
- session
- nina passthrough flag audit
- parent
- c93535d54e46
↓ nina-e1ccc457.tar.gzatlas at this save
files changed4 files
VERSION+1 −1src/commands.zig+4 −4src/exec.zig+21 −6src/main.zig+12 −0src/commands.zig+4 −4
@@ -1474,7 +1474,7 @@14741474 14751475 fn homeCheck(ctx: Ctx) !void {14761476 output.homeCheckStart(ctx.machine.name);1477- const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1477+ const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa, ctx.passthrough);14781478 if (code != 0) return error.BuildFailed;14791479 output.homeCheckOk();14801480 }@@ -1559,7 +1559,7 @@15591559 }15601560 if (ctx.dry) {15611561 output.homeApplyDry(ctx.machine.name);1562- const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1562+ const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa, ctx.passthrough);15631563 if (code != 0) return error.BuildFailed;15641564 output.homeApplyDryDone();15651565 return;@@ -1580,7 +1580,7 @@15801580 15811581 output.printSubstep("home-manager switch", .{});15821582 const start = std.Io.Clock.awake.now(ctx.io);1583- const code = try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa);1583+ const code = try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa, ctx.passthrough);15841584 if (code != 0) return error.BuildFailed;15851585 15861586 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());@@ -1611,7 +1611,7 @@16111611 const start = std.Io.Clock.awake.now(ctx.io);16121612 for (refs.items, 0..) |ref, i| {16131613 output.printSubstep("home-manager switch --flake {s}", .{ref});1614- switch (try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref)) {1614+ switch (try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref, ctx.passthrough)) {16151615 .ok => {16161616 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());16171617 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));src/exec.zig+21 −6
@@ -1666,9 +1666,13 @@16661666 return stream(io, machine, gpa, argv.items);16671667 }16681668 1669-pub fn homeManagerSwitch(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {1669+pub fn homeManagerSwitch(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, extra_flags: []const []const u8) !u8 {16701670 output.flush();1671- const r = try capture(gpa, io, machine, &.{ "home-manager", "switch" });1671+ var argv: std.ArrayList([]const u8) = .empty;1672+ defer argv.deinit(gpa);1673+ try argv.appendSlice(gpa, &.{ "home-manager", "switch" });1674+ try argv.appendSlice(gpa, extra_flags);1675+ const r = try capture(gpa, io, machine, argv.items);16721676 defer gpa.free(r.stdout);16731677 defer gpa.free(r.stderr);16741678 errors.setBuildStderr(r.stderr);@@ -1680,9 +1684,13 @@16801684 // genuine build failure.16811685 pub const SwitchOutcome = enum { ok, attr_missing, failed };16821686 1683-pub fn homeManagerSwitchFlake(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, flake_ref: []const u8) !SwitchOutcome {1687+pub fn homeManagerSwitchFlake(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, flake_ref: []const u8, extra_flags: []const []const u8) !SwitchOutcome {16841688 output.flush();1685- const r = try capture(gpa, io, machine, &.{ "home-manager", "switch", "--flake", flake_ref });1689+ var argv: std.ArrayList([]const u8) = .empty;1690+ defer argv.deinit(gpa);1691+ try argv.appendSlice(gpa, &.{ "home-manager", "switch", "--flake", flake_ref });1692+ try argv.appendSlice(gpa, extra_flags);1693+ const r = try capture(gpa, io, machine, argv.items);16861694 defer gpa.free(r.stdout);16871695 defer gpa.free(r.stderr);16881696 if (r.exit_code == 0) return .ok;@@ -1710,9 +1718,16 @@17101718 // check` and `home apply --dry`. `home-manager build` drops a ./result symlink17111719 // in the cwd, so run it in a throwaway temp dir to avoid littering the directory17121720 // nina was invoked from.1713-pub fn homeManagerBuild(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {1721+pub fn homeManagerBuild(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, extra_flags: []const []const u8) !u8 {17141722 output.flush();1715- const r = try capture(gpa, io, machine, &.{ "sh", "-c", "d=$(mktemp -d) && cd \"$d\" && home-manager build" });1723+ // extra_flags are passed as real argv elements after the literal `--`, picked1724+ // up via "$@" — never interpolated into the script text, so an extra flag1725+ // can't smuggle shell metacharacters into the command.1726+ var argv: std.ArrayList([]const u8) = .empty;1727+ defer argv.deinit(gpa);1728+ try argv.appendSlice(gpa, &.{ "sh", "-c", "d=$(mktemp -d) && cd \"$d\" && home-manager build \"$@\"", "--" });1729+ try argv.appendSlice(gpa, extra_flags);1730+ const r = try capture(gpa, io, machine, argv.items);17161731 defer gpa.free(r.stdout);17171732 defer gpa.free(r.stderr);17181733 errors.setBuildStderr(r.stderr);src/main.zig+12 −0
@@ -445,6 +445,18 @@445445 try std.testing.expectEqualStrings("--version", args.passthrough[1]);446446 }447447 448+test "parse args forwards home apply --show-trace" {449+ const gpa = std.testing.allocator;450+ const argv = [_][:0]const u8{ "nina", "home", "apply", "--show-trace" };451+ const args = try parseArgs(gpa, &argv);452+ defer gpa.free(args.rest);453+ defer gpa.free(args.passthrough);454+ try std.testing.expectEqualStrings("home", args.command);455+ try std.testing.expectEqualStrings("apply", args.sub);456+ try std.testing.expectEqual(@as(usize, 1), args.passthrough.len);457+ try std.testing.expectEqualStrings("--show-trace", args.passthrough[0]);458+}459+448460 test "parse subcommand" {449461 const gpa = std.testing.allocator;450462 const argv = [_][:0]const u8{ "nina", "service", "list" };