Fix --account N flag and credential priority in R, Raku, Julia, Groovy SDKs

All four implementations had the wrong credential priority order: env vars
were checked before accounts.csv even when an explicit --account N index
was provided.

Correct priority order implemented in all four:
1. Explicit -p/-k flags (function arguments)
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)
5. ./accounts.csv row 0

Changes per file:
- un.r: rewrote get_credentials() with correct priority, added account_index
  param, added --account N to parse_args(), replaced get_api_keys() calls in
  all cmd_* functions with get_credentials(account_index=args$account_index)
- un.raku: rewrote get-credentials() with correct priority, pre-parse
  --account N in MAIN before dispatch, added Int :$account-index param to
  all cmd-* functions and thread account-index through get-credentials calls
- un.jl: added load_accounts_csv() and get_credentials() functions with full
  5-tier priority, added --account to all ArgParse subcommand tables, wired
  account_index through all cmd function get_api_keys calls
- un.groovy: rewrote getCredentials() and getCredentialsStatic() with correct
  priority (added loadAccountsFromCsv/loadCsvAccounts helpers), rewrote
  getApiKeys() to delegate to getCredentials(), added accountIndex field to
  Args class, added --account N to parseArgs(), wired accountIndex through
  all cmdXxx function calls
This commit is contained in:
russell@unturf.com 2026-03-23 15:17:19 -04:00
parent 5373da4108
commit 422985c6db
6 changed files with 5906 additions and 163 deletions

View file

