Add --account N flag and full credential resolution to AWK, COBOL, Forth, Prolog

All four implementations now support:
- --account N flag: bypasses env vars, loads credentials from accounts.csv row N
- UNSANDBOX_ACCOUNT env var: selects default CSV row (fallback to row 0)
- accounts.csv fallback: tries ~/.unsandbox/accounts.csv then ./accounts.csv
- Correct priority: explicit flags > --account N > env vars > CSV row 0

AWK: Added load_accounts_csv() with native getline, GLOBAL_ACCOUNT_INDEX
variable, pre-scan loop in END block for --account/-p/-k global flags.

COBOL: Added GET-CREDENTIALS paragraph with shell-based CSV resolution,
WS-ACCOUNT-INDEX variable, --account N parsing at start of MAIN-PROCEDURE,
all HANDLE-* paragraphs now PERFORM GET-CREDENTIALS instead of inline
credential fetching.

Forth: Added account-index variable, load-accounts-csv-index word, sarg
shifted-arg accessor, arg-shift variable, --account N detection in main.
All handler N arg calls replaced with N sarg to support the shift.

Prolog: Added load_accounts_csv/3 predicate with file I/O, nb_setval/nb_getval
for account_index global, --account N pattern matching in main/1,
get_public_key/1 and get_secret_key/1 now check account_index before env vars.
This commit is contained in:
russell@unturf.com 2026-03-23 15:21:56 -04:00
parent 422985c6db
commit 7dd16bf796
4 changed files with 455 additions and 194 deletions

View file

