Add --account N flag and CSV credential resolution to Clojure, Common Lisp, Scheme, PowerShell

Each implementation gains:
- load-accounts-csv function parsing public_key,secret_key rows, skipping # comments and blanks
- Full 5-tier credential resolution: -p/-k flags > --account N > env vars > ~/.unsandbox/accounts.csv > ./accounts.csv
- --account N CLI flag (pre-parsed and stripped before main arg dispatch)
- UNSANDBOX_ACCOUNT env var support for default row selection in csv fallback
This commit is contained in:
russell@unturf.com 2026-03-23 15:11:04 -04:00
parent ce327b35e9
commit 59eb474995
4 changed files with 276 additions and 37 deletions

View file

@ -118,17 +118,51 @@
(or (second (re-find pattern-str json-str))
(second (re-find pattern-num json-str)))))
(def ^:dynamic *account-index* nil)
(defn load-accounts-csv [path index]
(when (.exists (io/file path))
(try
(let [lines (str/split-lines (slurp path))
rows (->> lines
(filter #(and (> (count %) 0)
(not (str/starts-with? % "#"))))
(map #(str/split % #"," 2))
(filter #(>= (count %) 2)))]
(when (< index (count rows))
(let [row (nth rows index)]
[(str/trim (first row)) (str/trim (second row))])))
(catch Exception _ nil))))
(defn get-api-keys []
(let [public-key (System/getenv "UNSANDBOX_PUBLIC_KEY")
secret-key (System/getenv "UNSANDBOX_SECRET_KEY")
api-key (System/getenv "UNSANDBOX_API_KEY")]
api-key (System/getenv "UNSANDBOX_API_KEY")
home (System/getenv "HOME")
account-idx (or *account-index* 0)]
(cond
;; --account N: load row N from accounts.csv, bypasses env vars
(some? *account-index*)
(or (load-accounts-csv (str home "/.unsandbox/accounts.csv") account-idx)
(load-accounts-csv "./accounts.csv" account-idx)
(do
(binding [*out* *err*]
(println (str "Error: account " account-idx " not found in accounts.csv")))
(System/exit 1)))
;; env vars
(and public-key secret-key) [public-key secret-key]
api-key [api-key nil]
:else (do
;; accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT)
:else
(let [row-idx (if-let [acc (System/getenv "UNSANDBOX_ACCOUNT")]
(try (Integer/parseInt acc) (catch Exception _ 0))
0)]
(or (load-accounts-csv (str home "/.unsandbox/accounts.csv") row-idx)
(load-accounts-csv "./accounts.csv" row-idx)
(do
(binding [*out* *err*]
(println "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"))
(System/exit 1)))))
(System/exit 1)))))))
(defn get-api-key []
(first (get-api-keys)))
@ -1216,4 +1250,14 @@
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file service-unfreeze-on-demand key-extend image-action image-id image-source-type image-visibility image-name image-ports mode))))
(parse-args *command-line-args*)
(let [raw-args *command-line-args*
account-val (second (drop-while #(not= % "--account") raw-args))
account-idx (when account-val
(try (Integer/parseInt account-val) (catch Exception _ nil)))
filtered-args (loop [in raw-args out []]
(cond
(empty? in) out
(= (first in) "--account") (recur (drop 2 in) out)
:else (recur (rest in) (conj out (first in)))))]
(binding [*account-index* account-idx]
(parse-args filtered-args)))

View file

@ -343,16 +343,63 @@
(curl-delete api-key (format nil "/services/~a/env" service-id))
(format t "~aVault deleted: ~a~a~%" *green* service-id *reset*))
(defparameter *account-index* nil)
(defun load-accounts-csv (path index)
"Load row INDEX from a CSV file of public_key,secret_key pairs.
Skips blank lines and lines starting with #. Returns (list pk sk) or nil."
(when (probe-file path)
(handler-case
(with-open-file (stream path)
(let ((rows nil))
(loop for line = (read-line stream nil nil)
while line do
(let ((trimmed (string-trim '(#\Space #\Tab #\Return) line)))
(when (and (> (length trimmed) 0)
(not (char= (char trimmed 0) #\#)))
(let ((comma-pos (position #\, trimmed)))
(when comma-pos
(push (list (string-trim '(#\Space #\Tab) (subseq trimmed 0 comma-pos))
(string-trim '(#\Space #\Tab) (subseq trimmed (1+ comma-pos))))
rows))))))
(let ((rows (nreverse rows)))
(when (< index (length rows))
(nth index rows)))))
(error () nil))))
(defun get-api-keys ()
(let ((public-key (uiop:getenv "UNSANDBOX_PUBLIC_KEY"))
(secret-key (uiop:getenv "UNSANDBOX_SECRET_KEY"))
(api-key (uiop:getenv "UNSANDBOX_API_KEY")))
(api-key (uiop:getenv "UNSANDBOX_API_KEY"))
(home (uiop:getenv "HOME")))
(cond
;; --account N: load row N from accounts.csv, bypasses env vars
((not (null *account-index*))
(let ((result (or (load-accounts-csv
(format nil "~a/.unsandbox/accounts.csv" home)
*account-index*)
(load-accounts-csv "./accounts.csv" *account-index*))))
(or result
(progn
(format *error-output* "Error: account ~a not found in accounts.csv~%" *account-index*)
(uiop:quit 1)))))
;; env vars
((and public-key secret-key) (list public-key secret-key))
(api-key (list api-key nil))
(t (progn
(format t "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~%")
(uiop:quit 1))))))
;; accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT env var)
(t
(let* ((acc-env (uiop:getenv "UNSANDBOX_ACCOUNT"))
(row-idx (if acc-env
(handler-case (parse-integer acc-env) (error () 0))
0))
(result (or (load-accounts-csv
(format nil "~a/.unsandbox/accounts.csv" home)
row-idx)
(load-accounts-csv "./accounts.csv" row-idx))))
(or result
(progn
(format *error-output* "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~%")
(uiop:quit 1))))))))
(defun get-api-key ()
(first (get-api-keys)))
@ -912,8 +959,26 @@
do (format t "~a~%" (subseq array-content (1+ start) end))))
(format t "")))))
(defun strip-account-flag (args)
"Remove --account N from args, set *account-index* as side-effect. Returns filtered args."
(let ((result nil)
(i 0)
(vec (coerce args 'vector)))
(loop while (< i (length vec)) do
(cond
((string= (aref vec i) "--account")
(when (< (+ i 1) (length vec))
(setf *account-index*
(handler-case (parse-integer (aref vec (+ i 1))) (error () nil))))
(incf i 2))
(t
(push (aref vec i) result)
(incf i))))
(nreverse result)))
(defun main ()
(let ((args (uiop:command-line-arguments)))
(let* ((raw-args (uiop:command-line-arguments))
(args (strip-account-flag raw-args)))
(if (null args)
(progn
(format t "Usage: un.lisp [options] <source_file>~%")

View file

@ -61,7 +61,43 @@ $EXT_MAP = @{
".raku" = "raku"; ".m" = "objc"; ".awk" = "awk"
}
$script:AccountIndex = $null
function Load-AccountsCSV {
param($Path, $Index)
if (-not (Test-Path $Path)) { return $null }
try {
$rows = @()
Get-Content $Path | Where-Object {
$_.Trim() -ne "" -and -not $_.TrimStart().StartsWith("#")
} | ForEach-Object {
$parts = $_ -split ",", 2
if ($parts.Count -ge 2) {
$rows += ,@($parts[0].Trim(), $parts[1].Trim())
}
}
if ($Index -lt $rows.Count) {
return $rows[$Index]
}
} catch {}
return $null
}
function Get-ApiKeys {
$home = $env:HOME
if (-not $home) { $home = $env:USERPROFILE }
# --account N: load row N from accounts.csv, bypasses env vars
if ($null -ne $script:AccountIndex) {
$result = Load-AccountsCSV -Path "$home/.unsandbox/accounts.csv" -Index $script:AccountIndex
if (-not $result) {
$result = Load-AccountsCSV -Path "./accounts.csv" -Index $script:AccountIndex
}
if ($result) { return $result }
Write-Error "Error: account $($script:AccountIndex) not found in accounts.csv"
exit 1
}
$publicKey = $env:UNSANDBOX_PUBLIC_KEY
$secretKey = $env:UNSANDBOX_SECRET_KEY
@ -71,11 +107,27 @@ function Get-ApiKeys {
$secretKey = ""
}
if (-not $publicKey) {
Write-Error "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set"
exit 1
if ($publicKey -and $secretKey) {
return @($publicKey, $secretKey)
}
return @($publicKey, $secretKey)
if ($publicKey) {
return @($publicKey, "")
}
# accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT env var)
$rowIdx = 0
if ($env:UNSANDBOX_ACCOUNT) {
try { $rowIdx = [int]$env:UNSANDBOX_ACCOUNT } catch {}
}
$result = Load-AccountsCSV -Path "$home/.unsandbox/accounts.csv" -Index $rowIdx
if (-not $result) {
$result = Load-AccountsCSV -Path "./accounts.csv" -Index $rowIdx
}
if ($result) { return $result }
Write-Error "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set"
exit 1
}
function Invoke-Api {
@ -1179,38 +1231,49 @@ Key options:
exit 0
}
if ($args[0] -eq "session") {
Invoke-Session -Args $args[1..($args.Count-1)]
} elseif ($args[0] -eq "service") {
Invoke-Service -Args $args[1..($args.Count-1)]
} elseif ($args[0] -eq "snapshot") {
Invoke-Snapshot -Args $args[1..($args.Count-1)]
} elseif ($args[0] -eq "image") {
Invoke-Image -Args $args[1..($args.Count-1)]
} elseif ($args[0] -eq "languages") {
Invoke-Languages -Args $args[1..($args.Count-1)]
} elseif ($args[0] -eq "key") {
Invoke-Key -Args $args[1..($args.Count-1)]
# Pre-parse --account N from args, strip from effective arg list
$effectiveArgs = @()
for ($i = 0; $i -lt $args.Count; $i++) {
if ($args[$i] -eq "--account" -and ($i + 1) -lt $args.Count) {
try { $script:AccountIndex = [int]$args[$i + 1] } catch {}
$i++
} else {
$effectiveArgs += $args[$i]
}
}
if ($effectiveArgs[0] -eq "session") {
Invoke-Session -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
} elseif ($effectiveArgs[0] -eq "service") {
Invoke-Service -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
} elseif ($effectiveArgs[0] -eq "snapshot") {
Invoke-Snapshot -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
} elseif ($effectiveArgs[0] -eq "image") {
Invoke-Image -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
} elseif ($effectiveArgs[0] -eq "languages") {
Invoke-Languages -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
} elseif ($effectiveArgs[0] -eq "key") {
Invoke-Key -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
} else {
# Parse execute args
$sourceFile = $null
$envVars = @{}
$network = $null
for ($i = 0; $i -lt $args.Count; $i++) {
switch ($args[$i]) {
for ($i = 0; $i -lt $effectiveArgs.Count; $i++) {
switch ($effectiveArgs[$i]) {
"-e" {
$kv = $args[$i+1] -split "=", 2
$kv = $effectiveArgs[$i+1] -split "=", 2
$envVars[$kv[0]] = $kv[1]
$i++
}
"-n" { $network = $args[$i+1]; $i++ }
"-n" { $network = $effectiveArgs[$i+1]; $i++ }
default {
if ($args[$i].StartsWith("-")) {
Write-Error "${RED}Unknown option: $($args[$i])${RESET}"
if ($effectiveArgs[$i].StartsWith("-")) {
Write-Error "${RED}Unknown option: $($effectiveArgs[$i])${RESET}"
exit 1
} else {
$sourceFile = $args[$i]
$sourceFile = $effectiveArgs[$i]
}
}
}

View file

@ -445,16 +445,72 @@
(curl-delete api-key (format #f "/services/~a/env" service-id))
(format #t "~aVault deleted for: ~a~a\n" green service-id reset))
(define *account-index* #f)
(define (load-accounts-csv path index)
"Load row INDEX from a CSV file of public_key,secret_key pairs.
Skips blank lines and lines starting with #. Returns (list pk sk) or #f."
(if (not (access? path R_OK))
#f
(catch #t
(lambda ()
(let ((port (open-input-file path))
(rows '()))
(let loop ()
(let ((line (read-line port)))
(when (not (eof-object? line))
(let ((trimmed (string-trim-both line)))
(when (and (> (string-length trimmed) 0)
(not (char=? (string-ref trimmed 0) #\#)))
(let ((comma-pos (string-index trimmed #\,)))
(when comma-pos
(set! rows (append rows
(list (list
(string-trim-both (substring trimmed 0 comma-pos))
(string-trim-both (substring trimmed (1+ comma-pos)))))))))))
(loop))))
(close-input-port port)
(if (< index (length rows))
(list-ref rows index)
#f)))
(lambda (key . args) #f))))
(define (get-api-keys)
(let ((public-key (getenv "UNSANDBOX_PUBLIC_KEY"))
(secret-key (getenv "UNSANDBOX_SECRET_KEY"))
(api-key (getenv "UNSANDBOX_API_KEY")))
(api-key (getenv "UNSANDBOX_API_KEY"))
(home (getenv "HOME")))
(cond
;; --account N: load row N from accounts.csv, bypasses env vars
((not (eq? *account-index* #f))
(let ((result (or (load-accounts-csv
(string-append home "/.unsandbox/accounts.csv")
*account-index*)
(load-accounts-csv "./accounts.csv" *account-index*))))
(or result
(begin
(format (current-error-port) "Error: account ~a not found in accounts.csv\n" *account-index*)
(exit 1)))))
;; env vars
((and public-key secret-key) (list public-key secret-key))
(api-key (list api-key #f))
(else (begin
(display "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)\n" (current-error-port))
(exit 1))))))
;; accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT env var)
(else
(let* ((acc-env (getenv "UNSANDBOX_ACCOUNT"))
(row-idx (if acc-env
(catch #t
(lambda () (string->number acc-env))
(lambda (key . args) 0))
0))
(row-idx (if (number? row-idx) (inexact->exact row-idx) 0))
(result (or (load-accounts-csv
(string-append home "/.unsandbox/accounts.csv")
row-idx)
(load-accounts-csv "./accounts.csv" row-idx))))
(or result
(begin
(display "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)\n" (current-error-port))
(exit 1)))))))
(define (get-api-key)
(car (get-api-keys)))
@ -1286,4 +1342,15 @@
(else
(execute-cmd (car args))))))
(main (cdr (command-line)))
(define (strip-account-flag args)
"Remove --account N from args list, setting *account-index* as side-effect."
(let loop ((in args) (out '()))
(cond
((null? in) (reverse out))
((and (equal? (car in) "--account") (pair? (cdr in)))
(let ((n (string->number (cadr in))))
(set! *account-index* (if (number? n) (inexact->exact n) #f))
(loop (cddr in) out)))
(else (loop (cdr in) (cons (car in) out))))))
(main (strip-account-flag (cdr (command-line))))