Add --account N flag and fix credential resolution priority in un.m, un.f90, un.zig, un.nim

Implements correct 5-tier credential priority in all four implementations:
  1. Explicit -p/-k flags
  2. --account N -> accounts.csv row N (bypasses env vars)
  3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
  4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
  5. ./accounts.csv row 0

un.m (Objective-C): adds UNLoadCredentialsFromCSV helper, updates UNGetCredentials
  to use g_accountIndex global, parses --account N in main() pre-scan.

un.f90 (Fortran): adds load_csv_row subroutine, updates get_credentials with
  optional account_index parameter, parses --account N in main program pre-scan,
  passes account_index via host association to all handle_* subroutines.

un.zig (Zig): adds loadCsvRow and resolveCredentials functions, parses --account N
  in main() pre-scan, updates execute-mode arg loop to skip known flags.

un.nim (Nim): adds loadCredentialsFromCsv and resolveCredentials procs, parses
  --account N in main() pre-scan, updates execute-mode loop to skip --account.
This commit is contained in:
russell@unturf.com 2026-03-23 15:15:19 -04:00
parent 13f15c8abc
commit 5373da4108
4 changed files with 375 additions and 103 deletions

View file

@ -806,6 +806,116 @@ fn buildInputFilesJson(allocator: std.mem.Allocator, files: std.ArrayList([]cons
return list.toOwnedSlice();
}
/// Load credentials from a CSV file at the given 0-based row index,
/// skipping blank lines and lines starting with '#'.
/// Returns allocated pk and sk slices, or null if not found.
fn loadCsvRow(allocator: std.mem.Allocator, csv_path: []const u8, row_index: usize) !?struct { pk: []const u8, sk: []const u8 } {
const file = fs.cwd().openFile(csv_path, .{}) catch return null;
defer file.close();
var buf_reader = std.io.bufferedReader(file.reader());
var reader = buf_reader.reader();
var line_buf: [4096]u8 = undefined;
var data_index: usize = 0;
while (true) {
const line = reader.readUntilDelimiterOrEof(&line_buf, '\n') catch break orelse break;
const trimmed = std.mem.trim(u8, line, " \t\r\n");
if (trimmed.len == 0 or trimmed[0] == '#') continue;
if (data_index == row_index) {
if (std.mem.indexOfScalar(u8, trimmed, ',')) |comma_pos| {
const pk = std.mem.trim(u8, trimmed[0..comma_pos], " \t");
const sk = std.mem.trim(u8, trimmed[comma_pos + 1 ..], " \t");
if (pk.len > 0 and sk.len > 0) {
return .{ .pk = try allocator.dupe(u8, pk), .sk = try allocator.dupe(u8, sk) };
}
}
return null;
}
data_index += 1;
}
return null;
}
/// Resolve credentials using 5-tier priority:
/// 1. arg_pk / arg_sk (explicit -p/-k flags) if both non-empty
/// 2. account_index >= 0 -> accounts.csv row N (bypasses env vars)
/// 3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
/// 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
/// 5. ./accounts.csv row 0
/// Returns allocated pk and sk slices.
fn resolveCredentials(allocator: std.mem.Allocator, arg_pk: []const u8, arg_sk: []const u8, account_index: i64) !struct { pk: []u8, sk: []u8 } {
// Tier 1: explicit key flags
if (arg_pk.len > 0 and arg_sk.len > 0) {
return .{ .pk = try allocator.dupe(u8, arg_pk), .sk = try allocator.dupe(u8, arg_sk) };
}
// Tier 2: --account N bypasses env vars
if (account_index >= 0) {
const acct_idx: usize = @intCast(account_index);
// try ~/.unsandbox/accounts.csv
const home_opt = process.getEnvVarOwned(allocator, "HOME") catch null;
if (home_opt) |home| {
defer allocator.free(home);
const home_csv = try std.fmt.allocPrint(allocator, "{s}/.unsandbox/accounts.csv", .{home});
defer allocator.free(home_csv);
if (try loadCsvRow(allocator, home_csv, acct_idx)) |creds| {
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
}
// try ./accounts.csv
if (try loadCsvRow(allocator, "accounts.csv", acct_idx)) |creds| {
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
const stderr = std.io.getStdErr().writer();
try stderr.print("{s}Error: No credentials found for account index {d} in accounts.csv{s}\n", .{ RED, account_index, RESET });
std.process.exit(1);
}
// Tier 3: env vars
const env_pk = process.getEnvVarOwned(allocator, "UNSANDBOX_PUBLIC_KEY") catch blk: {
break :blk process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch try allocator.dupe(u8, "");
};
const env_sk = process.getEnvVarOwned(allocator, "UNSANDBOX_SECRET_KEY") catch try allocator.dupe(u8, "");
if (env_pk.len > 0 and env_sk.len > 0) {
return .{ .pk = env_pk, .sk = env_sk };
}
// Tier 4: ~/.unsandbox/accounts.csv (default row)
const default_index_str = process.getEnvVarOwned(allocator, "UNSANDBOX_ACCOUNT") catch try allocator.dupe(u8, "0");
defer allocator.free(default_index_str);
const default_index = std.fmt.parseInt(usize, std.mem.trim(u8, default_index_str, " \t"), 10) catch 0;
const home_opt = process.getEnvVarOwned(allocator, "HOME") catch null;
if (home_opt) |home| {
defer allocator.free(home);
const home_csv = try std.fmt.allocPrint(allocator, "{s}/.unsandbox/accounts.csv", .{home});
defer allocator.free(home_csv);
if (try loadCsvRow(allocator, home_csv, default_index)) |creds| {
allocator.free(env_pk);
allocator.free(env_sk);
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
}
// Tier 5: ./accounts.csv
if (try loadCsvRow(allocator, "accounts.csv", default_index)) |creds| {
allocator.free(env_pk);
allocator.free(env_sk);
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
if (env_pk.len > 0) {
return .{ .pk = env_pk, .sk = env_sk };
}
allocator.free(env_pk);
allocator.free(env_sk);
const stderr = std.io.getStdErr().writer();
try stderr.print("{s}Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY{s}\n", .{ RED, RESET });
std.process.exit(1);
}
pub fn main() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
@ -837,18 +947,37 @@ pub fn main() !u8 {
std.debug.print(" image --visibility ID MODE Set visibility (private/unlisted/public)\n", .{});
std.debug.print(" image --spawn ID --name NAME Spawn service from image\n", .{});
std.debug.print(" image --clone ID --name NAME Clone an image\n", .{});
std.debug.print("\nCredential options (global):\n", .{});
std.debug.print(" -p PUBLIC_KEY Explicit public key\n", .{});
std.debug.print(" -k SECRET_KEY Explicit secret key\n", .{});
std.debug.print(" --account N Use row N from accounts.csv (bypasses env vars)\n", .{});
return 1;
}
var public_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_PUBLIC_KEY") catch blk: {
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
break :blk std.process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch try allocator.dupe(u8, "");
};
defer allocator.free(public_key);
// Pre-scan for --account N, -p, and -k before subcommand dispatch
var account_index: i64 = -1;
var arg_pk: []const u8 = "";
var arg_sk: []const u8 = "";
{
var scan_i: usize = 1;
while (scan_i < args.len) : (scan_i += 1) {
if (mem.eql(u8, args[scan_i], "--account") and scan_i + 1 < args.len) {
scan_i += 1;
account_index = std.fmt.parseInt(i64, args[scan_i], 10) catch -1;
} else if (mem.eql(u8, args[scan_i], "-p") and scan_i + 1 < args.len) {
scan_i += 1;
arg_pk = args[scan_i];
} else if (mem.eql(u8, args[scan_i], "-k") and scan_i + 1 < args.len) {
scan_i += 1;
arg_sk = args[scan_i];
}
}
}
const secret_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_SECRET_KEY") catch blk: {
break :blk try allocator.dupe(u8, "");
};
const creds = try resolveCredentials(allocator, arg_pk, arg_sk, account_index);
var public_key = creds.pk;
defer allocator.free(public_key);
const secret_key = creds.sk;
defer allocator.free(secret_key);
// Handle session command
@ -1727,16 +1856,27 @@ pub fn main() !u8 {
return 0;
}
// Execute mode - find source file
// Execute mode - find source file (skip known flags and their values)
var source_file: ?[]const u8 = null;
for (args[1..]) |arg| {
if (mem.startsWith(u8, arg, "-")) {
const stderr = std.io.getStdErr().writer();
stderr.print("{s}Unknown option: {s}{s}\n", .{ RED, arg, RESET }) catch {};
std.os.exit(1);
} else {
source_file = arg;
break;
{
var exec_i: usize = 1;
while (exec_i < args.len) : (exec_i += 1) {
const arg = args[exec_i];
if (mem.eql(u8, arg, "--account") or mem.eql(u8, arg, "-p") or
mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "-e") or
mem.eql(u8, arg, "-n") or mem.eql(u8, arg, "-v"))
{
exec_i += 1; // skip value
} else if (mem.eql(u8, arg, "-a")) {
// no value
} else if (mem.startsWith(u8, arg, "-")) {
const stderr = std.io.getStdErr().writer();
stderr.print("{s}Unknown option: {s}{s}\n", .{ RED, arg, RESET }) catch {};
std.os.exit(1);
} else {
source_file = arg;
break;
}
}
}