@ -64,6 +64,9 @@ BEGIN {
GREEN = "\033[32m" GREEN = "\033[32m"
YELLOW = "\033[33m" YELLOW = "\033[33m"
RESET = "\033[0m" RESET = "\033[0m"
# Global credential state
GLOBAL_ACCOUNT_INDEX = -1
} }
# ============================================================================ # ============================================================================
@ -106,32 +109,84 @@ function health_check( cmd, result) {
return (result == "200") return (result == "200")
} }
function get_api_keys( public_key, secret_key, cmd) { function load_accounts_csv(index , home, path, line, fields, count, pk, sk) {
# Get public key home = ENVIRON["HOME"]
cmd = "echo -n $UNSANDBOX_PUBLIC_KEY" count = -1
cmd | getline public_key pk = ""
close(cmd) sk = ""
# Try ~/.unsandbox/accounts.csv first
# Get secret key path = home "/.unsandbox/accounts.csv"
cmd = "echo -n $UNSANDBOX_SECRET_KEY" while ((getline line < path) > 0) {
cmd | getline secret_key if (line ~ /^[[:space:]]*$/ || line ~ /^[[:space:]]*#/) continue
close(cmd) count++
if (count == index) {
# Fallback to old UNSANDBOX_API_KEY for backwards compat split(line, fields, ",")
if (public_key == "") { pk = fields[1]; sk = fields[2]
cmd = "echo -n $UNSANDBOX_API_KEY" gsub(/^[[:space:]]+|[[:space:]]+$/, "", pk)
cmd | getline public_key gsub(/^[[:space:]]+|[[:space:]]+$/, "", sk)
close(cmd) close(path)
secret_key = "" GLOBAL_PUBLIC_KEY = pk; GLOBAL_SECRET_KEY = sk
return 1
}
} }
close(path)
# Try ./accounts.csv as fallback
count = -1; path = "accounts.csv"
while ((getline line < path) > 0) {
if (line ~ /^[[:space:]]*$/ || line ~ /^[[:space:]]*#/) continue
count++
if (count == index) {
split(line, fields, ",")
pk = fields[1]; sk = fields[2]
gsub(/^[[:space:]]+|[[:space:]]+$/, "", pk)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", sk)
close(path)
GLOBAL_PUBLIC_KEY = pk; GLOBAL_SECRET_KEY = sk
return 1
}
}
close(path)
return 0
}
if (public_key == "") { function get_api_keys( public_key, secret_key, cmd, default_index) {
print RED "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" RESET > "/dev/stderr" # Priority 1: already set via -p/-k flags
if (GLOBAL_PUBLIC_KEY != "" && GLOBAL_SECRET_KEY != "") { return }
# Priority 2: --account N bypasses env vars
if (GLOBAL_ACCOUNT_INDEX >= 0) {
if (load_accounts_csv(GLOBAL_ACCOUNT_INDEX)) {
if (GLOBAL_PUBLIC_KEY != "") return
}
print RED "Error: Account index " GLOBAL_ACCOUNT_INDEX " not found in accounts.csv" RESET > "/dev/stderr"
exit 1 exit 1
} }
GLOBAL_PUBLIC_KEY = public_key # Priority 3: env vars UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY
GLOBAL_SECRET_KEY = secret_key cmd = "echo -n $UNSANDBOX_PUBLIC_KEY"; cmd | getline public_key; close(cmd)
cmd = "echo -n $UNSANDBOX_SECRET_KEY"; cmd | getline secret_key; close(cmd)
if (public_key != "" && secret_key != "") {
GLOBAL_PUBLIC_KEY = public_key; GLOBAL_SECRET_KEY = secret_key; return
}
# Fallback to legacy UNSANDBOX_API_KEY for backwards compat
if (public_key == "") {
cmd = "echo -n $UNSANDBOX_API_KEY"; cmd | getline public_key; close(cmd)
secret_key = ""
}
if (public_key != "") {
GLOBAL_PUBLIC_KEY = public_key; GLOBAL_SECRET_KEY = secret_key; return
}
# Priority 4/5: accounts.csv with UNSANDBOX_ACCOUNT env var or row 0
cmd = "echo -n $UNSANDBOX_ACCOUNT"; cmd | getline default_index; close(cmd)
if (default_index == "") default_index = 0
if (load_accounts_csv(default_index + 0)) {
if (GLOBAL_PUBLIC_KEY != "") return
}
print RED "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" RESET > "/dev/stderr"
exit 1
} }
function get_extension(filename) { function get_extension(filename) {
@ -2359,6 +2414,20 @@ END {
exit 0 exit 0
} }
# Pre-scan ARGV for global flags: --account N, -p KEY, -k KEY
for (_gi = 1; _gi < ARGC; _gi++) {
if (ARGV[_gi] == "--account" && _gi + 1 < ARGC) {
GLOBAL_ACCOUNT_INDEX = ARGV[_gi + 1] + 0
_gi++
} else if (ARGV[_gi] == "-p" && _gi + 1 < ARGC) {
GLOBAL_PUBLIC_KEY = ARGV[_gi + 1]
_gi++
} else if (ARGV[_gi] == "-k" && _gi + 1 < ARGC) {
GLOBAL_SECRET_KEY = ARGV[_gi + 1]
_gi++
}
}
if (ARGV[1] == "session") { if (ARGV[1] == "session") {
if (ARGC >= 3 && ARGV[2] == "--list") { if (ARGC >= 3 && ARGV[2] == "--list") {
session_list() session_list()

View file

@ -45,15 +45,21 @@
SELECT SOURCE-FILE ASSIGN TO WS-FILENAME SELECT SOURCE-FILE ASSIGN TO WS-FILENAME
ORGANIZATION IS LINE SEQUENTIAL ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-FILE-STATUS. FILE STATUS IS WS-FILE-STATUS.
SELECT CRED-FILE ASSIGN TO "/tmp/unsb_creds.txt"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-CRED-STATUS.
DATA DIVISION. DATA DIVISION.
FILE SECTION. FILE SECTION.
FD SOURCE-FILE. FD SOURCE-FILE.
01 SOURCE-LINE PIC X(1024). 01 SOURCE-LINE PIC X(1024).
FD CRED-FILE.
01 CRED-LINE PIC X(512).
WORKING-STORAGE SECTION. WORKING-STORAGE SECTION.
01 WS-FILENAME PIC X(256). 01 WS-FILENAME PIC X(256).
01 WS-FILE-STATUS PIC XX. 01 WS-FILE-STATUS PIC XX.
01 WS-CRED-STATUS PIC XX.
01 WS-API-KEY PIC X(256). 01 WS-API-KEY PIC X(256).
01 WS-PUBLIC-KEY PIC X(256). 01 WS-PUBLIC-KEY PIC X(256).
01 WS-SECRET-KEY PIC X(256). 01 WS-SECRET-KEY PIC X(256).
@ -97,12 +103,23 @@
01 WS-UOD-ENABLED PIC X(8). 01 WS-UOD-ENABLED PIC X(8).
01 WS-TYPE PIC X(32). 01 WS-TYPE PIC X(32).
01 WS-SHELL PIC X(32). 01 WS-SHELL PIC X(32).
01 WS-ACCOUNT-INDEX PIC S9(4) VALUE -1.
01 WS-ACCOUNT-STR PIC X(16).
01 WS-ACCT-POS PIC 9(4) VALUE 0.
01 WS-ERROR-MSG PIC X(256).
PROCEDURE DIVISION. PROCEDURE DIVISION.
MAIN-PROCEDURE. MAIN-PROCEDURE.
* Get command line argument (first argument) * Get first command line argument
ACCEPT WS-ARG1 FROM COMMAND-LINE. ACCEPT WS-ARG1 FROM COMMAND-LINE.
* Pre-scan: handle --account N global flag before subcommand
IF WS-ARG1 = "--account"
ACCEPT WS-ACCOUNT-STR FROM ARGUMENT-VALUE
MOVE FUNCTION NUMVAL(WS-ACCOUNT-STR) TO WS-ACCOUNT-INDEX
ACCEPT WS-ARG1 FROM ARGUMENT-VALUE
END-IF.
IF WS-ARG1 = SPACES IF WS-ARG1 = SPACES
DISPLAY "Usage: un.cob <source_file>" UPON SYSERR DISPLAY "Usage: un.cob <source_file>" UPON SYSERR
DISPLAY " un.cob session [options]" UPON SYSERR DISPLAY " un.cob session [options]" UPON SYSERR
@ -147,7 +164,94 @@
PERFORM HANDLE-EXECUTE. PERFORM HANDLE-EXECUTE.
STOP RUN. STOP RUN.
GET-CREDENTIALS.
* If already set, return immediately
IF WS-PUBLIC-KEY NOT = SPACES AND WS-SECRET-KEY NOT = SPACES
EXIT PARAGRAPH
END-IF.
* Build shell script to resolve credentials with full priority
IF WS-ACCOUNT-INDEX >= 0
MOVE WS-ACCOUNT-INDEX TO WS-ACCOUNT-STR
STRING "IDX=" FUNCTION TRIM(WS-ACCOUNT-STR) "; "
"PK=''; SK=''; CNT=-1; "
"for CSV in \"$HOME/.unsandbox/accounts.csv\" "
"\"./accounts.csv\"; do "
"[ -f \"$CSV\" ] || continue; "
"while IFS= read -r line || [ -n \"$line\" ]; do "
"case \"$line\" in \"#\"*|\"\"|\" \"*) continue ;; esac; "
"CNT=$((CNT+1)); "
"if [ \"$CNT\" -eq \"$IDX\" ]; then "
"PK=$(echo \"$line\" | cut -d',' -f1 | tr -d ' '); "
"SK=$(echo \"$line\" | cut -d',' -f2 | tr -d ' '); "
"break 2; fi; "
"done < \"$CSV\"; done; "
"if [ -z \"$PK\" ]; then "
"echo -e '\\x1b[31mError: Account index "
FUNCTION TRIM(WS-ACCOUNT-STR)
" not found in accounts.csv\\x1b[0m' >&2; exit 1; fi; "
"printf '%s\\n%s\\n' \"$PK\" \"$SK\" "
"> /tmp/unsb_creds.txt"
DELIMITED BY SIZE INTO WS-CURL-CMD
END-STRING
ELSE
STRING "PK=\"$UNSANDBOX_PUBLIC_KEY\"; "
"SK=\"$UNSANDBOX_SECRET_KEY\"; "
"if [ -z \"$PK\" ]; then PK=\"$UNSANDBOX_API_KEY\"; SK=''; fi; "
"if [ -z \"$PK\" ]; then "
"IDX=\"${UNSANDBOX_ACCOUNT:-0}\"; "
"CNT=-1; "
"for CSV in \"$HOME/.unsandbox/accounts.csv\" "
"\"./accounts.csv\"; do "
"[ -f \"$CSV\" ] || continue; "
"while IFS= read -r line || [ -n \"$line\" ]; do "
"case \"$line\" in \"#\"*|\"\"|\" \"*) continue ;; esac; "
"CNT=$((CNT+1)); "
"if [ \"$CNT\" -eq \"$IDX\" ]; then "
"PK=$(echo \"$line\" | cut -d',' -f1 | tr -d ' '); "
"SK=$(echo \"$line\" | cut -d',' -f2 | tr -d ' '); "
"break 2; fi; "
"done < \"$CSV\"; done; fi; "
"if [ -z \"$PK\" ]; then "
"echo -e '\\x1b[31mError: No credentials found\\x1b[0m' "
">&2; exit 1; fi; "
"printf '%s\\n%s\\n' \"$PK\" \"$SK\" "
"> /tmp/unsb_creds.txt"
DELIMITED BY SIZE INTO WS-CURL-CMD
END-STRING
END-IF.
CALL "SYSTEM" USING WS-CURL-CMD
RETURNING WS-EXIT-CODE.
IF WS-EXIT-CODE NOT = 0
MOVE WS-EXIT-CODE TO RETURN-CODE
STOP RUN
END-IF.
* Read resolved credentials from temp file
MOVE SPACES TO WS-PUBLIC-KEY.
MOVE SPACES TO WS-SECRET-KEY.
OPEN INPUT CRED-FILE.
IF WS-CRED-STATUS = "00"
READ CRED-FILE INTO WS-PUBLIC-KEY
READ CRED-FILE INTO WS-SECRET-KEY
CLOSE CRED-FILE
END-IF.
IF WS-PUBLIC-KEY = SPACES
DISPLAY "Error: Could not resolve credentials"
UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF.
HANDLE-EXECUTE. HANDLE-EXECUTE.
* Get credentials
PERFORM GET-CREDENTIALS.
IF WS-API-KEY = SPACES
MOVE WS-PUBLIC-KEY TO WS-API-KEY
END-IF.
* Check if file exists * Check if file exists
OPEN INPUT SOURCE-FILE. OPEN INPUT SOURCE-FILE.
IF WS-FILE-STATUS NOT = "00" IF WS-FILE-STATUS NOT = "00"
@ -168,25 +272,14 @@
STOP RUN STOP RUN
END-IF. END-IF.
* Get API key from environment
ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY".
IF WS-API-KEY = SPACES
DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF.
* Use curl to make request * Use curl to make request
PERFORM MAKE-EXECUTE-REQUEST. PERFORM MAKE-EXECUTE-REQUEST.
HANDLE-SESSION. HANDLE-SESSION.
* Get API key * Get credentials
ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY". PERFORM GET-CREDENTIALS.
IF WS-API-KEY = SPACES IF WS-API-KEY = SPACES
DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR MOVE WS-PUBLIC-KEY TO WS-API-KEY
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF. END-IF.
* Initialize session parameters * Initialize session parameters
@ -209,27 +302,8 @@
END-IF. END-IF.
HANDLE-SERVICE. HANDLE-SERVICE.
* Get API keys (try new format first, fall back to old) * Get credentials
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". PERFORM GET-CREDENTIALS.
IF WS-PUBLIC-KEY NOT = SPACES
ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY"
IF WS-SECRET-KEY = SPACES
DISPLAY "Error: UNSANDBOX_SECRET_KEY not set"
UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
ELSE
ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY"
IF WS-API-KEY = SPACES
DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or "
"UNSANDBOX_API_KEY not set" UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
MOVE WS-API-KEY TO WS-PUBLIC-KEY
MOVE WS-API-KEY TO WS-SECRET-KEY
END-IF.
* Initialize service parameters * Initialize service parameters
MOVE SPACES TO WS-NAME. MOVE SPACES TO WS-NAME.
@ -332,26 +406,7 @@
END-IF. END-IF.
MAKE-EXECUTE-REQUEST. MAKE-EXECUTE-REQUEST.
* Get public/secret keys with fallback * Credentials already resolved by caller (GET-CREDENTIALS)
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY".
IF WS-PUBLIC-KEY NOT = SPACES
ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY"
IF WS-SECRET-KEY = SPACES
DISPLAY "Error: UNSANDBOX_SECRET_KEY not set"
UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
ELSE
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY"
IF WS-PUBLIC-KEY = SPACES
DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or "
"UNSANDBOX_API_KEY not set" UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
MOVE WS-PUBLIC-KEY TO WS-SECRET-KEY
END-IF.
* Build curl command using shell with HMAC signature * Build curl command using shell with HMAC signature
STRING "TS=$(date +%s); " STRING "TS=$(date +%s); "
@ -943,12 +998,10 @@
CALL "SYSTEM" USING WS-CURL-CMD. CALL "SYSTEM" USING WS-CURL-CMD.
HANDLE-KEY. HANDLE-KEY.
* Get API key * Get credentials
ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY". PERFORM GET-CREDENTIALS.
IF WS-API-KEY = SPACES IF WS-API-KEY = SPACES
DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR MOVE WS-PUBLIC-KEY TO WS-API-KEY
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF. END-IF.
* Parse key arguments * Parse key arguments
@ -1124,27 +1177,8 @@
CALL "SYSTEM" USING WS-CURL-CMD. CALL "SYSTEM" USING WS-CURL-CMD.
HANDLE-LANGUAGES. HANDLE-LANGUAGES.
* Get API keys * Get credentials
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". PERFORM GET-CREDENTIALS.
IF WS-PUBLIC-KEY NOT = SPACES
ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY"
IF WS-SECRET-KEY = SPACES
DISPLAY "Error: UNSANDBOX_SECRET_KEY not set"
UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
ELSE
ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY"
IF WS-API-KEY = SPACES
DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or "
"UNSANDBOX_API_KEY not set" UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
MOVE WS-API-KEY TO WS-PUBLIC-KEY
MOVE WS-API-KEY TO WS-SECRET-KEY
END-IF.
* Parse --json flag * Parse --json flag
MOVE SPACES TO WS-JSON-OUTPUT. MOVE SPACES TO WS-JSON-OUTPUT.
@ -1219,27 +1253,8 @@
CALL "SYSTEM" USING WS-CURL-CMD. CALL "SYSTEM" USING WS-CURL-CMD.
HANDLE-IMAGE. HANDLE-IMAGE.
* Get API keys (try new format first, fall back to old) * Get credentials
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". PERFORM GET-CREDENTIALS.
IF WS-PUBLIC-KEY NOT = SPACES
ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY"
IF WS-SECRET-KEY = SPACES
DISPLAY "Error: UNSANDBOX_SECRET_KEY not set"
UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
ELSE
ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY"
IF WS-API-KEY = SPACES
DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or "
"UNSANDBOX_API_KEY not set" UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
MOVE WS-API-KEY TO WS-PUBLIC-KEY
MOVE WS-API-KEY TO WS-SECRET-KEY
END-IF.
* Initialize image parameters * Initialize image parameters
MOVE SPACES TO WS-ID. MOVE SPACES TO WS-ID.
@ -1676,15 +1691,7 @@
HANDLE-SNAPSHOT. HANDLE-SNAPSHOT.
* Get credentials * Get credentials
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT PERFORM GET-CREDENTIALS.
"UNSANDBOX_PUBLIC_KEY".
ACCEPT WS-SECRET-KEY FROM ENVIRONMENT
"UNSANDBOX_SECRET_KEY".
IF WS-PUBLIC-KEY = SPACES OR WS-SECRET-KEY = SPACES
DISPLAY "Error: API keys not set" UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF.
* Get second argument (operation or --list) * Get second argument (operation or --list)
ACCEPT WS-ARG2 FROM ARGUMENT-VALUE. ACCEPT WS-ARG2 FROM ARGUMENT-VALUE.

View file

@ -97,8 +97,84 @@
find-ext ext-lang find-ext ext-lang
; ;
\ Account index for --account N flag (-1 = not set)
variable account-index
-1 account-index !
\ Argument shift: 0 normally, 2 when --account N is prepended
variable arg-shift
0 arg-shift !
\ Shifted arg accessor - applies arg-shift to all handler arg accesses
: sarg ( n -- addr len )
arg-shift @ + arg
;
\ Buffer for credentials loaded from CSV
256 constant MAX-KEY-LEN
create csv-pk-buf MAX-KEY-LEN allot
variable csv-pk-len
create csv-sk-buf MAX-KEY-LEN allot
variable csv-sk-len
\ Load credentials from accounts.csv at given index (n)
\ Writes PK/SK to /tmp/unsb_creds.txt; returns true if PK found
: load-accounts-csv-index ( n -- flag )
dup 0< if drop 0 exit then
\ Write a shell script with the index embedded
s" /tmp/unsb_cred_resolve.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw
s" IDX=" r@ write-file throw
dup 0 <# #s #> r@ write-file throw
s" " r@ write-line throw
s" CNT=-1; PK=''; SK=''" r@ write-line throw
s" for CSV in \"$HOME/.unsandbox/accounts.csv\" \"./accounts.csv\"; do" r@ write-line throw
s" [ -f \"$CSV\" ] || continue" r@ write-line throw
s" while IFS= read -r line || [ -n \"$line\" ]; do" r@ write-line throw
s" case \"$line\" in '#'*|'') continue ;; esac" r@ write-line throw
s" CNT=$((CNT+1))" r@ write-line throw
s" if [ \"$CNT\" -eq \"$IDX\" ]; then" r@ write-line throw
s" PK=$(echo \"$line\" | cut -d',' -f1 | tr -d ' ')" r@ write-line throw
s" SK=$(echo \"$line\" | cut -d',' -f2 | tr -d ' ')" r@ write-line throw
s" break 2" r@ write-line throw
s" fi" r@ write-line throw
s" done < \"$CSV\"" r@ write-line throw
s" done" r@ write-line throw
s" printf '%s\\n%s\\n' \"$PK\" \"$SK\" > /tmp/unsb_creds.txt" r@ write-line throw
s" [ -n \"$PK\" ]" r@ write-line throw
r> close-file throw
drop \ drop index
s" bash /tmp/unsb_cred_resolve.sh" system
0= if
\ Script exited 0: PK was found; read results
s" /tmp/unsb_creds.txt" r/o open-file
0= if
>r
csv-pk-buf MAX-KEY-LEN r@ read-line throw
drop csv-pk-len !
csv-sk-buf MAX-KEY-LEN r@ read-line throw
drop csv-sk-len !
r> close-file throw
-1
else
drop 0
then
else
0
then
;
\ Get API keys from environment (HMAC or legacy) \ Get API keys from environment (HMAC or legacy)
: get-public-key ( -- addr len ) : get-public-key ( -- addr len )
account-index @ dup 0>= if
load-accounts-csv-index if
csv-pk-buf csv-pk-len @ exit
then
s" Error: Account index not found in accounts.csv" type cr
1 (bye)
else
drop
then
s" UNSANDBOX_PUBLIC_KEY" getenv s" UNSANDBOX_PUBLIC_KEY" getenv
dup 0= if dup 0= if
2drop s" UNSANDBOX_API_KEY" getenv 2drop s" UNSANDBOX_API_KEY" getenv
@ -110,6 +186,20 @@
; ;
: get-secret-key ( -- addr len ) : get-secret-key ( -- addr len )
account-index @ dup 0>= if
\ CSV already loaded if pk-buf is non-empty
csv-pk-len @ 0> if
drop csv-sk-buf csv-sk-len @ exit
then
\ Load it now
load-accounts-csv-index if
csv-sk-buf csv-sk-len @ exit
then
\ Index not found - error was printed by get-public-key; return empty
s" " exit
else
drop
then
s" UNSANDBOX_SECRET_KEY" getenv s" UNSANDBOX_SECRET_KEY" getenv
dup 0= if dup 0= if
2drop s" UNSANDBOX_API_KEY" getenv 2drop s" UNSANDBOX_API_KEY" getenv
@ -716,7 +806,7 @@
0 (bye) 0 (bye)
then then
2 arg 2dup s" --extend" compare 0= if 2 sarg 2dup s" --extend" compare 0= if
2drop 2drop
1 validate-key 1 validate-key
0 (bye) 0 (bye)
@ -779,7 +869,7 @@
1 (bye) 1 (bye)
then then
2 arg 2dup s" --list" compare 0= if 2 sarg 2dup s" --list" compare 0= if
2drop session-list 2drop session-list
0 (bye) 0 (bye)
then then
@ -795,7 +885,7 @@
s" Error: --kill requires session ID" type cr s" Error: --kill requires session ID" type cr
1 (bye) 1 (bye)
then then
3 arg session-kill 3 sarg session-kill
0 (bye) 0 (bye)
then then
@ -835,7 +925,7 @@
1 (bye) 1 (bye)
then then
2 arg 2dup s" --list" compare 0= if 2 sarg 2dup s" --list" compare 0= if
2drop service-list 2drop service-list
0 (bye) 0 (bye)
then then
@ -856,7 +946,7 @@
s" Error: --info requires service ID" type cr s" Error: --info requires service ID" type cr
1 (bye) 1 (bye)
then then
3 arg service-info 3 sarg service-info
0 (bye) 0 (bye)
then then
@ -866,7 +956,7 @@
s" Error: --logs requires service ID" type cr s" Error: --logs requires service ID" type cr
1 (bye) 1 (bye)
then then
3 arg service-logs 3 sarg service-logs
0 (bye) 0 (bye)
then then
@ -876,7 +966,7 @@
s" Error: --freeze requires service ID" type cr s" Error: --freeze requires service ID" type cr
1 (bye) 1 (bye)
then then
3 arg service-sleep 3 sarg service-sleep
0 (bye) 0 (bye)
then then
@ -886,7 +976,7 @@
s" Error: --unfreeze requires service ID" type cr s" Error: --unfreeze requires service ID" type cr
1 (bye) 1 (bye)
then then
3 arg service-wake 3 sarg service-wake
0 (bye) 0 (bye)
then then
@ -896,7 +986,7 @@
s" Error: --destroy requires service ID" type cr s" Error: --destroy requires service ID" type cr
1 (bye) 1 (bye)
then then
3 arg service-destroy 3 sarg service-destroy
0 (bye) 0 (bye)
then then
@ -911,13 +1001,13 @@
s" Error: --resize requires --vcpu N" type cr s" Error: --resize requires --vcpu N" type cr
1 (bye) 1 (bye)
then then
4 arg 2dup s" --vcpu" compare 0= if 4 sarg 2dup s" --vcpu" compare 0= if
2drop 2drop
argc @ 6 < if argc @ 6 < if
s" Error: --vcpu requires a value" type cr s" Error: --vcpu requires a value" type cr
1 (bye) 1 (bye)
then then
3 arg 5 arg service-resize 3 sarg 5 sarg service-resize
0 (bye) 0 (bye)
then then
2dup s" -v" compare 0= if 2dup s" -v" compare 0= if
@ -926,7 +1016,7 @@
s" Error: -v requires a value" type cr s" Error: -v requires a value" type cr
1 (bye) 1 (bye)
then then
3 arg 5 arg service-resize 3 sarg 5 sarg service-resize
0 (bye) 0 (bye)
then then
2drop 2drop
@ -940,16 +1030,16 @@
s" Error: --dump-bootstrap requires service ID" type cr s" Error: --dump-bootstrap requires service ID" type cr
1 (bye) 1 (bye)
then then
3 arg 3 sarg
\ Check for --dump-file \ Check for --dump-file
argc @ 5 >= if argc @ 5 >= if
4 arg 2dup s" --dump-file" compare 0= if 4 sarg 2dup s" --dump-file" compare 0= if
2drop 2drop
argc @ 6 < if argc @ 6 < if
s" Error: --dump-file requires filename" type cr s" Error: --dump-file requires filename" type cr
1 (bye) 1 (bye)
then then
5 arg 5 sarg
else else
2drop 0 0 2drop 0 0
then then
@ -967,13 +1057,13 @@
s" Usage: un.forth service env <status|set|export|delete> <service_id> [options]" type cr s" Usage: un.forth service env <status|set|export|delete> <service_id> [options]" type cr
1 (bye) 1 (bye)
then then
3 arg 2dup s" status" compare 0= if 3 sarg 2dup s" status" compare 0= if
2drop 2drop
argc @ 5 < if argc @ 5 < if
s" Error: status requires service ID" type cr s" Error: status requires service ID" type cr
1 (bye) 1 (bye)
then then
4 arg service-env-status 4 sarg service-env-status
0 (bye) 0 (bye)
then then
2dup s" set" compare 0= if 2dup s" set" compare 0= if
@ -991,7 +1081,7 @@
s" Error: export requires service ID" type cr s" Error: export requires service ID" type cr
1 (bye) 1 (bye)
then then
4 arg service-env-export 4 sarg service-env-export
0 (bye) 0 (bye)
then then
2dup s" delete" compare 0= if 2dup s" delete" compare 0= if
@ -1000,7 +1090,7 @@
s" Error: delete requires service ID" type cr s" Error: delete requires service ID" type cr
1 (bye) 1 (bye)
then then
4 arg service-env-delete 4 sarg service-env-delete
0 (bye) 0 (bye)
then then
2drop 2drop
@ -1072,7 +1162,7 @@
0 (bye) 0 (bye)
then then
2 arg 2dup s" --json" compare 0= if 2 sarg 2dup s" --json" compare 0= if
2drop 2drop
1 languages-list 1 languages-list
0 (bye) 0 (bye)
@ -1670,7 +1760,7 @@
0 (bye) 0 (bye)
then then
2 arg 2dup s" --list" compare 0= if 2 sarg 2dup s" --list" compare 0= if
2drop snapshot-list 2drop snapshot-list
0 (bye) 0 (bye)
then then
@ -1686,7 +1776,7 @@
s" Error: --info requires snapshot ID" type cr s" Error: --info requires snapshot ID" type cr
1 (bye) 1 (bye)
then then
3 arg snapshot-info 3 sarg snapshot-info
0 (bye) 0 (bye)
then then
@ -1696,7 +1786,7 @@
s" Error: --restore requires snapshot ID" type cr s" Error: --restore requires snapshot ID" type cr
1 (bye) 1 (bye)
then then
3 arg snapshot-restore 3 sarg snapshot-restore
0 (bye) 0 (bye)
then then
@ -1706,7 +1796,7 @@
s" Error: --delete requires snapshot ID" type cr s" Error: --delete requires snapshot ID" type cr
1 (bye) 1 (bye)
then then
3 arg snapshot-delete 3 sarg snapshot-delete
0 (bye) 0 (bye)
then then
@ -1716,7 +1806,7 @@
s" Error: --lock requires snapshot ID" type cr s" Error: --lock requires snapshot ID" type cr
1 (bye) 1 (bye)
then then
3 arg snapshot-lock 3 sarg snapshot-lock
0 (bye) 0 (bye)
then then
@ -1726,7 +1816,7 @@
s" Error: --unlock requires snapshot ID" type cr s" Error: --unlock requires snapshot ID" type cr
1 (bye) 1 (bye)
then then
3 arg snapshot-unlock 3 sarg snapshot-unlock
0 (bye) 0 (bye)
then then
@ -1736,7 +1826,7 @@
s" Error: --clone requires snapshot ID" type cr s" Error: --clone requires snapshot ID" type cr
1 (bye) 1 (bye)
then then
3 arg snapshot-clone 3 sarg snapshot-clone
0 (bye) 0 (bye)
then then
@ -1752,7 +1842,7 @@
1 (bye) 1 (bye)
then then
2 arg 2dup s" --list" compare 0= if 2 sarg 2dup s" --list" compare 0= if
2drop image-list 2drop image-list
0 (bye) 0 (bye)
then then
@ -1768,7 +1858,7 @@
s" Error: --info requires image ID" type cr s" Error: --info requires image ID" type cr
1 (bye) 1 (bye)
then then
3 arg image-info 3 sarg image-info
0 (bye) 0 (bye)
then then
@ -1778,7 +1868,7 @@
s" Error: --delete requires image ID" type cr s" Error: --delete requires image ID" type cr
1 (bye) 1 (bye)
then then
3 arg image-delete 3 sarg image-delete
0 (bye) 0 (bye)
then then
@ -1788,7 +1878,7 @@
s" Error: --lock requires image ID" type cr s" Error: --lock requires image ID" type cr
1 (bye) 1 (bye)
then then
3 arg image-lock 3 sarg image-lock
0 (bye) 0 (bye)
then then
@ -1798,7 +1888,7 @@
s" Error: --unlock requires image ID" type cr s" Error: --unlock requires image ID" type cr
1 (bye) 1 (bye)
then then
3 arg image-unlock 3 sarg image-unlock
0 (bye) 0 (bye)
then then
@ -1818,7 +1908,7 @@
s" Error: --visibility requires image ID and mode" type cr s" Error: --visibility requires image ID and mode" type cr
1 (bye) 1 (bye)
then then
3 arg 4 arg image-visibility 3 sarg 4 sarg image-visibility
0 (bye) 0 (bye)
then then
@ -1828,7 +1918,7 @@
s" Error: --spawn requires image ID" type cr s" Error: --spawn requires image ID" type cr
1 (bye) 1 (bye)
then then
3 arg image-spawn 3 sarg image-spawn
0 (bye) 0 (bye)
then then
@ -1838,7 +1928,7 @@
s" Error: --clone requires image ID" type cr s" Error: --clone requires image ID" type cr
1 (bye) 1 (bye)
then then
3 arg image-clone 3 sarg image-clone
0 (bye) 0 (bye)
then then
@ -1860,8 +1950,21 @@
1 (bye) 1 (bye)
then then
\ Get first argument (skip gforth and script name) \ Check for --account N as first argument (before arg-shift is applied)
1 arg 1 arg 2dup s" --account" compare 0= if
2drop
argc @ 3 < if
s" Error: --account requires a numeric argument" type cr
1 (bye)
then
2 arg s>number drop account-index !
2 arg-shift !
else
2drop
then
\ Get subcommand (adjusted for arg-shift)
1 arg-shift @ + arg
\ Check for subcommands \ Check for subcommands
2dup s" session" compare 0= if 2dup s" session" compare 0= if

View file

@ -39,6 +39,9 @@
:- initialization(main, main). :- initialization(main, main).
% Initialize global account index to -1 (not set)
:- nb_setval(account_index, -1).
% Constants % Constants
portal_base('https://unsandbox.com'). portal_base('https://unsandbox.com').
languages_cache_ttl(3600). % 1 hour cache TTL languages_cache_ttl(3600). % 1 hour cache TTL
@ -85,26 +88,91 @@ read_file_content(Filename, Content) :-
read_string(Stream, _, Content), read_string(Stream, _, Content),
close(Stream). close(Stream).
% Get API keys from environment (HMAC or legacy) % Load credentials from accounts.csv at 0-indexed row
% Tries ~/.unsandbox/accounts.csv first, then ./accounts.csv
load_accounts_csv(Index, PK, SK) :-
getenv('HOME', Home),
atomic_list_concat([Home, '/.unsandbox/accounts.csv'], Path1),
( exists_file(Path1)
-> load_accounts_csv_file(Path1, Index, PK, SK)
; exists_file('accounts.csv')
-> load_accounts_csv_file('accounts.csv', Index, PK, SK)
; fail
).
load_accounts_csv_file(Path, Index, PK, SK) :-
setup_call_cleanup(
open(Path, read, Stream),
load_accounts_csv_stream(Stream, 0, Index, PK, SK),
close(Stream)
).
load_accounts_csv_stream(Stream, Count, Index, PK, SK) :-
read_line_to_string(Stream, Line),
Line \= end_of_file,
!,
% Skip blank lines and comments
( (Line = "" ; string_code(1, Line, 35)) % 35 = '#'
-> load_accounts_csv_stream(Stream, Count, Index, PK, SK)
; Count =:= Index
-> split_string(Line, ",", " \t", [PKStr, SKStr|_]),
atom_string(PK, PKStr),
atom_string(SK, SKStr)
; Next is Count + 1,
load_accounts_csv_stream(Stream, Next, Index, PK, SK)
).
% Get API keys from environment (HMAC or legacy), respecting --account N
get_public_key(PublicKey) :- get_public_key(PublicKey) :-
( getenv('UNSANDBOX_PUBLIC_KEY', PublicKey), ( nb_getval(account_index, Idx), Idx >= 0
PublicKey \= '' -> ( load_accounts_csv(Idx, PublicKey, _)
-> true -> true
; getenv('UNSANDBOX_API_KEY', PublicKey), ; format(user_error, 'Error: Account index ~w not found in accounts.csv~n', [Idx]),
PublicKey \= '' halt(1)
-> true )
; write(user_error, 'Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY environment variable not set\n'), ; ( getenv('UNSANDBOX_PUBLIC_KEY', PublicKey),
halt(1) PublicKey \= ''
-> true
; getenv('UNSANDBOX_API_KEY', PublicKey),
PublicKey \= ''
-> true
; % Try accounts.csv with UNSANDBOX_ACCOUNT or row 0
( getenv('UNSANDBOX_ACCOUNT', IdxStr),
atom_number(IdxStr, DefaultIdx)
-> true
; DefaultIdx = 0
),
( load_accounts_csv(DefaultIdx, PublicKey, _)
-> true
; write(user_error, 'Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY environment variable not set\n'),
halt(1)
)
)
). ).
get_secret_key(SecretKey) :- get_secret_key(SecretKey) :-
( getenv('UNSANDBOX_SECRET_KEY', SecretKey), ( nb_getval(account_index, Idx), Idx >= 0
SecretKey \= '' -> ( load_accounts_csv(Idx, _, SecretKey)
-> true -> true
; getenv('UNSANDBOX_API_KEY', SecretKey), ; SecretKey = ''
SecretKey \= '' )
-> true ; ( getenv('UNSANDBOX_SECRET_KEY', SecretKey),
; SecretKey = '' SecretKey \= ''
-> true
; getenv('UNSANDBOX_API_KEY', SecretKey),
SecretKey \= ''
-> true
; % Try accounts.csv with UNSANDBOX_ACCOUNT or row 0
( getenv('UNSANDBOX_ACCOUNT', IdxStr),
atom_number(IdxStr, DefaultIdx)
-> true
; DefaultIdx = 0
),
( load_accounts_csv(DefaultIdx, _, SecretKey)
-> true
; SecretKey = ''
)
)
). ).
% Get API key (legacy compatibility) % Get API key (legacy compatibility)
@ -742,8 +810,22 @@ service_create_with_vault(Name, Ports, Bootstrap, BootstrapFile, ServiceType, In
% Main program % Main program
main(Argv) :- main(Argv) :-
% Initialize account_index global to -1 (not set)
nb_setval(account_index, -1),
% Parse --account N global flag if present
( Argv = ['--account', NStr|Rest]
-> ( atom_number(NStr, N), integer(N)
-> nb_setval(account_index, N),
ActualArgv = Rest
; write(user_error, 'Error: --account requires an integer argument\n'),
halt(1)
)
; ActualArgv = Argv
),
% Check arguments % Check arguments
( Argv = [] ( ActualArgv = []
-> write(user_error, 'Usage: un.pro [options] <source_file>\n'), -> write(user_error, 'Usage: un.pro [options] <source_file>\n'),
write(user_error, ' un.pro session [options]\n'), write(user_error, ' un.pro session [options]\n'),
write(user_error, ' un.pro service [options]\n'), write(user_error, ' un.pro service [options]\n'),
@ -782,19 +864,19 @@ main(Argv) :-
), ),
% Parse subcommands % Parse subcommands
( Argv = ['session'|Rest] ( ActualArgv = ['session'|Rest]
-> handle_session(Rest) -> handle_session(Rest)
; Argv = ['service'|Rest] ; ActualArgv = ['service'|Rest]
-> handle_service(Rest) -> handle_service(Rest)
; Argv = ['snapshot'|Rest] ; ActualArgv = ['snapshot'|Rest]
-> handle_snapshot(Rest) -> handle_snapshot(Rest)
; Argv = ['image'|Rest] ; ActualArgv = ['image'|Rest]
-> handle_image(Rest) -> handle_image(Rest)
; Argv = ['languages'|Rest] ; ActualArgv = ['languages'|Rest]
-> handle_languages(Rest) -> handle_languages(Rest)
; Argv = ['key'|Rest] ; ActualArgv = ['key'|Rest]
-> handle_key(Rest) -> handle_key(Rest)
; Argv = [Filename|_] ; ActualArgv = [Filename|_]
-> execute_file(Filename) -> execute_file(Filename)
; write(user_error, 'Error: Invalid arguments\n'), ; write(user_error, 'Error: Invalid arguments\n'),
halt(1) halt(1)