@ -222,42 +222,67 @@ def signRequest(String secretKey, long timestamp, String method, String path, St
* @return Tuple of [publicKey, secretKey]
* @throws AuthenticationError if no credentials found
*/
def getCredentials(String publicKey = null, String secretKey = null, int accountIndex = 0) {
def loadAccountsFromCsv(File path) {
def validAccounts = []
if (!path.exists()) return validAccounts
try {
def lines = path.text.trim().split('\n')
lines.each { line ->
def trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
if (trimmed.contains(',')) {
def parts = trimmed.split(',', 2)
def pk = parts[0].trim()
def sk = parts[1].trim()
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
validAccounts << [pk, sk]
}
}
}
} catch (Exception e) {
// Ignore file read errors
}
return validAccounts
}
def getCredentials(String publicKey = null, String secretKey = null, int accountIndex = -1) {
// Priority 1: Function arguments
if (publicKey && secretKey) {
return [publicKey, secretKey]
}
// Priority 2: Environment variables
// Priority 2: --account N => accounts.csv row N (bypasses env vars)
if (accountIndex >= 0) {
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadAccountsFromCsv(path)
if (accts && accountIndex < accts.size()) {
return accts[accountIndex]
}
}
throw new AuthenticationError("No account at index ${accountIndex} in accounts.csv")
}
// Priority 3: Environment variables
def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
if (envPk && envSk) {
return [envPk, envSk]
}
// Priority 3: Config file
def accountsPath = new File(System.getProperty('user.home'), '.unsandbox/accounts.csv')
if (accountsPath.exists()) {
try {
def lines = accountsPath.text.trim().split('\n')
def validAccounts = []
lines.each { line ->
def trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
if (trimmed.contains(',')) {
def parts = trimmed.split(',', 2)
def pk = parts[0]
def sk = parts[1]
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
validAccounts << [pk, sk]
}
}
}
if (validAccounts && accountIndex < validAccounts.size()) {
return validAccounts[accountIndex]
}
} catch (Exception e) {
// Ignore file read errors
// Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger()
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadAccountsFromCsv(path)
if (accts && defaultIdx < accts.size()) {
return accts[defaultIdx]
}
}
@ -267,21 +292,14 @@ def getCredentials(String publicKey = null, String secretKey = null, int account
)
}
// Legacy compatibility
def getApiKeys(argsKey) {
def publicKey = System.getenv('UNSANDBOX_PUBLIC_KEY')
def secretKey = System.getenv('UNSANDBOX_SECRET_KEY')
if (!publicKey || !secretKey) {
def legacyKey = argsKey ?: System.getenv('UNSANDBOX_API_KEY')
if (!legacyKey) {
System.err.println("${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}")
System.exit(1)
}
return [legacyKey, null]
// Legacy compatibility - now delegates to getCredentials for proper priority
def getApiKeys(argsKey, int accountIndex = -1) {
try {
return getCredentials(argsKey ?: null, null, accountIndex)
} catch (AuthenticationError e) {
System.err.println("${RED}Error: ${e.message}${RESET}")
System.exit(1)
}
return [publicKey, secretKey]
}
// ============================================================================
@ -606,7 +624,7 @@ def execute(String language, String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(
options.publicKey,
options.secretKey,
options.accountIndex ?: 0
options.accountIndex != null ? options.accountIndex : -1
)
def payload = [
@ -656,7 +674,7 @@ def executeAsync(String language, String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(
options.publicKey,
options.secretKey,
options.accountIndex ?: 0
options.accountIndex != null ? options.accountIndex : -1
)
def payload = [
@ -703,7 +721,7 @@ def run(String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(
options.publicKey,
options.secretKey,
options.accountIndex ?: 0
options.accountIndex != null ? options.accountIndex : -1
)
def ttl = options.ttl ?: DEFAULT_TTL
@ -728,7 +746,7 @@ def runAsync(String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(
options.publicKey,
options.secretKey,
options.accountIndex ?: 0
options.accountIndex != null ? options.accountIndex : -1
)
def ttl = options.ttl ?: DEFAULT_TTL
@ -1589,45 +1607,72 @@ class Client {
def creds = getCredentialsStatic(
options.publicKey,
options.secretKey,
options.accountIndex ?: 0
options.accountIndex != null ? options.accountIndex : -1
)
this.publicKey = creds[0]
this.secretKey = creds[1]
}
private static loadCsvAccounts(File path) {
def accounts = []
if (!path.exists()) return accounts
try {
path.text.trim().split('\n').each { line ->
def trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
if (trimmed.contains(',')) {
def parts = trimmed.split(',', 2)
def pk = parts[0].trim()
def sk = parts[1].trim()
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
accounts << [pk, sk]
}
}
}
} catch (Exception e) {
// Ignore
}
return accounts
}
private static getCredentialsStatic(String publicKey, String secretKey, int accountIndex) {
// Priority 1: explicit arguments
if (publicKey && secretKey) {
return [publicKey, secretKey]
}
// Priority 2: --account N => CSV row N (bypasses env vars)
if (accountIndex >= 0) {
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadCsvAccounts(path)
if (accts && accountIndex < accts.size()) {
return accts[accountIndex]
}
}
throw new AuthenticationError("No account at index ${accountIndex} in accounts.csv")
}
// Priority 3: Environment variables
def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
if (envPk && envSk) {
return [envPk, envSk]
}
def accountsPath = new File(System.getProperty('user.home'), '.unsandbox/accounts.csv')
if (accountsPath.exists()) {
try {
def lines = accountsPath.text.trim().split('\n')
def validAccounts = []
lines.each { line ->
def trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
if (trimmed.contains(',')) {
def parts = trimmed.split(',', 2)
def pk = parts[0]
def sk = parts[1]
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
validAccounts << [pk, sk]
}
}
}
if (validAccounts && accountIndex < validAccounts.size()) {
return validAccounts[accountIndex]
}
} catch (Exception e) {
// Ignore
// Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger()
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadCsvAccounts(path)
if (accts && defaultIdx < accts.size()) {
return accts[defaultIdx]
}
}
@ -1738,6 +1783,7 @@ class Args {
String sourceFile = null
String inlineLang = null
String apiKey = null
Integer accountIndex = -1
String network = null
Integer vcpu = 0
List<String> env = []
@ -1839,7 +1885,7 @@ def serviceEnvSet(serviceId, content, publicKey, secretKey) {
}
def cmdServiceEnv(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
switch (args.envAction) {
case 'status':
@ -1875,7 +1921,7 @@ def cmdServiceEnv(args) {
}
def cmdExecute(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
String code
String language
@ -1956,7 +2002,7 @@ def cmdExecute(args) {
}
def cmdSession(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.sessionSnapshot) {
def payload = [:]
@ -2033,7 +2079,7 @@ def openBrowser(url) {
}
def cmdSnapshot(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.snapshotList) {
def output = apiRequest('/snapshots', 'GET', null, publicKey, secretKey)
@ -2073,7 +2119,7 @@ def cmdSnapshot(args) {
}
def cmdImage(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.imageList) {
def output = apiRequest('/images', 'GET', null, publicKey, secretKey)
@ -2153,7 +2199,7 @@ def cmdImage(args) {
}
def cmdLanguages(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
def result = languages([publicKey: publicKey, secretKey: secretKey, forceRefresh: true])
def langList = result.languages ?: []
@ -2168,7 +2214,7 @@ def cmdLanguages(args) {
}
def cmdKey(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate",
'-H', 'Content-Type: application/json']
@ -2240,7 +2286,7 @@ def cmdKey(args) {
}
def cmdService(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey)
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.serviceSnapshot) {
def payload = [:]
@ -2457,6 +2503,9 @@ def parseArgs(argv) {
case '--public-key':
args.apiKey = argv[++i] // For compatibility
break
case '--account':
args.accountIndex = argv[++i].toInteger()
break
case '-n':
case '--network':
args.network = argv[++i]