Update SQLite repository references
ober
8344b89779adc1c46c9c69bc482aba9020e08704
--- a/cpt_corpus_v6_prep.jsonl +++ b/cpt_corpus_v6_prep.jsonl @@ -2013,7 +2013,7 @@ {"text":";; FILE: jerboa-shell/script.ss\n;;; script.ss — Script execution for gsh\n;;; Handles running script files and sourcing files into the current environment.\n\n(export #t)\n(import :std/sugar\n :std/format\n (except (std misc string) string-join string-index)\n :jsh/util\n :jsh/ast\n :jsh/environment\n :jsh/functions\n :jsh/lexer\n :jsh/parser\n :jsh/executor\n :jsh/signals\n :jsh/jobs\n :jsh/static-compat\n :jsh/registry)\n\n;;; --- Meta-command handler (set by main.ss to wire up ,compile etc.) ---\n\n(def *meta-command-handler* (make-parameter #f))\n(def *meta-command-positional* (make-parameter '()))\n\n;;; --- Jerboa Expander (lazy init) ---\n\n(def *jerboa-eval-initialized* #f)\n\n(def (ensure-jerboa-eval!)\n ;; Jerboa: eval uses Chez's (interaction-environment) — no separate expander\n ;; needed. The 'tiny' tier still blocks eval to keep the binary lean.\n (when (string=? (*jsh-tier*) \"tiny\")\n (error #f \"eval not available in this build (tier: tiny). Rebuild with JSH_TIER=small or higher\"))\n (unless *jerboa-eval-initialized*\n (set! *jerboa-eval-initialized* #t)\n (ensure-static-compat!)))\n\n;;; --- Scheme Evaluation Helpers ---\n\n(def (fmt-bytes b)\n \"Format a byte count as a human-readable string (B/KB/MB/GB).\"\n (cond\n ((>= b (* 1024 1024 1024))\n (string-append (number->string (/ (floor (* (/ b (* 1024 1024 1024)) 100)) 100.0)) \" GB\"))\n ((>= b (* 1024 1024))\n (string-append (number->string (/ (floor (* (/ b (* 1024 1024)) 100)) 100.0)) \" MB\"))\n ((>= b 1024)\n (string-append (number->string (/ (floor (* (/ b 1024) 100)) 100.0)) \" KB\"))\n (else\n (string-append (number->string (inexact->exact (floor b))) \" B\"))))\n\n(def (handle-room-command)\n ;; Display GC, heap, and runtime information (Chez equivalent of Common Lisp's ROOM).\n (with-catch\n (lambda (e)\n (cons (call-with-output-string\n (lambda (port) (display \"Error: \" port) (display-exception e port)))\n 1))\n (lambda ()\n (collect)\n (let* ((alloc-total (bytes-allocated))\n (cpu-ms (cpu-time))\n (real-ms (real-time))\n (num-gcs (collections)))\n (cons\n (call-with-output-string\n (lambda (port)\n (display \"--- GC & Heap ---\\n\" port)\n (display \" Bytes allocated: \" port) (display (fmt-bytes alloc-total) port) (newline port)\n (display \" GC runs: \" port) (display num-gcs port) (newline port)\n (newline port)\n (display \"--- Process ---\\n\" port)\n (display \" CPU time: \" port) (display cpu-ms port) (display \" ms\" port) (newline port)\n (display \" Real time: \" port) (display real-ms port) (display \" ms\" port) (newline port)\n (newline port)\n (display \"--- Runtime ---\\n\" port)\n (display \" Chez Scheme: \" port) (display (scheme-version) port) (newline port)\n (display \" Machine type: \" port) (display (machine-type) port)))\n 0)))))\n\n(def (eval-scheme-expr expr-str)\n ;; Evaluate a Jerboa Scheme expression string and return (cons result-string status)\n ;; Status: 0 = success, 1 = error\n ;; Handle built-in meta-commands that don't need any tier\n (cond\n ((string=? expr-str \"room\") (handle-room-command))\n (else\n ;; Check for meta-commands (,compile, ,load, ,use, ,exports)\n (let ((handler (*meta-command-handler*)))\n (or (and handler (handler expr-str))\n ;; Normal Scheme eval\n (begin\n (ensure-jerboa-eval!)\n (with-catch\n (lambda (e)\n (cons (call-with-output-string\n (lambda (port)\n (display \"Scheme error: \" port)\n (display-exception e port)))\n 1))\n (lambda ()\n (let* ((expr (call-with-input-string expr-str read))\n (result (eval expr)))\n (cons\n (cond\n ;; void: no output\n ((eq? result (void)) \"\")\n ;; Multiline results: use pretty-print\n ((or (pair? result) (vector? result))\n (call-with-output-string\n (lambda (port)\n (pretty-print result port))))\n ;; Simple values: use write for unambiguous output\n (else\n (call-with-output-string\n (lambda (port)\n (write result port)))))\n 0))))))))))\n\n(def (scheme-eval-line? line)\n ;; Check if line starts with comma meta-command\n (and (> (string-length line) 0)\n (char=? (string-ref line 0) #\\,)))\n\n(def (extract-scheme-expr line)\n ;; Strip leading comma and whitespace\n (let* ((without-comma (substring line 1 (string-length line)))\n (start 0)\n (end (string-length without-comma)))\n ;; Trim leading whitespace\n (let loop-start ((i 0))\n (if (and (< i end) (char-whitespace? (string-ref without-comma i)))\n (loop-start (+ i 1))\n (substring without-comma i end)))))\n\n;;; --- Public interface ---\n\n;; Execute a script file with arguments.\n;; Sets $0 to filename, $1.. to args.\n;; Returns exit status.\n(def (execute-script filename args env)\n (if (not (file-exists? filename))\n (begin\n (fprintf (current-error-port) \"jsh: ~a: No such file or directory~n\" filename)\n 127)\n (with-catch\n (lambda (e)\n (cond\n ((break-exception? e) 0)\n ((continue-exception? e) 0)\n ((subshell-exit-exception? e) (subshell-exit-exception-status e))\n ((nounset-exception? e) (nounset-exception-status e))\n (else\n (fprintf (current-error-port) \"jsh: ~a: ~a~n\" filename (exception-message e))\n 1)))\n (lambda ()\n (let* ((content (read-file-to-string filename))\n ;; Strip shebang if present\n (script-content (strip-shebang content))\n ;; Create child environment for script\n (script-env (env-push-scope env)))\n ;; Set positional parameters\n (env-set-shell-name! script-env filename)\n (env-set-positional! script-env args)\n ;; Set LINENO tracking\n (env-set! script-env \"LINENO\" \"0\")\n ;; Execute the script content\n (parameterize ((*current-source-file* filename))\n (execute-string script-content script-env)))))))\n\n;; Source a file into the current environment (like bash's `source` or `.`)\n;; Runs in the CURRENT environment, not a child.\n;; Returns exit status.\n(def (source-file! filename env)\n (if (not (file-exists? filename))\n (begin\n (fprintf (current-error-port) \"jsh: ~a: No such file or directory~n\" filename)\n 1)\n (with-catch\n (lambda (e)\n (cond\n ;; break/continue must propagate to caller's loop\n ((break-exception? e) (raise e))\n ((continue-exception? e) (raise e))\n ;; return exits the sourced file, not the calling function\n ((return-exception? e) (return-exception-status e))\n ((errexit-exception? e) (raise e))\n ((subshell-exit-exception? e) (raise e))\n ((nounset-exception? e) (raise e))\n (else\n (fprintf (current-error-port) \"jsh: ~a: ~a~n\" filename (exception-message e))\n 1)))\n (lambda ()\n (let* ((content (read-file-to-string filename))\n (script-content (strip-shebang content)))\n (parameterize ((*current-source-file* filename))\n (execute-string script-content env)))))))\n\n;;; --- String execution ---\n\n;; Parse and execute a string of shell commands.\n;; Used by both execute-script and source-file!\n;; Lines starting with comma (,) are evaluated as Scheme instead of being parsed as shell.\n(def (execute-string input env (interactive? #f))\n ;; Split input into lines for preprocessing\n (let ((lines (string-split input #\\newline)))\n (let line-loop ((remaining-lines lines) (status 0) (shell-buffer '()))\n (cond\n ;; No more lines - execute any pending shell commands\n ((null? remaining-lines)\n (if (null? shell-buffer)\n status\n (let ((shell-input (string-join (reverse shell-buffer) \"\\n\")))\n (execute-shell-lines shell-input env interactive? status))))\n ;; Scheme eval line (starts with comma)\n ((scheme-eval-line? (car remaining-lines))\n ;; First, execute any accumulated shell commands\n (let* ((shell-status (if (null? shell-buffer)\n status\n (let ((shell-input (string-join (reverse shell-buffer) \"\\n\")))\n (execute-shell-lines shell-input env interactive? status))))\n ;; Then evaluate the Scheme expression\n (expr-str (extract-scheme-expr (car remaining-lines)))\n (result-status\n (parameterize ((*meta-command-positional*\n (env-positional-list env)))\n (eval-scheme-expr expr-str)))\n (result (car result-status))\n (scheme-status (cdr result-status)))\n ;; Display result if non-empty\n (unless (string=? result \"\")\n (display result)\n (newline))\n (env-set-last-status! env scheme-status)\n (line-loop (cdr remaining-lines) scheme-status '())))\n ;; Regular shell line - accumulate\n (else\n (line-loop (cdr remaining-lines) status (cons (car remaining-lines) shell-buffer)))))))\n\n;; Execute accumulated shell lines using the lexer/parser\n(def (execute-shell-lines input env interactive? initial-status)\n (let ((lexer (make-shell-lexer input (env-shopt? env \"extglob\"))))\n (let loop ((status initial-status))\n (let ((cmd (with-catch\n (lambda (e)\n (fprintf (current-error-port) \"jsh: syntax error: ~a~n\"\n (exception-message e))\n 'error)\n (lambda ()\n ;; Update lexer extglob flag in case shopt changed it\n (set! (lexer-extglob? lexer) (env-shopt? env \"extglob\"))\n ;; Build alias lookup: checks expand_aliases shopt, returns value or #f\n (let ((alias-fn (and (env-shopt? env \"expand_aliases\")\n (lambda (word) (alias-get env word)))))\n (parse-one-line lexer (env-shopt? env \"extglob\") alias-fn))))))\n (cond\n ((eq? cmd 'error) 2) ;; syntax error\n ((not cmd) status) ;; end of input\n ;; Unterminated quote/construct after parsing — syntax error\n ((lexer-want-more? lexer)\n (fprintf (current-error-port)\n \"jsh: syntax error: unexpected end of file~n\")\n (env-set-last-status! env 2)\n 2)\n (else\n (let ((new-status\n (with-catch\n (lambda (e)\n (cond\n ((nounset-exception? e)\n ;; In interactive mode, nounset only aborts current line\n (if interactive?\n (nounset-exception-status e)\n (raise e)))\n ((errexit-exception? e)\n (errexit-exception-status e))\n ((break-exception? e) (raise e))\n ((continue-exception? e) (raise e))\n ((subshell-exit-exception? e) (raise e))\n ((return-exception? e) (raise e))\n (else\n ;; Catch-all: print error and continue\n (let ((msg (exception-message e)))\n (with-catch (lambda (_) #!void)\n (lambda ()\n (fprintf (current-error-port) \"jsh: ~a~n\" msg)))\n ;; POSIX: syntax errors / unclosed bad substitution → exit code 2\n (if (and (string? msg)\n (or (string-prefix? \"parse error\" msg)\n (string-prefix? \"bad substitution: unclosed\" msg)))\n 2 1)))))\n (lambda ()\n (execute-command cmd env)))))\n ;; Flush stdout/stderr so builtin output appears before next command\n (with-catch (lambda (_) #!void)\n (lambda ()\n (force-output (current-output-port))\n (force-output (current-error-port))))\n (let ((__cth (*command-trace-hook*)))\n (when __cth\n (with-catch (lambda (_) #!void)\n (lambda () (__cth cmd new-status env)))))\n (env-set-last-status! env new-status)\n ;; Process pending signals between commands\n (process-pending-traps! env)\n ;; If errexit triggered, stop executing further commands\n (if (and (not (= new-status 0))\n (env-option? env \"errexit\")\n (not (*in-condition-context*)))\n new-status\n (loop new-status)))))))))\n\n;; Process pending signals and execute trap commands\n;; Lightweight version for script.ss (avoids circular import with main.ss)\n;; Traps execute with $? isolated — they don't affect the main script's $?\n(def (process-pending-traps! env)\n ;; No sleep needed — C-level signal flags are checked synchronously\n ;; in pending-signals! via ffi-signal-flag-check\n (let ((signals (pending-signals!)))\n (for-each\n (lambda (sig-name)\n (cond\n ((string=? sig-name \"CHLD\")\n (job-update-status!)\n (job-notify!))\n (else #!void))\n (let ((action (trap-get sig-name)))\n (cond\n ;; Signal has a trap command — execute it\n ((and action (string? action))\n ;; Save and restore $? so trap doesn't affect main flow\n (let ((saved-status (shell-environment-last-status env))\n (exec-fn (*execute-input*)))\n (when exec-fn\n (exec-fn action env))\n (env-set-last-status! env saved-status)))\n ;; Fatal signal with no trap — exit the script\n ;; (POSIX: default action for INT/TERM/HUP/XFSZ is to terminate)\n ((and (not action)\n (member sig-name '(\"INT\" \"TERM\" \"HUP\" \"XFSZ\")))\n (let ((signum (signal-name->number sig-name)))\n (raise (make-subshell-exit-exception (+ 128 (or signum 2)))))))))\n signals)))\n\n;;; --- Helpers ---\n\n(def (strip-shebang content)\n ;; Replace #! line with blank line (preserves line numbering for extdebug)\n (if (and (>= (string-length content) 2)\n (char=? (string-ref content 0) #\\#)\n (char=? (string-ref content 1) #\\!))\n ;; Find end of first line and replace with empty\n (let loop ((i 0))\n (cond\n ((>= i (string-length content)) \"\")\n ((char=? (string-ref content i) #\\newline)\n (substring content i (string-length content)))\n (else (loop (+ i 1)))))\n content))\n\n(def (read-file-to-string filename)\n ;; Read entire file contents as a string\n (call-with-input-file filename\n (lambda (port)\n (let ((out (open-output-string)))\n (let loop ()\n (let ((ch (read-char port)))\n (unless (eof-object? ch)\n (write-char ch out)\n (loop))))\n (get-output-string out)))))\n"} {"text":";; FILE: jerboa-shell/build-jerboa.ss\n#!chezscheme\n;;; build-jerboa.ss — Legacy compiler path for jerboa-shell .ss modules\n;;; with Jerboa imports.\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib> --compile-imported-libraries < build-jerboa.ss\n;;;\n;;; The output .sls files import from Jerboa's module paths:\n;;; (jerboa runtime), (std ...), etc.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (compiler compile))\n\n;; Thread-safe mutex helper used throughout this file\n(define-syntax with-mutex\n (syntax-rules ()\n [(_ m body ...)\n (dynamic-wind\n (lambda () (mutex-acquire m))\n (lambda () body ...)\n (lambda () (mutex-release m)))]))\n\n;; --- Configuration ---\n(define submodule-dir \"jerboa-shell\")\n(define output-dir \"src/jsh\")\n\n;; Find source file: check local override first, then submodule\n(define (find-source name)\n (let ([local (string-append \"./\" name \".ss\")]\n [sub (string-append submodule-dir \"/\" name \".ss\")])\n (cond\n ((file-exists? local) local)\n ((file-exists? sub) sub)\n (else (error 'find-source \"source file not found\" name)))))\n\n;; --- Import map: Jerboa module → Jerboa library ---\n;; KEY DIFFERENCE from the legacy shell: maps to (std ...) / (jerboa ...) paths\n(define jsh-import-map\n '(;; Standard library → Jerboa stdlib\n (:std/sugar . (std sugar))\n (:std/format . (std format))\n (:std/sort . (std sort))\n (:std/pregexp . (std pregexp))\n (:std/regex . (std regex))\n (:std/misc/string . (std misc string))\n (:std/misc/list . (std misc list))\n (:std/misc/path . (std os path))\n (:std/misc/hash . (jerboa runtime))\n (:std/iter . #f) ;; stripped — Gherkin compiles for-loops natively\n (:std/error . (std error))\n (:std/os/signal . (std os signal))\n (:std/os/signal-handler . (std os signal))\n (:std/os/fdio . (std os fdio))\n (:std/srfi/1 . (std misc list))\n (:std/foreign . #f) ;; stripped\n (:std/build-script . #f) ;; stripped\n ;; Jerboa runtime\n (:jerboa/core . #f) ;; stripped\n (:jerboa/runtime . #f) ;; stripped\n (:jerboa/runtime/init . #f)\n (:jerboa/runtime/loader . #f)\n (:jerboa/expander . #f)\n (:jerboa/compiler . #f)\n ;; Relative imports\n (\"./pregexp-compat\" . (jsh pregexp-compat))\n ;; gsh module mappings → jsh\n (:jsh/arithmetic . (jsh arithmetic))\n (:jsh/ast . (jsh ast))\n (:jsh/builtins . (jsh builtins))\n (:jsh/completion . (jsh completion))\n (:jsh/control . (jsh control))\n (:jsh/environment . (jsh environment))\n (:jsh/executor . (jsh executor))\n (:jsh/expander . (jsh expander))\n (:jsh/ffi . (jsh ffi))\n (:jsh/functions . (jsh functions))\n (:jsh/fuzzy . (jsh fuzzy))\n (:jsh/fzf . (jsh fzf))\n (:jsh/glob . (jsh glob))\n (:jsh/history . (jsh history))\n (:jsh/jobs . (jsh jobs))\n (:jsh/lexer . (jsh lexer))\n (:jsh/lineedit . (jsh lineedit))\n (:jsh/macros . (jsh macros))\n (:jsh/main . (jsh main))\n (:jsh/parser . (jsh parser))\n (:jsh/pipeline . (jsh pipeline))\n (:jsh/pregexp-compat . (jsh pregexp-compat))\n (:jsh/prompt . (jsh prompt))\n (:jsh/redirect . (jsh redirect))\n (:jsh/registry . (jsh registry))\n (:jsh/script . (jsh script))\n (:jsh/signals . (jsh signals))\n (:jsh/stage . (jsh stage))\n (:jsh/startup . (jsh startup))\n (:jsh/static-compat . (jsh static-compat))\n (:jsh/util . (jsh util))\n (:jsh/recorder . (jsh recorder))\n (:jsh/player . (jsh player))\n (:jsh/recording-index . (jsh recording-index))\n ;; Actor system\n (:std/actor/core . (std actor core))\n (:std/actor/protocol . (std actor protocol))\n (:std/actor/transport . (std actor transport))\n (:std/actor/registry . (std actor registry))\n (:std/actor/supervisor . (std actor supervisor))\n ;; Crypto\n (:std/crypto/cipher . (std crypto cipher))\n (:std/crypto/hmac . (std crypto hmac))\n (:std/crypto/etc . (std crypto etc))\n ))\n\n;; --- Base imports for all compiled modules ---\n;; KEY DIFFERENCE: uses (jerboa runtime) and local (compat gambit), not\n;; legacy runtime util/table/mop/hash modules.\n(define jsh-base-imports\n '((except (chezscheme) box box? unbox set-box!\n iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error? raise with-exception-handler identifier?\n hash-table? make-hash-table\n sort sort! path-extension\n printf fprintf\n ;; Exclude Chez builtins that (compat gambit) replaces\n file-directory? file-exists? getenv close-port\n ;; Chez void takes 0 args; Jerboa's is variadic\n void\n ;; Gambit-compatible: handles /dev/fd/N and keyword args\n open-output-file open-input-file)\n ;; Jerboa runtime provides: hash tables, keywords, errors, utilities,\n ;; and method dispatch.\n ;; Exclude void — (jerboa runtime) re-exports Chez's 0-arg void,\n ;; but Jerboa's void is variadic. Let (compat gambit)'s version win.\n (except (jerboa runtime) ~ void)\n\n ;; Import most of (compat gambit) — u8vector, threading, etc.\n ;; Exclude names that conflict with (chezscheme) builtins we still need:\n (except (compat gambit) number->string make-mutex\n with-output-to-string)\n ;; Std error for Error type, error predicates\n (std error)\n ;; Std misc for string-split, string-join, string-prefix?, path-expand, etc.\n (std misc string)\n (std misc list)\n (std misc alist)\n (std os path)\n (std format)\n (std sort)\n (std pregexp)\n (std regex)\n ))\n\n;; --- Import conflict resolution ---\n;; Import conflict resolution is compiler infrastructure, not runtime.\n(define (fix-import-conflicts lib-form)\n (let* ([lib-name (cadr lib-form)]\n [export-clause (caddr lib-form)]\n [import-clause (cadddr lib-form)]\n [body (cddddr lib-form)]\n [imports (cdr import-clause)]\n [local-defs\n (let lp ([forms body] [names '()])\n (if (null? forms)\n names\n (lp (cdr forms)\n (append (extract-def-names (car forms)) names))))]\n [all-earlier-names\n (let lp ([imps imports] [seen '()] [result '()])\n (if (null? imps)\n (reverse result)\n (let* ([imp (car imps)]\n [lib (get-import-lib-name imp)]\n [exports (if lib (cached-library-exports lib) '())]\n [provided (cond\n ((and (pair? imp) (eq? (car imp) 'except))\n (filter (lambda (s) (not (memq s (cddr imp))))\n exports))\n ((and (pair? imp) (eq? (car imp) 'only))\n (cddr imp))\n (else exports))])\n (lp (cdr imps)\n (append provided seen)\n (cons seen result)))))])\n (let ([fixed-imports\n (map (lambda (imp earlier-names)\n (fix-one-import imp\n (append local-defs earlier-names)))\n imports all-earlier-names)])\n (let ([fixed-body (fix-assigned-exports\n (cdr export-clause)\n (list (cons 'import fixed-imports))\n body)])\n `(library ,lib-name ,export-clause\n (import ,@fixed-imports) ,@fixed-body)))))\n\n;; Fix exported variables that are set!'d (R6RS forbids this)\n(define (fix-assigned-exports exports import-forms body)\n (let ([assigned-names\n (let lp ([tree body] [names '()])\n (cond\n ((not (pair? tree)) names)\n ((and (eq? (car tree) 'set!)\n (pair? (cdr tree))\n (symbol? (cadr tree))\n (memq (cadr tree) exports)\n (not (memq (cadr tree) names)))\n (cons (cadr tree) names))\n (else\n (lp (cdr tree) (lp (car tree) names)))))])\n (if (null? assigned-names)\n body\n (let ([new-body\n (let lp ([forms body] [result '()])\n (if (null? forms)\n (reverse result)\n (let ([form (car forms)])\n (cond\n ((and (pair? form)\n (eq? (car form) 'define)\n (let ([def-name (if (pair? (cadr form)) (caadr form) (cadr form))])\n (and (symbol? def-name) (memq def-name assigned-names))))\n (let* ([def-name (if (pair? (cadr form)) (caadr form) (cadr form))]\n [init (if (pair? (cadr form))\n `(lambda ,(cdadr form) ,@(cddr form))\n (if (pair? (cddr form)) (caddr form) '(void)))]\n [cell-name (string->symbol\n (string-append (symbol->string def-name) \"-cell\"))])\n (lp (cdr forms)\n (append\n (list\n `(define-syntax ,def-name\n (identifier-syntax\n (id (vector-ref ,cell-name 0))\n ((set! id v) (vector-set! ,cell-name 0 v))))\n `(define ,cell-name (vector ,init)))\n result))))\n (else\n (lp (cdr forms) (cons form result)))))))])\n new-body))))\n\n(define (extract-def-names form)\n (cond\n ((not (pair? form)) '())\n ((eq? (car form) 'define)\n (cond\n ((symbol? (cadr form)) (list (cadr form)))\n ((pair? (cadr form)) (list (caadr form)))\n (else '())))\n ((eq? (car form) 'define-syntax)\n (if (symbol? (cadr form)) (list (cadr form)) '()))\n ((eq? (car form) 'begin)\n (let lp ([forms (cdr form)] [names '()])\n (if (null? forms) names\n (lp (cdr forms) (append (extract-def-names (car forms)) names)))))\n (else '())))\n\n;; Serialize eval/library-exports across threads — both load and cache are protected\n(define eval-mutex (make-mutex 'eval))\n\n(define (ensure-library-loaded lib-name)\n (with-mutex eval-mutex\n (guard (e (#t #f))\n (eval `(import ,lib-name) (interaction-environment))\n #t)))\n\n;; Export cache: avoids repeated library-exports calls for the same library\n;; across all module compilations. With ~15 imports × 22 modules = ~330 calls,\n;; most hit the same base libraries — cache turns these into hash lookups.\n(define export-cache (make-hashtable equal-hash equal?))\n\n(define (cached-library-exports lib-name)\n (with-mutex eval-mutex\n (let ([hit (hashtable-ref export-cache lib-name 'miss)])\n (if (not (eq? hit 'miss))\n hit\n (begin\n (guard (e (#t #f))\n (eval `(import ,lib-name) (interaction-environment)))\n (let ([exports (or (guard (e (#t #f)) (library-exports lib-name))\n (read-sls-exports lib-name)\n '())])\n (hashtable-set! export-cache lib-name exports)\n exports))))))\n\n(define (read-sls-exports lib-name)\n (let ([path (lib-name->sls-path lib-name)])\n (if (and path (file-exists? path))\n (guard (e (#t #f))\n (call-with-input-file path\n (lambda (port)\n ;; Chez doesn't expand #,(...) at read or eval time, so reading\n ;; an untrusted (library ...) header is safe — no need to gate.\n (let ([first (read port)])\n (let ([lib-form (if (and (pair? first) (eq? (car first) 'library))\n first\n (read port))])\n (if (and (pair? lib-form) (eq? (car lib-form) 'library))\n (let ([export-clause (caddr lib-form)])\n (if (and (pair? export-clause) (eq? (car export-clause) 'export))\n (cdr export-clause)\n #f))\n #f))))))\n #f)))\n\n(define (lib-name->sls-path lib-name)\n (cond\n ((and (pair? lib-name) (= (length lib-name) 2)\n (eq? (car lib-name) 'jsh))\n (string-append output-dir \"/\" (symbol->string (cadr lib-name)) \".sls\"))\n ((and (pair? lib-name) (= (length lib-name) 2)\n (eq? (car lib-name) 'compat))\n (string-append \"src/compat/\" (symbol->string (cadr lib-name)) \".sls\"))\n (else #f)))\n\n(define (fix-one-import imp local-defs)\n (let ([lib-name (get-import-lib-name imp)])\n (if (not lib-name)\n imp\n (let* ([lib-exports (cached-library-exports lib-name)]\n [conflicts (filter (lambda (d) (memq d lib-exports))\n local-defs)])\n (if (null? conflicts)\n imp\n (cond\n ((and (pair? imp) (eq? (car imp) 'except))\n (let ([existing (cddr imp)])\n `(except ,(cadr imp)\n ,@existing\n ,@(filter (lambda (d) (not (memq d existing)))\n conflicts))))\n ((and (pair? imp) (eq? (car imp) 'only))\n (let ([kept (filter (lambda (s) (not (memq s conflicts)))\n (cddr imp))])\n `(only ,(cadr imp) ,@kept)))\n ((pair? imp)\n `(except ,imp ,@conflicts))\n (else imp)))))))\n\n(define (get-import-lib-name spec)\n (cond\n ((and (pair? spec)\n (memq (car spec) '(except only rename prefix)))\n (get-import-lib-name (cadr spec)))\n ((and (pair? spec) (symbol? (car spec)))\n spec)\n (else #f)))\n\n;; --- Incremental builds: skip unchanged modules ---\n;; Set FORCE=1 in env to rebuild everything regardless of timestamps.\n(define force-rebuild?\n (let ([v (getenv \"FORCE\")])\n (and v (not (string=? v \"\")))))\n\n(define (needs-rebuild? input-path output-path)\n (or force-rebuild?\n (not (file-exists? output-path))\n (< (time-second (file-modification-time output-path))\n (time-second (file-modification-time input-path)))))\n\n;; Track which .sls files were freshly generated this session.\n;; Post-build patches should only apply to these files (not to up-to-date ones).\n(define compiled-files-mutex (make-mutex 'compiled-files))\n(define compiled-files '())\n(define (record-compiled! path)\n (with-mutex compiled-files-mutex\n (set! compiled-files (cons path compiled-files))))\n(define (compiled-this-session? path)\n (with-mutex compiled-files-mutex\n (member path compiled-files)))\n\n;; --- Module compilation ---\n(define (compile-module name)\n (let* ([input-path (find-source name)]\n [output-path (string-append output-dir \"/\" name \".sls\")]\n [lib-name `(jsh ,(string->symbol name))])\n (if (not (needs-rebuild? input-path output-path))\n (begin (display (string-append \" Up-to-date: \" name \".ss\\n\")) #t)\n (begin\n (display (string-append \" Compiling: \" name \".ss → \" name \".sls\\n\"))\n (guard (exn\n (#t (display (string-append \" ERROR: \" name \".ss failed: \"))\n (display (condition-message exn))\n (when (irritants-condition? exn)\n (display \" — \")\n (display (condition-irritants exn)))\n (newline)\n #f))\n (let* ([lib-form (jerboa-compile-to-library\n input-path lib-name\n jsh-import-map jsh-base-imports)]\n [lib-form (fix-import-conflicts lib-form)])\n (call-with-output-file output-path\n (lambda (port)\n (display \"#!chezscheme\\n\" port)\n (parameterize ([print-gensym #f])\n (pretty-print lib-form port)))\n 'replace)\n (record-compiled! output-path)\n (display (string-append \" OK: \" output-path \"\\n\"))\n #t))))))\n\n;; --- Parallel tier compilation ---\n;; Modules within a tier are independent; compile them concurrently.\n;; eval/library-exports calls are serialized via eval-mutex; the\n;; jerboa-compile-to-library step (reading + parsing .ss) runs in parallel.\n(define (compile-tier label modules)\n (display (format \"\\n--- ~a ---\\n\" label))\n (if (= (length modules) 1)\n ;; Single module: avoid thread overhead\n (compile-module (car modules))\n (let* ([done-mutex (make-mutex 'done)]\n [done-cond (make-condition)]\n [remaining (length modules)])\n (for-each\n (lambda (mod)\n (fork-thread\n (lambda ()\n (compile-module mod)\n (with-mutex done-mutex\n (set! remaining (- remaining 1))\n (when (= remaining 0)\n (condition-signal done-cond))))))\n modules)\n (with-mutex done-mutex\n (let lp ()\n (when (> remaining 0)\n (condition-wait done-cond done-mutex)\n (lp)))))))\n\n;; --- Main ---\n(display \"=== Jerboa Shell Builder ===\\n\\n\")\n\n;; When JERBUILD_SKIP_COMPILE=1, the .ss→.sls step was already done by jerbuild.ss.\n;; Skip Gherkin compilation and go straight to post-build patching.\n(unless (equal? (getenv \"JERBUILD_SKIP_COMPILE\") \"1\")\n ;; Pre-warm export cache with all base imports so parallel threads get cache hits\n (display \"--- Pre-warming export cache ---\\n\")\n (for-each\n (lambda (imp)\n (let ([lib (get-import-lib-name imp)])\n (when lib (cached-library-exports lib))))\n jsh-base-imports)\n\n (compile-tier \"Tier 1: Foundation\" '(\"ast\" \"registry\"))\n (compile-tier \"Tier 2: Core\" '(\"macros\" \"util\"))\n (compile-tier \"Tier 3: Modules\" '(\"environment\" \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\" \"recorder\" \"player\"))\n (compile-tier \"Tier 4: Processing\" '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (compile-tier \"Tier 5: Execution\" '(\"redirect\" \"control\" \"jobs\"))\n (compile-tier \"Tier 5b: Builtins\" '(\"builtins\"))\n (compile-tier \"Tier 6: Pipeline\" '(\"pipeline\"))\n (compile-tier \"Tier 6b: UI\" '(\"executor\" \"completion\" \"prompt\" \"lineedit\"))\n (compile-tier \"Tier 7: Top-level\" '(\"fzf\" \"script\" \"startup\" \"main\")))\n\n;; --- Post-build: Force library invocation for side-effecting modules ---\n(display \"\\n--- Post-build: Patching for Chez lazy invocation ---\\n\")\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (let* ([path \"src/jsh/main.sls\"]\n [content (call-with-input-file path\n (lambda (p) (get-string-all p)))])\n (if (or (not (compiled-this-session? path)) (string-find content \"_force-builtins\"))\n (display \" (skip) main.sls lazy invocation\\n\")\n (let ([needle (string #\\newline #\\space #\\space #\\( #\\d #\\e #\\f #\\i #\\n #\\e #\\space)])\n (let ([idx (string-find content needle)])\n (if idx\n (begin\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display \"\\n ;; Force invocation of (jsh builtins) for defbuiltin registration\\n\" p)\n (display \" (define _force-builtins special-builtin?)\" p)\n (display (substring content idx (string-length content)) p))\n 'replace)\n (display \" Patched main.sls for lazy invocation\\n\"))\n (display \" WARNING: Could not find insertion point in main.sls\\n\")))))))\n\n;; BSD sed (macOS, FreeBSD) requires -i '' (empty backup suffix); Linux sed uses -i alone\n(define sed-i-flag\n (let* ([mt (symbol->string (machine-type))]\n [len (string-length mt)]\n [ends-fb (and (>= len 2)\n (string=? (substring mt (- len 2) len) \"fb\"))]\n [ends-osx (and (>= len 3)\n (string=? (substring mt (- len 3) len) \"osx\"))])\n (if (or ends-fb ends-osx)\n \"-i ''\" \"-i\")))\n;; SECURITY: pattern and files are interpolated into a shell command.\n;; Only call with hard-coded string literals — never with dynamic input.\n(define (sed-replace pattern files)\n (system (format \"sed ~a '~a' ~a\" sed-i-flag pattern files)))\n\n;; --- Post-build patches ---\n(display \"\\n--- Post-build: Applying patches ---\\n\")\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n ;; Fix env-push-scope: keyword-style args → positional\n (patch-file! \"src/jsh/environment.sls\"\n \"(define (env-push-scope env)\\n (make-shell-environment\\n 'parent:\\n env\\n 'name:\\n (shell-environment-shell-name env)))\"\n \"(define (env-push-scope env)\\n (make-shell-environment env (shell-environment-shell-name env)))\")\n\n ;; Fix env-clone: no-arg constructor + manual set\n (patch-file! \"src/jsh/environment.sls\"\n \"(define (env-clone env)\\n (let ([clone (make-shell-environment\\n 'name:\\n (shell-environment-shell-name env))])\"\n \"(define (env-clone env)\\n (let ([clone (let ([e (make-shell-environment)])\\n (shell-environment-shell-name-set! e (shell-environment-shell-name env))\\n e)])\")\n\n ;; Fix exception-message: Chez condition-message returns raw format templates\n (patch-file! \"src/jsh/util.sls\"\n \"(define (exception-message e)\\n (cond\\n [(Error? e) (Error-message e)]\\n [(error-exception? e) (error-exception-message e)]\\n [(string? e) e]\\n [(os-exception? e)\\n (call-with-output-string\\n (lambda (p) (display-exception e p)))]\\n [else\\n (call-with-output-string\\n (lambda (p) (display-exception e p)))]))\"\n \"(define (exception-message e)\\n (define (format-condition e)\\n (let ([msg (call-with-string-output-port\\n (lambda (p) (display-condition e p)))])\\n (if (and (> (string-length msg) 11)\\n (string=? (substring msg 0 11) \\\"Exception: \\\"))\\n (substring msg 11 (string-length msg))\\n (if (and (> (string-length msg) 13)\\n (string=? (substring msg 0 13) \\\"Exception in \\\"))\\n (let loop ([i 13])\\n (cond\\n [(>= (+ i 1) (string-length msg)) msg]\\n [(and (char=? (string-ref msg i) #\\\\:)\\n (char=? (string-ref msg (+ i 1)) #\\\\space))\\n (substring msg (+ i 2) (string-length msg))]\\n [else (loop (+ i 1))]))\\n msg))))\\n (cond\\n [(string? e) e]\\n [(condition? e) (format-condition e)]\\n [else (call-with-string-output-port\\n (lambda (p) (display e p)))]))\")\n\n ;; Fix make-mutex: Chez requires symbol or #f, not strings\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(make-mutex \\\"pipeline-fd\\\")\"\n \"(make-mutex 'pipeline-fd)\")\n\n ;; In pipelines, prefer fork+exec over builtins for true multi-CPU parallelism.\n ;; External binaries use optimized C I/O with large buffers and zero contention.\n (patch-file! \"src/jsh/pipeline.sls\"\n \"[(and cmd-name (builtin-lookup cmd-name))\\n (launch-thread-piped cmd child-env execute-fn\\n has-pipe-in? has-pipe-out?)]\\n [(and cmd-name (which cmd-name))\"\n \"[(and cmd-name (which cmd-name))\")\n\n ;; --- FreeBSD portability: /dev/fd/N doesn't work for pipe fds ---\n ;; Patching is done via a separate script (support/patch-devfd.ss)\n ;; called from the Makefile after the jerboa step.\n\n ;; --- Performance optimizations ---\n\n ;; Add (std misc lru-cache) import to util.sls for which-cached\n (patch-file! \"src/jsh/util.sls\"\n \"(except (jerboa runtime)\"\n \"(std misc lru-cache)\\n (except (jerboa runtime)\")\n\n ;; Add (std misc trie) import to completion.sls for command completion cache\n (patch-file! \"src/jsh/completion.sls\"\n \"(except (jerboa runtime)\"\n \"(std misc trie)\\n (except (jerboa runtime)\")\n\n ;; Add trie-based PATH command completion cache to completion.sls\n (patch-file! \"src/jsh/completion.sls\"\n \"(define (complete-command prefix env)\"\n (string-append\n \";; Trie-based command completion cache — rebuilt when PATH changes\\n\"\n \" (define *cmd-trie* #f)\\n\"\n \" (define *cmd-trie-path* #f)\\n\"\n \" (define (ensure-cmd-trie! env)\\n\"\n \" (let ([current-path (or (env-get env \\\"PATH\\\") \\\"\\\")])\\n\"\n \" (unless (equal? current-path *cmd-trie-path*)\\n\"\n \" (set! *cmd-trie-path* current-path)\\n\"\n \" (let ([t (make-trie)])\\n\"\n \" ;; PATH executables\\n\"\n \" (let ([path-dirs (string-split-path current-path)])\\n\"\n \" (for-each\\n\"\n \" (lambda (dir)\\n\"\n \" (with-catch\\n\"\n \" (lambda (e) (void))\\n\"\n \" (lambda ()\\n\"\n \" (when (file-exists? dir)\\n\"\n \" (for-each\\n\"\n \" (lambda (name)\\n\"\n \" (let ([full-path (string-append dir \\\"/\\\" name)])\\n\"\n \" (when (executable? full-path)\\n\"\n \" (trie-insert! t name))))\\n\"\n \" (directory-files dir))))))\\n\"\n \" path-dirs))\\n\"\n \" (set! *cmd-trie* t)))\\n\"\n \" *cmd-trie*))\\n\"\n \" (define (complete-command prefix env)\"))\n\n ;; Replace PATH executable scanning with trie lookup in complete-command\n (patch-file! \"src/jsh/completion.sls\"\n (string-append\n \" ;; PATH executables\\n\"\n \" (let ((path-dirs (string-split-path (or (env-get env \\\"PATH\\\") \\\"\\\"))))\\n\"\n \" (for-each\\n\"\n \" (lambda (dir)\\n\"\n \" (with-catch\\n\"\n \" (lambda (e) #!void)\\n\"\n \" (lambda ()\\n\"\n \" (when (file-exists? dir)\\n\"\n \" (for-each\\n\"\n \" (lambda (name)\\n\"\n \" (when (and (string-prefix-match? prefix name)\\n\"\n \" (not (member name results)))\\n\"\n \" (let ((full-path (string-append dir \\\"/\\\" name)))\\n\"\n \" (when (executable? full-path)\\n\"\n \" (set! results (cons name results))))))\\n\"\n \" (directory-files dir))))))\\n\"\n \" path-dirs))\")\n (string-append\n \" ;; PATH executables via trie cache\\n\"\n \" (let ([trie (ensure-cmd-trie! env)])\\n\"\n \" (for-each\\n\"\n \" (lambda (name)\\n\"\n \" (unless (member name results)\\n\"\n \" (set! results (cons name results))))\\n\"\n \" (trie-prefix-search trie prefix)))\"))\n\n ;; Add PATH lookup cache to util.sls (which-cached) using LRU cache\n (patch-file! \"src/jsh/util.sls\"\n \"string-last-index-of which find-file-in-path executable?\"\n \"string-last-index-of which which-cached which-cache-invalidate!\\n find-file-in-path executable?\")\n\n (patch-file! \"src/jsh/util.sls\"\n \"(def (find-file-in-path\"\n (string-append\n \";; PATH lookup cache — LRU bounded cache (bash command_hash equivalent)\\n\"\n \" (define *which-cache* (make-lru-cache 256))\\n\"\n \" (define *which-cache-path* #f)\\n\"\n \" (define (which-cache-invalidate!)\\n\"\n \" (lru-cache-clear! *which-cache*)\\n\"\n \" (set! *which-cache-path* #f))\\n\"\n \" (define (which-cached name)\\n\"\n \" (if (string-contains? name \\\"/\\\")\\n\"\n \" (which name)\\n\"\n \" (let ([current-path (or (getenv \\\"PATH\\\" #f) \\\"/usr/bin:/bin\\\")])\\n\"\n \" (unless (equal? current-path *which-cache-path*)\\n\"\n \" (lru-cache-clear! *which-cache*)\\n\"\n \" (set! *which-cache-path* current-path))\\n\"\n \" (let ([cached (lru-cache-get *which-cache* name #f)])\\n\"\n \" (or cached\\n\"\n \" (let ([found (which name)])\\n\"\n \" (when found (lru-cache-put! *which-cache* name found))\\n\"\n \" found))))))\\n\"\n \" ;; Validate a file path: reject null bytes that would truncate at C level\\n\"\n \" (define (validate-file-path path who)\\n\"\n \" (let loop ([i 0])\\n\"\n \" (when (< i (string-length path))\\n\"\n \" (when (char=? (string-ref path i) #\\\\nul)\\n\"\n \" (error who (string-append path \\\": path contains null byte\\\")))\\n\"\n \" (loop (+ i 1))))\\n\"\n \" path)\\n\"\n \" (def (find-file-in-path\"))\n\n ;; Replace (which cmd-name) with (which-cached cmd-name) in executor.sls\n ;; Only apply when executor.sls was freshly generated this session\n (let ([path \"src/jsh/executor.sls\"])\n (when (or force-rebuild? (compiled-this-session? path))\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [old \"(which cmd-name)\"]\n [new \"(which-cached cmd-name)\"]\n [olen (string-length old)]\n [nlen (string-length new)])\n (let loop ([i 0] [result \"\"])\n (cond\n ((> (+ i olen) (string-length content))\n (let ([final (string-append result (substring content i (string-length content)))])\n (call-with-output-file path\n (lambda (p) (display final p))\n 'replace)\n (printf \" Patched executor.sls: which -> which-cached~n\")))\n ((string=? (substring content i (+ i olen)) old)\n (loop (+ i olen) (string-append result new)))\n (else\n (loop (+ i 1) (string-append result (substring content i (+ i 1))))))))))\n\n ;; env-get single-pass: eliminate double hash-table scan in env-get\n (patch-file! \"src/jsh/environment.sls\"\n (string-append\n \" [else\\n\"\n \" (let ([resolved (resolve-nameref name env)])\\n\"\n \" (let ([var (find-var-in-chain env resolved)])\\n\"\n \" (if (and var (shell-var-nameref? var))\\n\"\n \" #f\\n\"\n \" (env-get-chain env resolved))))])\")\n (string-append\n \" [else\\n\"\n \" (let ([resolved (resolve-nameref name env)])\\n\"\n \" (let ([var (find-var-in-chain env resolved)])\\n\"\n \" (cond\\n\"\n \" [(not var) (getenv resolved #f)]\\n\"\n \" [(shell-var-nameref? var) #f]\\n\"\n \" [else (shell-var-scalar-value var)])))])\"))\n\n ;; Add *command-trace-hook* parameter to environment.sls — used by ,debug\n (patch-file! \"src/jsh/environment.sls\"\n \"(export *execute-input* *arith-eval-fn*\"\n \"(export *execute-input* *command-trace-hook* *arith-eval-fn*\")\n\n (patch-file! \"src/jsh/environment.sls\"\n \"(define *execute-input* (make-parameter #f))\"\n \"(define *execute-input* (make-parameter #f))\\n (define *command-trace-hook* (make-parameter #f))\")\n\n ;; Call *command-trace-hook* after each command executes in execute-shell-lines\n (patch-file! \"src/jsh/script.sls\"\n \" (env-set-last-status! env new-status)\"\n (string-append\n \" (let ([__cth (*command-trace-hook*)])\\n\"\n \" (when __cth\\n\"\n \" (guard (__e [#t (%%void)])\\n\"\n \" (__cth cmd new-status env))))\\n\"\n \" (env-set-last-status! env new-status)\")))\n\n;; --- main.sls patches: wire *current-jsh-env* for meta-commands ---\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n ;; Add harden-startup! call early in main (after init-smp!, before user input)\n (patch-file! \"src/jsh/main.sls\"\n \"(*gambit-scheduler-wfd* (ffi-gambit-scheduler-wfd))\\n (let* ([args-hash (parse-args args)])\"\n \"(*gambit-scheduler-wfd* (ffi-gambit-scheduler-wfd))\\n (harden-startup!)\\n (let* ([args-hash (parse-args args)])\")\n\n ;; Rename all gsh references to jsh throughout all generated files\n (sed-replace \"s/\\\\*gsh-/\\\\*jsh-/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\"gsh: /\\\"jsh: /g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\"gsh -/\\\"jsh -/g\" \"src/jsh/*.sls\")\n (sed-replace \"s|/bin/gsh|/bin/jsh|g\" \"src/jsh/*.sls\")\n (sed-replace \"s/GSH_VERSION/JSH_VERSION/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/GSH_PROCESSORS/JSH_PROCESSORS/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/GSH_ENV/JSH_ENV/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/Jerboa Shell/jsh/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gshrc/.jshrc/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gsh_profile/.jsh_profile/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gsh_login/.jsh_login/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gsh_logout/.jsh_logout/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gsh_history/.jsh_history/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/GSH_EXE/JSH_EXE/g\" \"src/jsh/*.sls\")\n (display \" Patched all .sls files (gsh → jsh)\\n\"))\n\n;; --- Embed support: patch startup.sls and script.sls ---\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n (display \"\\n--- Embed patches ---\\n\")\n\n ;; Add (jsh embed) import to startup.sls\n (patch-file! \"src/jsh/startup.sls\"\n \"(jsh environment) (jsh script) (jsh recorder))\"\n \"(jsh environment) (jsh script) (jsh recorder) (jsh embed))\")\n\n ;; Patch startup.sls: source embedded .jshrc before filesystem .jshrc\n ;; Login shell\n (patch-file! \"src/jsh/startup.sls\"\n \"(source-if-exists! \\\"/etc/profile\\\" env)\"\n \"(source-if-exists! \\\"//embed/.jsh_profile\\\" env)\\n (source-if-exists! \\\"/etc/profile\\\" env)\")\n\n ;; Interactive shell: source embedded .jshrc, then filesystem .jshrc\n ;; Note: target uses .jshrc because gsh→jsh sed rename runs before embed patches\n (patch-file! \"src/jsh/startup.sls\"\n \"(source-if-exists! (string-append home \\\"/.jshrc\\\") env))]\"\n \"(source-if-exists! \\\"//embed/.jshrc\\\" env)\\n (source-if-exists! (string-append home \\\"/.jshrc\\\") env))]\")\n\n ;; Patch source-if-exists! to handle //embed/ paths\n ;; For embed paths, read content via embed module and execute directly\n ;; (source-file! doesn't know about embed, so we bypass it)\n (patch-file! \"src/jsh/startup.sls\"\n \"(define (source-if-exists! path env)\\n (if (file-exists? path)\\n (begin (source-file! path env) #t)\\n #f))\"\n \"(define (source-if-exists! path env)\\n (cond\\n [(embed-path? path)\\n (cond\\n [(not (embed-unlocked?))\\n #f]\\n [else\\n (let ([content (embed-file->string path)])\\n (if content\\n (begin (execute-string (strip-shebang content) env) #t)\\n #f))])]\\n [(file-exists? path)\\n (begin (source-file! path env) #t)]\\n [else #f]))\")\n\n ;; embed-ls / embed-cat / embed-cp / embed-fd builtins and the (jsh embed)\n ;; import are now defined directly in main.ss — no patch needed here.\n\n ;; Add (jsh embed) import to script.sls\n (patch-file! \"src/jsh/script.sls\"\n \"(jsh registry))\"\n \"(jsh registry)\\n (jsh embed))\")\n\n ;; Patch source-file! in script.sls to handle //embed/ paths\n ;; This fixes ALL callers (source builtin, startup, etc.)\n (patch-file! \"src/jsh/script.sls\"\n \"(define (source-file! filename env)\\n (if (not (file-exists? filename))\"\n (string-append\n \"(define (source-file! filename env)\\n\"\n \" (if (embed-path? filename)\\n\"\n \" (let ([content (embed-file->string filename)])\\n\"\n \" (if content\\n\"\n \" (guard (__exn\\n\"\n \" [#t\\n\"\n \" ((lambda (e)\\n\"\n \" (cond\\n\"\n \" [(break-exception? e) (raise e)]\\n\"\n \" [(continue-exception? e) (raise e)]\\n\"\n \" [(return-exception? e) (return-exception-status e)]\\n\"\n \" [(errexit-exception? e) (raise e)]\\n\"\n \" [(subshell-exit-exception? e) (raise e)]\\n\"\n \" [(nounset-exception? e) (raise e)]\\n\"\n \" [else\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: ~a: ~a~n\\\" filename (exception-message e))\\n\"\n \" 1]))\\n\"\n \" __exn)])\\n\"\n \" (execute-string (strip-shebang content) env))\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: ~a: No such embedded file~n\\\" filename)\\n\"\n \" 1)))\\n\"\n \" (if (not (file-exists? filename))\"))\n\n ;; Close the extra outer if we added to source-file!\n (patch-file! \"src/jsh/script.sls\"\n \"(parameterize ([*current-source-file* filename])\\n (execute-string script-content env)))))))\"\n \"(parameterize ([*current-source-file* filename])\\n (execute-string script-content env))))))))\")\n\n ;; Add (jsh embed) import to executor.sls\n (patch-file! \"src/jsh/executor.sls\"\n \"(jsh arithmetic) (jsh ffi) (jsh glob) (jsh signals))\"\n \"(jsh arithmetic) (jsh ffi) (jsh glob) (jsh signals)\\n (jsh embed))\")\n\n ;; Add embed-rewrite-arg helper to executor.sls (after execute-external definition begins)\n ;; Rewrites //embed/ paths to /proc/self/fd/N via memfd, so external commands can read them.\n ;; Uses embed-register-fd!/embed-cleanup-fds! from (jsh embed) for fd tracking.\n (patch-file! \"src/jsh/executor.sls\"\n \"(define (execute-external cmd-name args env)\"\n (string-append\n \"(define (embed-rewrite-arg arg)\\n\"\n \" (if (embed-path? arg)\\n\"\n \" (let ([fd-path (embed-file->fd-path arg)])\\n\"\n \" (if fd-path\\n\"\n \" (begin\\n\"\n \" (embed-register-fd!\\n\"\n \" (string->number\\n\"\n \" (substring fd-path 14 (string-length fd-path))))\\n\"\n \" fd-path)\\n\"\n \" arg))\\n\"\n \" arg))\\n\"\n \" (define (execute-external cmd-name args env)\"))\n\n ;; Patch pack-with-soh call to rewrite embed args\n (patch-file! \"src/jsh/executor.sls\"\n \"(let* ([packed-argv (pack-with-soh\\n (map string->c-safe\\n (cons cmd-name args)))])\"\n \"(let* ([packed-argv (pack-with-soh\\n (map string->c-safe\\n (map embed-rewrite-arg\\n (cons cmd-name args))))])\")\n\n ;; Add embed memfd fds to keep-fds so child process inherits them\n (patch-file! \"src/jsh/executor.sls\"\n \"(let* ([keep-fds (pack-fds-with-soh\\n (*active-redirect-fds*))])\"\n \"(let* ([keep-fds (pack-fds-with-soh\\n (append (embed-active-fds)\\n (*active-redirect-fds*)))])\")\n\n ;; Close memfds in parent after child exits.\n ;; The child inherits its own fd table, so these parent fds are only\n ;; for bookkeeping. Closed after wait + sigchld-unblock.\n (patch-file! \"src/jsh/executor.sls\"\n \"(ffi-sigchld-unblock)\\n (if stopped?\"\n \"(ffi-sigchld-unblock)\\n (embed-cleanup-fds!)\\n (if stopped?\")\n ;; Clear decrypted plaintext cache and zero derived key on exit.\n ;; Patching run-exit-trap! covers all exit paths (EOF, exit builtin, -c, stdin).\n (patch-file! \"src/jsh/main.sls\"\n \"(define (run-exit-trap! env)\\n (let ([action (trap-get \\\"EXIT\\\")])\\n (when (and action (string? action))\\n (trap-set! \\\"EXIT\\\" 'default)\\n (execute-input action env))))\"\n \"(define (run-exit-trap! env)\\n (embed-clear-cache!)\\n (guard (e [#t (void)])\\n ((foreign-procedure \\\"jerboa_ssh_agent_stop\\\" () void)))\\n (let ([action (trap-get \\\"EXIT\\\")])\\n (when (and action (string? action))\\n (trap-set! \\\"EXIT\\\" 'default)\\n (execute-input action env))))\")\n\n) ;; end (let () ...) for embed patches\n\n;; --- Recording patches ---\n(display \"\\n--- Recording patches ---\\n\")\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n ;; 1. Add (jsh recorder) import to main.sls\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh embed))\"\n \"(jsh embed)\\n (jsh recorder))\")\n\n ;; 2. Patch REPL to emit input recording after line-edit\n ;; NOTE: After the 12ade70 commit, history-add! is wrapped in (when (*jsh-history-enabled*) ...)\n ;; so the patch target has an extra ) closing that wrapper.\n (patch-file! \"src/jsh/main.sls\"\n \"(history-add! expanded))\\n (when execute?\"\n \"(history-add! expanded))\\n (when (*recording?*)\\n (recorder-input! (string-append expanded \\\"\\\\n\\\")))\\n (when execute?\")\n\n ;; 3. Patch REPL to emit output recording — record prompt display\n ;; NOTE: After 12ade70, line-edit args are split across lines (Chez formatting)\n (patch-file! \"src/jsh/main.sls\"\n \"(let* ([input (line-edit\\n prompt-str\\n complete-fn\\n edit-mode)])\"\n \"(let* ([input (begin (when (*recording?*) (recorder-output! prompt-str))\\n (line-edit\\n prompt-str\\n complete-fn\\n edit-mode))])\")\n\n ;; 4. Patch execute-input to emit command and exit-status events\n ;; NOTE: After 12ade70, (let ([status...]) is indented 2 more spaces + includes (__exn\n (patch-file! \"src/jsh/main.sls\"\n \"(when execute?\\n (let ([status (guard (__exn\"\n \"(when execute?\\n (when (*recording?*)\\n (recorder-command! expanded (or (env-get env \\\"PWD\\\") \\\".\\\")))\\n (let ([status (guard (__exn\")\n\n ;; 5. Patch after execute to record exit status + duration\n ;; NOTE: After 12ade70, indented 31 spaces (was 29)\n (patch-file! \"src/jsh/main.sls\"\n \"(env-set-last-status! env status)\\n (process-traps! env)\"\n \"(env-set-last-status! env status)\\n (when (*recording?*)\\n (recorder-exit-status! status))\\n (process-traps! env)\")\n\n ;; 6. Patch run-exit-trap! to stop recording on exit\n (patch-file! \"src/jsh/main.sls\"\n \"(define (run-exit-trap! env)\\n (embed-clear-cache!)\"\n \"(define (run-exit-trap! env)\\n (when (*recording?*) (recorder-stop!))\\n (embed-clear-cache!)\")\n\n ;; 7. Add (jsh recorder) import to executor.sls\n (patch-file! \"src/jsh/executor.sls\"\n \"(jsh redirect) (jsh control)\"\n \"(jsh redirect) (jsh recorder) (jsh control)\")\n\n ;; 8. Patch SIGWINCH to record resize events\n (patch-file! \"src/jsh/main.sls\"\n \"[(string=? sig-name \\\"WINCH\\\") (%%void)]\"\n \"[(string=? sig-name \\\"WINCH\\\")\\n (when (*recording?*)\\n (recorder-resize! (ffi-terminal-columns 1) (ffi-terminal-rows 1)))\\n (%%void)]\")\n\n ;; 9. Add PTY output capture to recorder.sls — captures command stdout/stderr\n ;; 9a. Add capture state parameters after existing parameters\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define *recorder-flush-threshold* (make-parameter 32))\"\n \"(define *recorder-flush-threshold* (make-parameter 32))\\n (define *recorder-pty-master* (make-parameter -1))\\n (define *recorder-saved-fd1* (make-parameter -1))\\n (define *recorder-saved-fd2* (make-parameter -1))\\n (define *recorder-capture-thread* (make-parameter #f))\")\n\n ;; 9b. Add capture functions before recorder-elapsed\n ;; NOTE: recorder-start-capture! is called AFTER (*recording?* #t) so the\n ;; forked thread inherits *recording?*=#t and can use recorder-output!.\n ;; The capture loop calls recorder-flush! after each event because the\n ;; thread-local buffer would otherwise never reach threshold and be lost.\n ;; Uses ffi-bv-write (write()) not ffi-bv-send (send()) — PTY fds not sockets.\n ;; Sets PTY slave to cfmakeraw to eliminate line discipline interference.\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define (recorder-elapsed)\"\n \"(define (recorder-start-capture!)\\n (guard (e [#t (void)])\\n (when (= 1 (ffi-isatty 0))\\n (let-values ([(master slave) (ffi-pty-open)])\\n (let ([cols (ffi-terminal-columns 1)]\\n [rows (ffi-terminal-rows 1)])\\n (when (and (> cols 0) (> rows 0))\\n (ffi-pty-set-size slave cols rows)))\\n (ffi-set-pty-raw slave)\\n (let ([saved-1 (ffi-dup 1)]\\n [saved-2 (ffi-dup 2)])\\n (ffi-dup2 slave 1)\\n (ffi-dup2 slave 2)\\n (ffi-close-fd slave)\\n (ffi-set-nonblock master)\\n (*recorder-pty-master* master)\\n (*recorder-saved-fd1* saved-1)\\n (*recorder-saved-fd2* saved-2)\\n (let ([thread (fork-thread\\n (lambda ()\\n (recorder-capture-loop master saved-1 saved-2)))])\\n (*recorder-capture-thread* thread)))))))\\n (define (recorder-capture-loop master-fd real-fd1 real-fd2)\\n (let ([buf (make-bytevector 4096)])\\n (let loop ()\\n (let ([n (guard (e [#t -1])\\n (ffi-bv-read-nonblock master-fd buf 0 4096))])\\n (cond\\n [(> n 0)\\n (let ([data (make-bytevector n)])\\n (bytevector-copy! buf 0 data 0 n)\\n (guard (e [#t (void)])\\n (ffi-bv-write real-fd1 data 0 n))\\n (guard (e [#t (void)])\\n (let ([str (guard (e [#t\\n (let ([s (make-string n)])\\n (do ([i 0 (+ i 1)])\\n ((= i n) s)\\n (string-set! s i\\n (integer->char\\n (bytevector-u8-ref data i)))))])\\n (utf8->string data))])\\n (recorder-output! str)\\n (recorder-flush!))))\\n (loop)]\\n [(= n 0)\\n (ffi-nanosleep-us 500)\\n (loop)]\\n [else (void)])))))\\n (define (recorder-stop-capture!)\\n (let ([saved-1 (*recorder-saved-fd1*)]\\n [saved-2 (*recorder-saved-fd2*)]\\n [master (*recorder-pty-master*)])\\n (when (> saved-1 0)\\n (ffi-dup2 saved-1 1)\\n (ffi-close-fd saved-1)\\n (*recorder-saved-fd1* -1))\\n (when (> saved-2 0)\\n (ffi-dup2 saved-2 2)\\n (ffi-close-fd saved-2)\\n (*recorder-saved-fd2* -1))\\n (when (> master 0)\\n (guard (e [#t (void)])\\n (ffi-close-fd master))\\n (*recorder-pty-master* -1))\\n (*recorder-capture-thread* #f)))\\n (define (recorder-elapsed)\")\n\n ;; 9c. Add recorder-start-capture! in all 3 arms of recorder-start!\n ;; Each call patches the next unpatched occurrence (patch-file! finds first match)\n (patch-file! \"src/jsh/recorder.sls\"\n \"(*recorder-buffer-count* 0)\\n (when stream-url\"\n \"(*recorder-buffer-count* 0)\\n (recorder-start-capture!)\\n (when stream-url\")\n (patch-file! \"src/jsh/recorder.sls\"\n \"(*recorder-buffer-count* 0)\\n (when stream-url\"\n \"(*recorder-buffer-count* 0)\\n (recorder-start-capture!)\\n (when stream-url\")\n ;; Third arm has different indentation (10 spaces vs 13)\n (patch-file! \"src/jsh/recorder.sls\"\n \"(*recorder-buffer-count* 0)\\n (when stream-url\"\n \"(*recorder-buffer-count* 0)\\n (recorder-start-capture!)\\n (when stream-url\")\n\n ;; 9f. Add recorder-stop-capture! call in recorder-stop!\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define (recorder-stop!)\\n (when (*recording?*)\\n (recorder-flush!)\"\n \"(define (recorder-stop!)\\n (when (*recording?*)\\n (recorder-stop-capture!)\\n (recorder-flush!)\")\n\n ;; 10. Fix export-script in player.sls to show command output, not just commands\n (patch-file! \"src/jsh/player.sls\"\n \"(define (export-script filename)\"\n \"(define (strip-ansi str)\\n (let ([len (string-length str)])\\n (let loop ([i 0] [acc '()])\\n (cond\\n [(>= i len)\\n (list->string (reverse acc))]\\n [(and (char=? (string-ref str i) #\\\\x1b)\\n (< (+ i 1) len))\\n (let ([next (string-ref str (+ i 1))])\\n (cond\\n [(char=? next #\\\\[)\\n (let skip ([j (+ i 2)])\\n (cond\\n [(>= j len) (loop j acc)]\\n [(char-alphabetic? (string-ref str j))\\n (loop (+ j 1) acc)]\\n [else (skip (+ j 1))]))]\\n [else (loop (+ i 2) acc)]))]\\n [(char=? (string-ref str i) #\\\\return)\\n (loop (+ i 1) acc)]\\n [else\\n (loop (+ i 1) (cons (string-ref str i) acc))]))))\\n (define (extract-json-field data field)\\n (let ([pos (str-contains data (string-append \\\"\\\\\\\"\\\" field \\\"\\\\\\\":\\\\\\\"\\\" ))])\\n (if pos\\n (let ([start (+ pos (+ 4 (string-length field)))])\\n (let sloop ([i start])\\n (cond\\n [(>= i (string-length data)) (substring data start i)]\\n [(and (char=? (string-ref data i) #\\\\\\\\)\\n (< (+ i 1) (string-length data)))\\n (sloop (+ i 2))]\\n [(char=? (string-ref data i) #\\\\\\\") (substring data start i)]\\n [else (sloop (+ i 1))])))\\n #f)))\\n (define (export-script filename)\")\n\n ;; Rename old export-script so it's kept but unused, then add new one after it\n (patch-file! \"src/jsh/player.sls\"\n \"(define (export-script filename)\"\n \"(define (export-script-old filename)\")\n\n ;; Add new export-script that shows commands AND output, right before list-recordings\n (patch-file! \"src/jsh/player.sls\"\n \"(define (list-recordings)\"\n \"(define (export-script filename)\\n (if (not (file-exists? filename))\\n (begin\\n (fprintf (current-error-port) \\\"record export: ~a: No such file~n\\\" filename)\\n 1)\\n (call-with-input-file filename\\n (lambda (port)\\n (let ([header (get-line port)])\\n (fprintf (current-output-port) \\\"#!/bin/sh~n\\\")\\n (fprintf (current-output-port)\\n \\\"# Exported from: ~a~n~n\\\"\\n (path-strip-directory filename))\\n (let loop ([in-cmd? #f])\\n (let ([line (get-line port)])\\n (if (eof-object? line)\\n 0\\n (if (= 0 (string-length line))\\n (loop in-cmd?)\\n (let-values ([(ts typ data) (parse-event-line line)])\\n (cond\\n [(and ts (string=? typ \\\"c\\\"))\\n (let ([cmd (extract-json-field data \\\"cmd\\\")])\\n (when cmd\\n (fprintf (current-output-port) \\\"$ ~a~n\\\" cmd)))\\n (loop #t)]\\n [(and ts (string=? typ \\\"o\\\") in-cmd?)\\n (let ([clean (strip-ansi data)])\\n (when (> (string-length clean) 0)\\n (display clean (current-output-port))))\\n (loop #t)]\\n [(and ts (string=? typ \\\"x\\\"))\\n (let ([status (extract-json-field data \\\"status\\\")])\\n (when (and status (not (string=? status \\\"0\\\")))\\n (fprintf (current-output-port)\\n \\\"# exit: ~a~n\\\" status)))\\n (loop #f)]\\n [else (loop in-cmd?)]))))))))))) (define (list-recordings)\")\n\n ;; ---- Auto-Record with Encrypted Sessions ----\n\n ;; 10a. Rename console-logs → .jsh/logs in auto-generated recorder.sls and player.sls\n (let ()\n (define (str-replace-all s old new)\n (let ([old-len (string-length old)]\n [slen (string-length s)])\n (let loop ([start 0] [acc '()])\n (let ([rest (substring s start slen)])\n (let ([idx (string-find rest old)])\n (if idx\n (loop (+ start idx old-len)\n (cons new (cons (substring s start (+ start idx)) acc)))\n (apply string-append\n (reverse (cons rest acc)))))))))\n (define (replace-all-in-file! path old new)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [updated (str-replace-all content old new)])\n (unless (string=? content updated)\n (call-with-output-file path\n (lambda (p) (display updated p))\n 'replace)\n (printf \" Patched ~a (replace-all ~s → ~s)~n\" path old new))))\n (when (or force-rebuild? (compiled-this-session? \"src/jsh/recorder.sls\"))\n (replace-all-in-file! \"src/jsh/recorder.sls\" \"ensure-console-logs-dir!\" \"ensure-jsh-logs-dir!\")\n (replace-all-in-file! \"src/jsh/recorder.sls\" \"/console-logs\" \"/.jsh/logs\"))\n (when (or force-rebuild? (compiled-this-session? \"src/jsh/player.sls\"))\n (replace-all-in-file! \"src/jsh/player.sls\" \"/console-logs\" \"/.jsh/logs\")))\n\n ;; 10b. Fix ensure-jsh-logs-dir! to create parent ~/.jsh/ before ~/.jsh/logs/\n (patch-file! \"src/jsh/recorder.sls\"\n \"(unless (file-exists? dir)\\n (guard (__exn [#t ((lambda (e) #f) __exn)]) (mkdir dir)))\"\n \"(unless (file-exists? dir)\\n (let ([parent (string-append (or (getenv \\\"HOME\\\" #f) \\\"/tmp\\\") \\\"/.jsh\\\")])\\n (unless (file-exists? parent)\\n (guard (__exn [#t ((lambda (e) #f) __exn)]) (mkdir parent))))\\n (guard (__exn [#t ((lambda (e) #f) __exn)]) (mkdir dir)))\")\n\n ;; 11. Add encryption exports and embed-data import to recorder.sls\n (patch-file! \"src/jsh/recorder.sls\"\n \"recorder-duration! recorder-status)\"\n \"recorder-duration! recorder-status\\n *recorder-encrypt?* recorder-ensure-record-key!)\")\n (patch-file! \"src/jsh/recorder.sls\"\n \"(jsh recording-index)\"\n \"(jsh recording-index) (only (jsh embed-data) %record-pubkey)\")\n\n ;; 12. Add *recorder-encrypt?* parameter and in-memory encryption after capture thread.\n ;; Public key is baked into the binary at build time (%record-pubkey from embed-data).\n ;; Private key is in //embed/record.key — only accessible after ,unlock.\n ;; SECURITY: Recording data is NEVER written to disk as plaintext. All data is\n ;; accumulated in an in-memory string port and only written as encrypted .cast.enc.\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define *recorder-capture-thread* (make-parameter #f))\"\n (string-append\n \"(define *recorder-capture-thread* (make-parameter #f))\\n\"\n \" (define *recorder-encrypt?* (make-parameter #f))\\n\"\n \"\\n\"\n \" ;; Load public key from the binary. If present, enable encryption.\\n\"\n \" (define (recorder-ensure-record-key!)\\n\"\n \" (unless (*recorder-encrypt?*)\\n\"\n \" (when %record-pubkey\\n\"\n \" (*recorder-encrypt?* #t))))\\n\"\n \"\\n\"\n \" ;; Encrypt in-memory recording data -> .cast.enc using sealed-box.\\n\"\n \" ;; No plaintext .cast file is ever created on disk.\\n\"\n \" (define (recorder-encrypt-and-write! cast-data enc-path)\\n\"\n \" (when (and %record-pubkey (> (string-length cast-data) 0))\\n\"\n \" (let* ((plaintext (string->utf8 cast-data))\\n\"\n \" (encrypted (ffi-sealed-box-encrypt %record-pubkey plaintext)))\\n\"\n \" (when encrypted\\n\"\n \" (call-with-port\\n\"\n \" (open-file-output-port enc-path\\n\"\n \" (file-options no-fail) (buffer-mode block))\\n\"\n \" (lambda (p) (put-bytevector p encrypted)))\\n\"\n \" (chmod enc-path #o600)\\n\"\n \" enc-path))))\"))\n\n ;; 12b. Patch recorder-start! to use in-memory port — no plaintext .cast on disk.\n ;; Replace (open-output-file file) with (open-output-string) so all recording\n ;; data stays in RAM until encrypted and written as .cast.enc on stop.\n (patch-file! \"src/jsh/recorder.sls\"\n \"(let* ((file (or filename (generate-recording-filename)))\\n (port (open-output-file file)))\"\n \"(unless (*recorder-encrypt?*)\\n (error 'recorder-start! \\\"encryption key not available — refusing to record unencrypted\\\"))\\n (let* ((file (or filename (generate-recording-filename)))\\n (port (open-output-string)))\")\n\n ;; 12c. Patch generate-recording-filename to produce .cast.enc path\n (patch-file! \"src/jsh/recorder.sls\"\n \"(string-append dir \\\"/\\\" ts \\\".cast\\\"))\"\n \"(string-append dir \\\"/\\\" ts \\\".cast.enc\\\"))\")\n\n ;; 13. Modify recorder-output! to feed into SQLite index for searchability\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define (recorder-output! str) (recorder-emit! \\\"o\\\" str))\"\n \"(define (recorder-output! str)\\n (recorder-emit! \\\"o\\\" str)\\n ;; Feed output into the SQLite index for searchability\\n (guard (__exn [#t ((lambda (e) #f) __exn)])\\n (when (recording-db-ready?)\\n (recording-index-output! str))))\")\n\n ;; 14. Modify recorder-stop! to encrypt in-memory data and write .cast.enc\n ;; The port is an in-memory string port — get the accumulated data, encrypt,\n ;; and write directly to .cast.enc. No plaintext .cast file ever exists.\n (patch-file! \"src/jsh/recorder.sls\"\n \"(let ((port (*recorder-port*)))\\n (when port\\n (flush-output-port port)\\n (close-output-port port)))\"\n \"(let ((port (*recorder-port*)))\\n (when port\\n (let ((cast-data (get-output-string port)))\\n (when (and (*recorder-encrypt?*) (*recorder-file*))\\n (guard (__exn [#t ((lambda (e) #f) __exn)])\\n (recorder-encrypt-and-write! cast-data (*recorder-file*)))))))\")\n\n ;; 15. Add decrypt-cast-file export to player.sls\n (patch-file! \"src/jsh/player.sls\"\n \"session-report export-script list-recordings)\"\n \"session-report export-script list-recordings\\n decrypt-cast-file)\")\n\n ;; 16. (jsh ffi) import already present — Jerboa source imports :jsh/ffi\n\n ;; 17. Add decrypt-cast-file function to player.sls before strip-ansi\n ;; Private key is in //embed/record.key — requires ,unlock first.\n ;; Also need to add (jsh embed) import to player.sls for embed-file-ref.\n ;; Add (jsh embed) import to player.sls\n (patch-file! \"src/jsh/player.sls\"\n \"(std actor core)\"\n \"(std actor core)\\n (only (jsh embed) embed-file-ref embed-unlocked?)\")\n (patch-file! \"src/jsh/player.sls\"\n \"(define (strip-ansi str)\"\n (string-append\n \"(define (decrypt-cast-file enc-path)\\n\"\n \" (and (file-exists? enc-path)\\n\"\n \" (embed-unlocked?)\\n\"\n \" (let ([privkey (guard (e [#t #f]) (embed-file-ref \\\"record.key\\\"))])\\n\"\n \" (when privkey\\n\"\n \" (let* ([encrypted (call-with-port (open-file-input-port enc-path)\\n\"\n \" get-bytevector-all)]\\n\"\n \" [decrypted (ffi-sealed-box-decrypt privkey encrypted)])\\n\"\n \" (when decrypted\\n\"\n \" (let ([tmp (string-append enc-path \\\".tmp\\\")])\\n\"\n \" (call-with-port\\n\"\n \" (open-file-output-port tmp\\n\"\n \" (file-options no-fail) (buffer-mode block))\\n\"\n \" (lambda (p) (put-bytevector p decrypted)))\\n\"\n \" tmp)))))))\\n\"\n \" (define (strip-ansi str)\"))\n\n ;; 18. Modify find-cast-files in player.sls to include .cast.enc files\n (patch-file! \"src/jsh/player.sls\"\n \"(str-ends-with? f \\\".cast\\\")\"\n \"(or (str-ends-with? f \\\".cast\\\") (str-ends-with? f \\\".cast.enc\\\"))\")\n\n ;; 19. Add auto-record startup in main.sls repl function\n ;; Target: after ffi-termios-save in the repl function, before edit-mode let\n (patch-file! \"src/jsh/main.sls\"\n \"(when (= (ffi-isatty 0) 1) (ffi-termios-save 0 0))\\n (let ([edit-mode\"\n \"(when (= (ffi-isatty 0) 1) (ffi-termios-save 0 0))\\n ;; Auto-record every interactive session\\n (guard (__exn\\n [#t ((lambda (e)\\n (fprintf (current-error-port)\\n \\\"jsh: autorecord failed: ~a~n\\\"\\n (if (message-condition? e)\\n (condition-message e)\\n e)))\\n __exn)])\\n (recorder-ensure-record-key!)\\n (recorder-start!))\\n (let ([edit-mode\")\n\n ;; 20. Add explicit cleanup on interactive exit path\n (patch-file! \"src/jsh/main.sls\"\n \"(when (*recording?*) (recorder-stop!))\\n (embed-clear-cache!)\"\n \"(when (*recording?*) (recorder-stop!))\\n (guard (__exn [#t ((lambda (e) #f) __exn)]) (ffi-record-zero-key))\\n (embed-clear-cache!)\")\n\n ;; 21. Fix SIGWINCH: update recorder PTY size so child processes (top, vim, etc.) see correct terminal dimensions\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define (recorder-resize! cols rows)\\n (when (and (*recording?*) (not (*recording-paused?*)))\"\n \"(define (recorder-resize! cols rows)\\n ;; Update PTY size so child processes see the new terminal dimensions\\n (let ([master (*recorder-pty-master*)])\\n (when (> master 0)\\n (ffi-pty-set-size master cols rows)))\\n (when (and (*recording?*) (not (*recording-paused?*)))\")\n\n ;; 22. Ensure fds 0/1/2 are open at the top of main — when started via ffi-fork-exec\n ;; (e.g. ,server), all fds are closed before exec. Without this, code that runs\n ;; before the --server handler (init-smp!, harden-startup!, init-shell-env) crashes\n ;; because Chez Scheme's runtime can't write to stderr.\n (patch-file! \"src/jsh/main.sls\"\n \" (define (main . args)\\n (init-smp!)\"\n \" (define (main . args)\\n (ffi-ensure-std-fds)\\n (init-smp!)\")\n\n ;; 23. Set _git_branch variable before each prompt (reads .git/HEAD, no external commands)\n (patch-file! \"src/jsh/main.sls\"\n \"(let* ([ps1 (or (env-get env \\\"PS1\\\") \\\"$ \\\")])\"\n (string-append\n \";; Set _git_branch before prompt expansion\\n\"\n \" (let ([__branch (guard (__e [#t #f]) (git-branch-name))])\\n\"\n \" (if __branch\\n\"\n \" (env-set! env \\\"_git_branch\\\" (string-append \\\" (\\\" __branch \\\")\\\"))\\n\"\n \" (env-set! env \\\"_git_branch\\\" \\\"\\\")))\\n\"\n \" (let* ([ps1 (or (env-get env \\\"PS1\\\") \\\"$ \\\")])\"))\n\n ;; 23. Add (jsh prompt) import to main.sls for git-branch-name\n ;; NOTE: Must match text BEFORE mux patches add (jsh mux-server)\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh embed)\\n (jsh recorder))\"\n \"(jsh embed)\\n (only (jsh prompt) git-branch-name)\\n (jsh recorder))\")\n\n ;; ---- Prompt: \\g git branch escape is in local prompt.ss override ----\n\n ;; ---- Multiplexer (Phase 1) ----\n\n ;; 1. Add (jsh mux-server) and (jsh mux-client) imports to main.sls\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh recorder))\"\n \"(jsh recorder)\\n (jsh mux-server)\\n (jsh mux-client))\")\n\n ;; Add (jsh harden) import to main.sls (after mux imports)\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh mux-client))\"\n \"(jsh mux-client)\\n (jsh harden))\")\n\n ;; 1b. Add default #f entries for mux keys in parse-args hash\n (patch-file! \"src/jsh/main.sls\"\n \"(hash-put! ht 'args (list))\"\n \"(hash-put! ht 'args (list))\\n (hash-put! ht 'server #f)\\n (hash-put! ht 'attach #f)\\n (hash-put! ht 'list-servers #f)\\n (hash-put! ht 'mux-name #f)\\n (hash-put! ht 'listen-port #f)\\n (hash-put! ht 'mux-cert #f)\\n (hash-put! ht 'mux-key #f)\\n (hash-put! ht 'mux-ca #f)\\n (hash-put! ht 'mux-remote #f)\\n (hash-put! ht 'verbose #f)\")\n\n ;; 2. Add --server, --attach, --list-servers and TLS flags to parse-args\n (patch-file! \"src/jsh/main.sls\"\n \"[(string=? (car args) \\\"-c\\\")\"\n \"[(string=? (car args) \\\"--server\\\")\\n (hash-put! result 'server #t)\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--attach\\\")\\n (hash-put! result 'attach #t)\\n (loop (cdr args))]\\n [(or (string=? (car args) \\\"-A\\\"))\\n (hash-put! result 'attach #t)\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--list-servers\\\")\\n (hash-put! result 'list-servers #t)\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--name\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-name (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--listen-port\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'listen-port (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--cert\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-cert (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--key\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-key (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--ca\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-ca (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--remote\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-remote (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--verbose\\\")\\n (hash-put! result 'verbose #t)\\n (loop (cdr args))]\\n [(string=? (car args) \\\"-c\\\")\")\n\n ;; 3. Dispatch to server/client/list before the normal command/script/repl dispatch\n ;; Server password comes ONLY from _JSH_MUX_PW env var (set in-process by ,server).\n ;; --password is NEVER accepted on the CLI (would leak via ps/pgrep).\n ;; Attach password is NEVER accepted either — the protocol prompts interactively.\n (patch-file! \"src/jsh/main.sls\"\n \"(cond\\n [command\"\n \"(cond\\n [(hash-ref args-hash 'server)\\n (when (hash-ref args-hash 'verbose)\\n (let ([logpath (string-append (or (getenv \\\"HOME\\\") \\\"/tmp\\\") \\\"/.jsh/mux-server.log\\\")])\\n (putenv \\\"MUX_SERVER_DEBUG\\\" logpath)\\n (fprintf (current-error-port) \\\" verbose log: ~a~n\\\" logpath)))\\n (guard (e [#t (exit 1)])\\n (let* ([name (or (hash-ref args-hash 'mux-name) \\\"default\\\")]\\n [pw (let ([v (getenv \\\"_JSH_MUX_PW\\\")])\\n (putenv \\\"_JSH_MUX_PW\\\" \\\"\\\")\\n (and v (> (string-length v) 0) v))]\\n [listen-port (hash-ref args-hash 'listen-port)]\\n [cert (hash-ref args-hash 'mux-cert)]\\n [key (hash-ref args-hash 'mux-key)]\\n [ca (hash-ref args-hash 'mux-ca)])\\n (if listen-port\\n (let ([port (string->number listen-port)])\\n (if ca\\n (mux-server-start-tcp name port cert key pw ca)\\n (mux-server-start-tcp name port cert key pw)))\\n (if pw\\n (mux-server-start name pw)\\n (mux-server-start name)))))\\n (exit 0)]\\n [(hash-ref args-hash 'list-servers)\\n (mux-list-servers)\\n (exit 0)]\\n [(hash-ref args-hash 'attach)\\n (guard (e [#t (exit 1)])\\n (let ([name (or (hash-ref args-hash 'mux-name) \\\"default\\\")]\\n [remote (hash-ref args-hash 'mux-remote)])\\n (if remote\\n (let* ([parts (let ([colon (let loop ([i (- (string-length remote) 1)])\\n (cond [(< i 0) #f]\\n [(char=? (string-ref remote i) #\\\\:) i]\\n [else (loop (- i 1))]))])\\n (if colon\\n (cons (substring remote 0 colon)\\n (string->number (substring remote (+ colon 1) (string-length remote))))\\n (cons remote 443)))]\\n [host (car parts)]\\n [port (cdr parts)])\\n (mux-client-attach-remote host port))\\n (mux-client-attach name))))\\n (exit 0)]\\n [command\")\n\n ;; 4. Add \"mux\" builtin to register-late-builtins! (after embed-fd)\n ;; The function ends with embed-fd's closing: 1)))))))\n ;; We change the 7th ) to 6 parens (close embed-fd), add mux builtin, then close the function.\n (patch-file! \"src/jsh/main.sls\"\n \" 1)))))))\\n (define (init-shell-env args-hash)\"\n (string-append\n \" 1))))))\\n\"\n \" ;; ---- mux — multiplexer control ----\\n\"\n \" (builtin-register! \\\"mux\\\"\\n\"\n \" (lambda (args env)\\n\"\n \" (if (null? args)\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"usage: mux <command> [options]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux server [--name NAME]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux server -p PORT[,PORT...] [--name NAME]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux attach [--name NAME]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux attach HOST[:PORT]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux list~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"~nCerts/keys come from //embed/keys/. Passwords are prompted~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"interactively (never passed on the command line).~n\\\")\\n\"\n \" 2)\\n\"\n \" (let ([subcmd (car args)]\\n\"\n \" [rest (cdr args)])\\n\"\n \" (cond\\n\"\n \" [(string=? subcmd \\\"server\\\")\\n\"\n \" (mux-cmd-server rest)]\\n\"\n \" [(string=? subcmd \\\"attach\\\")\\n\"\n \" (mux-cmd-attach rest)]\\n\"\n \" [(string=? subcmd \\\"list\\\")\\n\"\n \" (mux-list-servers) 0]\\n\"\n \" [else\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux: unknown subcommand '~a'~n\\\" subcmd) 2]))))))\\n\"\n \" ;; Parse --flag VALUE pairs from an arg list into an alist.\\n\"\n \" ;; Bare --flag (no value or next arg is also a flag) is stored as (flag . #t).\\n\"\n \" ;; -p PORT is shorthand for --listen PORT.\\n\"\n \" ;; Positional args with '.' or ':' are treated as remote targets.\\n\"\n \" (define (mux-parse-flags args)\\n\"\n \" (let loop ([args args] [result '()])\\n\"\n \" (cond\\n\"\n \" [(null? args) result]\\n\"\n \" [(and (string=? (car args) \\\"-p\\\") (pair? (cdr args)))\\n\"\n \" (loop (cddr args) (cons (cons \\\"listen\\\" (cadr args)) result))]\\n\"\n \" [(and (> (string-length (car args)) 2)\\n\"\n \" (char=? (string-ref (car args) 0) #\\\\-)\\n\"\n \" (char=? (string-ref (car args) 1) #\\\\-))\\n\"\n \" (let ([flag (substring (car args) 2 (string-length (car args)))])\\n\"\n \" (if (and (pair? (cdr args))\\n\"\n \" (or (= (string-length (cadr args)) 0)\\n\"\n \" (not (char=? (string-ref (cadr args) 0) #\\\\-))))\\n\"\n \" (loop (cddr args) (cons (cons flag (cadr args)) result))\\n\"\n \" (loop (cdr args) (cons (cons flag #t) result))))]\\n\"\n \" [(let ([a (car args)])\\n\"\n \" (or (string-contains a \\\".\\\") (string-contains a \\\":\\\")))\\n\"\n \" (loop (cdr args) (cons (cons \\\"remote\\\" (car args)) result))]\\n\"\n \" [else (loop (cdr args) result)])))\\n\"\n \" (define (mux-flag-ref flags key default)\\n\"\n \" (let ([pair (assoc key flags)])\\n\"\n \" (if pair (cdr pair) default)))\\n\"\n \" ;; Resolve //embed/ path to /proc/self/fd/N via memfd, or return path as-is\\n\"\n \" (define (resolve-embed-path path who)\\n\"\n \" (if (embed-path? path)\\n\"\n \" (let ([fd-path (embed-file->fd-path path)])\\n\"\n \" (unless fd-path\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux ~a: embedded file not found: ~a~n\\\" who path)\\n\"\n \" (error 'mux \\\"embedded file not found\\\" path))\\n\"\n \" fd-path)\\n\"\n \" path))\\n\"\n \" (define (mux-parse-ports str)\\n\"\n \" (let loop ([s str] [acc '()])\\n\"\n \" (let ([comma (string-contains s \\\",\\\")])\\n\"\n \" (if comma\\n\"\n \" (loop (substring s (+ comma 1) (string-length s))\\n\"\n \" (cons (substring s 0 comma) acc))\\n\"\n \" (reverse (cons s acc))))))\\n\"\n \" (define (mux-validate-port str)\\n\"\n \" (let ([p (string->number str)])\\n\"\n \" (if (and p (fixnum? p) (> p 0) (< p 65536))\\n\"\n \" p\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux server: invalid port '~a'~n\\\" str)\\n\"\n \" (error 'mux \\\"invalid port\\\" str)))))\\n\"\n \" (define (mux-cmd-server args)\\n\"\n \" (let* ([flags (mux-parse-flags args)]\\n\"\n \" [name (mux-flag-ref flags \\\"name\\\" \\\"default\\\")]\\n\"\n \" [listen-port (mux-flag-ref flags \\\"listen\\\" #f)]\\n\"\n \" [cert-path \\\"//embed/keys/cert.pem\\\"]\\n\"\n \" [key-path \\\"//embed/keys/key.pem\\\"])\\n\"\n \" ;; Password is never read from CLI (leaks via ps). When auth is\\n\"\n \" ;; required, _JSH_MUX_PW (set in-process by ,server) is consumed;\\n\"\n \" ;; otherwise the server runs unauthenticated.\\n\"\n \" (let ([pw (let ([v (getenv \\\"_JSH_MUX_PW\\\")])\\n\"\n \" (putenv \\\"_JSH_MUX_PW\\\" \\\"\\\")\\n\"\n \" (and v (> (string-length v) 0) v))])\\n\"\n \" (cond\\n\"\n \" [listen-port\\n\"\n \" (let ([ports (map mux-validate-port (mux-parse-ports listen-port))])\\n\"\n \" (let ([real-cert (resolve-embed-path cert-path \\\"server\\\")]\\n\"\n \" [real-key (resolve-embed-path key-path \\\"server\\\")])\\n\"\n \" (guard (e [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux server: ~a~n\\\"\\n\"\n \" (if (message-condition? e) (condition-message e) (format \\\"~a\\\" e)))\\n\"\n \" 1])\\n\"\n \" (mux-server-start-tcp name ports real-cert real-key pw)\\n\"\n \" 0)))]\\n\"\n \" [else\\n\"\n \" (guard (e [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux server: ~a~n\\\"\\n\"\n \" (if (message-condition? e) (condition-message e) (format \\\"~a\\\" e)))\\n\"\n \" 1])\\n\"\n \" (if pw\\n\"\n \" (mux-server-start name pw)\\n\"\n \" (mux-server-start name))\\n\"\n \" 0)]))))\\n\"\n \" (define (mux-cmd-attach args)\\n\"\n \" (let* ([flags (mux-parse-flags args)]\\n\"\n \" [name (mux-flag-ref flags \\\"name\\\" \\\"default\\\")]\\n\"\n \" [remote (mux-flag-ref flags \\\"remote\\\" #f)]\\n\"\n \" [cert-path \\\"//embed/keys/cert.pem\\\"]\\n\"\n \" [key-path \\\"//embed/keys/key.pem\\\"]\\n\"\n \" [mtls? (assoc \\\"mtls\\\" flags)]\\n\"\n \" [ws? (assoc \\\"ws\\\" flags)])\\n\"\n \" ;; Password is NEVER taken from CLI (leaks via ps). The client\\n\"\n \" ;; prompts interactively when the server requests auth.\\n\"\n \" (cond\\n\"\n \" [remote\\n\"\n \" (let-values ([(host port) (mux-parse-host-port remote 443)])\\n\"\n \" (guard (e [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux attach: ~a~n\\\"\\n\"\n \" (if (message-condition? e) (condition-message e) (format \\\"~a\\\" e)))\\n\"\n \" 1])\\n\"\n \" (cond\\n\"\n \" [(and mtls? ws?)\\n\"\n \" (let ([rc (resolve-embed-path cert-path \\\"attach\\\")]\\n\"\n \" [rk (resolve-embed-path key-path \\\"attach\\\")])\\n\"\n \" (mux-client-attach-remote-ws host port rc rk rc #f))]\\n\"\n \" [mtls?\\n\"\n \" (let ([rc (resolve-embed-path cert-path \\\"attach\\\")]\\n\"\n \" [rk (resolve-embed-path key-path \\\"attach\\\")])\\n\"\n \" (mux-client-attach-remote-mtls host port rc rk rc #f))]\\n\"\n \" [ws?\\n\"\n \" (mux-client-attach-remote-ws host port #f)]\\n\"\n \" [else\\n\"\n \" (mux-client-attach-remote host port)])\\n\"\n \" 0))]\\n\"\n \" [else\\n\"\n \" (guard (e [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux attach: ~a~n\\\"\\n\"\n \" (if (message-condition? e) (condition-message e) (format \\\"~a\\\" e)))\\n\"\n \" 1])\\n\"\n \" (mux-client-attach name)\\n\"\n \" 0)])))\\n\"\n \" ;; Parse \\\"host:port\\\" or \\\"host\\\" (defaulting port)\\n\"\n \" (define (mux-parse-host-port str default-port)\\n\"\n \" (let ([colon-pos (let loop ([i (- (string-length str) 1)])\\n\"\n \" (cond\\n\"\n \" [(< i 0) #f]\\n\"\n \" [(char=? (string-ref str i) #\\\\:) i]\\n\"\n \" [else (loop (- i 1))]))])\\n\"\n \" (if colon-pos\\n\"\n \" (let ([host (substring str 0 colon-pos)]\\n\"\n \" [port-str (substring str (+ colon-pos 1) (string-length str))])\\n\"\n \" (let ([port (string->number port-str)])\\n\"\n \" (if (and port (fixnum? port) (> port 0) (< port 65536))\\n\"\n \" (values host port)\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: invalid port '~a' in '~a'~n\\\" port-str str)\\n\"\n \" (error 'mux \\\"invalid port\\\")))))\\n\"\n \" (values str default-port))))\\n\"\n \" (define (init-shell-env args-hash)\"))\n\n) ;; end (let () ...) for recording + mux patches\n\n;; --- mux gen-certs subcommand ---\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n;; Add \"gen-certs\" to mux help text\n(patch-file! \"src/jsh/main.sls\"\n \" \\\" mux list~n\\\")\"\n (string-append\n \" \\\" mux list~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux gen-certs [--ip IP] [--days N] [--cert PATH] [--key PATH]~n\\\")\"))\n\n;; Add \"gen-certs\" dispatch case\n(patch-file! \"src/jsh/main.sls\"\n \" [(string=? subcmd \\\"list\\\")\\n (mux-list-servers) 0]\"\n (string-append\n \" [(string=? subcmd \\\"list\\\")\\n\"\n \" (mux-list-servers) 0]\\n\"\n \" [(string=? subcmd \\\"gen-certs\\\")\\n\"\n \" (mux-cmd-gen-certs rest)]\"))\n\n;; Add mux-cmd-gen-certs handler (before mux-parse-host-port)\n(patch-file! \"src/jsh/main.sls\"\n \" ;; Parse \\\"host:port\\\" or \\\"host\\\" (defaulting port)\"\n (string-append\n \" ;; --- mux gen-certs: generate self-signed TLS certs ---\\n\"\n \" (define c-x509-generate\\n\"\n \" (foreign-procedure \\\"jerboa_x509_generate_self_signed\\\"\\n\"\n \" (u8* size_t int u8* size_t u8* size_t) int))\\n\"\n \" (define c-x509-fingerprint\\n\"\n \" (foreign-procedure \\\"jerboa_x509_cert_fingerprint\\\"\\n\"\n \" (u8* size_t u8* size_t) int))\\n\"\n \" (define c-last-error\\n\"\n \" (foreign-procedure \\\"jerboa_last_error\\\" (u8* size_t) size_t))\\n\"\n \" (define (x509-last-error)\\n\"\n \" (let ([buf (make-bytevector 1024)])\\n\"\n \" (let ([len (c-last-error buf 1024)])\\n\"\n \" (if (> len 0)\\n\"\n \" (utf8->string (let ([out (make-bytevector (min len 1023))])\\n\"\n \" (bytevector-copy! buf 0 out 0 (min len 1023)) out))\\n\"\n \" \\\"unknown error\\\"))))\\n\"\n \" (define (x509-fingerprint cert-path)\\n\"\n \" (let ([cert-bv (string->utf8 cert-path)]\\n\"\n \" [out (make-bytevector 32)])\\n\"\n \" (let ([rc (c-x509-fingerprint cert-bv (bytevector-length cert-bv) out 32)])\\n\"\n \" (if (< rc 0) \\\"??\\\" (bytevector->hex-string out)))))\\n\"\n \" (define (bytevector->hex-string bv)\\n\"\n \" (let* ([len (bytevector-length bv)]\\n\"\n \" [out (make-string (* len 2))])\\n\"\n \" (do ([i 0 (+ i 1)]) ((= i len) out)\\n\"\n \" (let* ([b (bytevector-u8-ref bv i)]\\n\"\n \" [hi (bitwise-arithmetic-shift-right b 4)]\\n\"\n \" [lo (bitwise-and b #xf)])\\n\"\n \" (string-set! out (* i 2) (string-ref \\\"0123456789abcdef\\\" hi))\\n\"\n \" (string-set! out (+ (* i 2) 1) (string-ref \\\"0123456789abcdef\\\" lo))))))\\n\"\n \" (define (x509-ensure-parent-dirs! path)\\n\"\n \" (let ([idx (let loop ([i (- (string-length path) 1)])\\n\"\n \" (cond [(< i 0) #f]\\n\"\n \" [(char=? (string-ref path i) #\\\\/) i]\\n\"\n \" [else (loop (- i 1))]))])\\n\"\n \" (when idx\\n\"\n \" (let ([dir (substring path 0 idx)])\\n\"\n \" (unless (or (string=? dir \\\"\\\") (file-exists? dir))\\n\"\n \" (guard (e [#t (void)]) (mkdir dir)))))))\\n\"\n \" (define (mux-cmd-gen-certs args)\\n\"\n \" (let* ([flags (mux-parse-flags args)]\\n\"\n \" [ip (mux-flag-ref flags \\\"ip\\\" \\\"0.0.0.0\\\")]\\n\"\n \" [days-str (mux-flag-ref flags \\\"days\\\" \\\"365\\\")]\\n\"\n \" [cert-path (mux-flag-ref flags \\\"cert\\\"\\n\"\n \" (string-append (or (getenv \\\"HOME\\\") \\\".\\\") \\\"/.embed/keys/cert.pem\\\"))]\\n\"\n \" [key-path (mux-flag-ref flags \\\"key\\\"\\n\"\n \" (string-append (or (getenv \\\"HOME\\\") \\\".\\\") \\\"/.embed/keys/key.pem\\\"))])\\n\"\n \" (let ([days (or (string->number days-str) 365)])\\n\"\n \" (x509-ensure-parent-dirs! cert-path)\\n\"\n \" (x509-ensure-parent-dirs! key-path)\\n\"\n \" (let* ([ip-bv (string->utf8 ip)]\\n\"\n \" [cert-bv (string->utf8 cert-path)]\\n\"\n \" [key-bv (string->utf8 key-path)]\\n\"\n \" [rc (c-x509-generate ip-bv (bytevector-length ip-bv)\\n\"\n \" days cert-bv (bytevector-length cert-bv)\\n\"\n \" key-bv (bytevector-length key-bv))])\\n\"\n \" (cond\\n\"\n \" [(< rc 0)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux gen-certs: ~a~n\\\" (x509-last-error))\\n\"\n \" 1]\\n\"\n \" [else\\n\"\n \" (let ([fp (x509-fingerprint cert-path)])\\n\"\n \" (printf \\\"Certificate generated:~n\\\")\\n\"\n \" (printf \\\" cert: ~a~n\\\" cert-path)\\n\"\n \" (printf \\\" key: ~a~n\\\" key-path)\\n\"\n \" (printf \\\" ip: ~a~n\\\" ip)\\n\"\n \" (printf \\\" days: ~a~n\\\" days)\\n\"\n \" (printf \\\" fingerprint: ~a~n\\\" fp)\\n\"\n \" 0)])))))\\n\"\n \" ;; Parse \\\"host:port\\\" or \\\"host\\\" (defaulting port)\"))\n\n) ;; end (let () ...) for mux gen-certs patches\n\n;; --- Security hardening patches ---\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n ;; Replace all occurrences of a string in a file\n (define (patch-file-all! path old new)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [olen (string-length old)]\n [nlen (string-length new)])\n (let loop ([i 0] [result \"\"])\n (cond\n ((> (+ i olen) (string-length content))\n (let ([final (string-append result (substring content i (string-length content)))])\n (call-with-output-file path\n (lambda (p) (display final p))\n 'replace)))\n ((string=? (substring content i (+ i olen)) old)\n (loop (+ i olen) (string-append result new)))\n (else\n (loop (+ i 1) (string-append result (substring content i (+ i 1)))))))))\n\n (display \"\\n--- Security hardening patches ---\\n\")\n\n ;; 1. Randomize process substitution FIFO names with CSPRNG\n ;; Replace predictable /tmp/jsh-procsub-{pid}-{counter} with random hex\n (patch-file! \"src/jsh/expander.sls\"\n (string-append\n \"(define (make-procsub-fifo!)\\n\"\n \" (set! *procsub-counter* (+ *procsub-counter* 1))\\n\"\n \" (let ([path (string-append\\n\"\n \" \\\"/tmp/jsh-procsub-\\\"\\n\"\n \" (number->string (ffi-getpid))\\n\"\n \" \\\"-\\\"\\n\"\n \" (number->string *procsub-counter*))])\\n\"\n \" (let ([rc (ffi-mkfifo path 384)])\\n\"\n \" (when (< rc 0) (error 'jerboa \\\"mkfifo failed\\\" path))\\n\"\n \" path)))\")\n (string-append\n \"(define (random-hex-string n)\\n\"\n \" ;; Generate a hex string of 2*n characters from n random bytes\\n\"\n \" (let ([bv (make-bytevector n)])\\n\"\n \" (if (ffi-embed-random-bytes bv)\\n\"\n \" (let ([out (make-string (* n 2))])\\n\"\n \" (let loop ([i 0])\\n\"\n \" (if (>= i n) out\\n\"\n \" (let* ([b (bytevector-u8-ref bv i)]\\n\"\n \" [hi (bitwise-arithmetic-shift-right b 4)]\\n\"\n \" [lo (bitwise-and b #xf)])\\n\"\n \" (string-set! out (* i 2)\\n\"\n \" (string-ref \\\"0123456789abcdef\\\" hi))\\n\"\n \" (string-set! out (+ (* i 2) 1)\\n\"\n \" (string-ref \\\"0123456789abcdef\\\" lo))\\n\"\n \" (loop (+ i 1))))))\\n\"\n \" ;; Fallback if /dev/urandom unavailable\\n\"\n \" (string-append (number->string (ffi-getpid)) \\\"-\\\"\\n\"\n \" (number->string *procsub-counter*)))))\\n\"\n \" (define (make-procsub-fifo!)\\n\"\n \" (set! *procsub-counter* (+ *procsub-counter* 1))\\n\"\n \" (let ([path (string-append\\n\"\n \" \\\"/tmp/jsh-procsub-\\\"\\n\"\n \" (random-hex-string 16))])\\n\"\n \" (let ([rc (ffi-mkfifo path #o600)])\\n\"\n \" (when (< rc 0) (error 'jerboa \\\"mkfifo failed\\\" path))\\n\"\n \" path)))\"))\n\n ;; 2. Add validate-file-path to util.sls for null-byte rejection\n (patch-file! \"src/jsh/util.sls\"\n \"shell-display shell-display-raw file-regular?\"\n (string-append\n \"shell-display shell-display-raw validate-file-path file-regular?\"))\n\n ;; 3. Add null-byte validation to redirect-fd-to-file!\n (patch-file! \"src/jsh/redirect.sls\"\n \"(define (redirect-fd-to-file! fd filename flags mode)\\n (let ([raw-fd (ffi-open-raw filename flags mode)])\"\n \"(define (redirect-fd-to-file! fd filename flags mode)\\n (validate-file-path filename 'redirect)\\n (let ([raw-fd (ffi-open-raw filename flags mode)])\")\n\n ;; 4. Add null-byte validation to source builtin in main.sls\n (patch-file! \"src/jsh/main.sls\"\n \"(let* ([filename (car args)])\\n (let* ([filepath (if (string-contains?\"\n \"(let* ([filename (validate-file-path (car args) 'source)])\\n (let* ([filepath (if (string-contains?\")\n\n ;; 5. Expand sandbox import in main.sls + add conditions import\n ;; Use (except ...) to avoid duplicate definitions — (jsh functions) already\n ;; exports the exception types that (jsh conditions) re-exports.\n (patch-file! \"src/jsh/main.sls\"\n \"(only (jsh sandbox) *current-jsh-env*)\"\n \"(jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\")\n\n ;; 6. Add sandbox builtin registration in main.sls\n (patch-file! \"src/jsh/main.sls\"\n \" ;; Embed builtins\"\n (string-append\n \" ;; Sandbox builtin — runs commands under Landlock + seccomp + timeout\\n\"\n \" (builtin-register! \\\"sandbox\\\"\\n\"\n \" (lambda (args env)\\n\"\n \" (if (null? args)\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: usage: sandbox [opts] -c 'cmd' | script~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -r PATH allow read -w PATH allow write~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -x PATH allow exec -t MS timeout~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" --net allow network --no-net deny network~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -c CMD run command~n\\\")\\n\"\n \" 2)\\n\"\n \" (guard (exn\\n\"\n \" [(jsh-sandbox-error? exn)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: ~a: ~a~n\\\"\\n\"\n \" (jsh-sandbox-error-phase exn)\\n\"\n \" (jsh-sandbox-error-detail exn))\\n\"\n \" 1]\\n\"\n \" [(jsh-path-security-error? exn)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: ~a: ~a~n\\\"\\n\"\n \" (jsh-path-security-error-path exn)\\n\"\n \" (jsh-path-security-error-reason exn))\\n\"\n \" 1]\\n\"\n \" [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: ~a~n\\\"\\n\"\n \" (if (message-condition? exn)\\n\"\n \" (condition-message exn)\\n\"\n \" (format \\\"~a\\\" exn)))\\n\"\n \" 1])\\n\"\n \" (let* ([parsed (parse-sb-args args)]\\n\"\n \" [script (sb-parsed-script parsed)]\\n\"\n \" [cmd (sb-parsed-cmd parsed)]\\n\"\n \" [opts (sb-parsed-opts parsed)])\\n\"\n \" (cond\\n\"\n \" [cmd\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda () (run-cmd cmd)))]\\n\"\n \" [script\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda () (run-script script)))]\\n\"\n \" [else\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: no command or script specified~n\\\")\\n\"\n \" 2]))))))\\n\"\n \" ;; Embed builtins\"))\n\n ;; 7-9. Sandbox patches are applied directly to src/jsh/sandbox.sls\n ;; (hand-written file, not auto-generated)\n\n ;; ========== Round 2: Additional security hardening ==========\n\n ;; 10. Seccomp FFI bindings are in ffi.sls directly (hand-written file)\n\n ;; 11-14. Sandbox, seccomp, safe-eval patches applied directly to src/jsh/sandbox.sls\n ;; (hand-written file, not auto-generated)\n\n ;; 12. Guardian-based FD/FIFO leak detection in main.sls\n ;; Track FD opens/closes and warn on leaks in the REPL loop\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\"\n (string-append\n \"(jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\\n\"\n \" ;; Leak detection\\n\"))\n\n (patch-file! \"src/jsh/main.sls\"\n \"(define (main args)\"\n (string-append\n \";; ========== Guardian-based resource leak detection ==========\\n\"\n \" (define *fd-guardian* (make-guardian))\\n\"\n \" (define *fd-tracker* (make-hashtable equal-hash equal?))\\n\\n\"\n \" ;; Register an FD with the guardian for leak tracking.\\n\"\n \" ;; info: descriptive string (e.g., \\\"redirect /tmp/foo\\\" or \\\"procsub FIFO\\\")\\n\"\n \" (define (track-fd! fd info)\\n\"\n \" (let ([token (cons fd info)])\\n\"\n \" (hashtable-set! *fd-tracker* fd token)\\n\"\n \" (*fd-guardian* token)))\\n\\n\"\n \" ;; Mark an FD as properly closed (suppress guardian warning)\\n\"\n \" (define (untrack-fd! fd)\\n\"\n \" (hashtable-delete! *fd-tracker* fd))\\n\\n\"\n \" ;; Poll the guardian for leaked FDs and log warnings\\n\"\n \" (define (poll-fd-leaks!)\\n\"\n \" (let loop ([leaked 0])\\n\"\n \" (let ([token (*fd-guardian*)])\\n\"\n \" (if token\\n\"\n \" (let ([fd (car token)] [info (cdr token)])\\n\"\n \" (when (hashtable-contains? *fd-tracker* fd)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: warning: leaked fd ~a (~a)~n\\\" fd info)\\n\"\n \" (hashtable-delete! *fd-tracker* fd))\\n\"\n \" (loop (+ leaked 1)))\\n\"\n \" leaked))))\\n\\n\"\n \" (define (main args)\"))\n\n ;; 13. SQL safety validation is applied directly to src/jsh/recording-index.sls\n ;; (hand-written file, not auto-generated — no patch needed here)\n\n ;; 14. Add --eval flag to sandbox builtin for restricted eval\n ;; Safe eval with a minimal binding allowlist\n (patch-file! \"src/jsh/main.sls\"\n \" (fprintf (current-error-port)\\n \\\" -c CMD run command~n\\\")\"\n (string-append\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -c CMD run command~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -e EXPR eval expression in restricted env~n\\\")\"))\n\n ;; sandbox.sls -e flag, safe-eval, and seccomp are applied directly\n ;; (hand-written file, not auto-generated — no patch needed here)\n\n ;; Wire -e flag in the sandbox builtin (main.sls)\n (patch-file! \"src/jsh/main.sls\"\n \" (cond\\n [cmd\\n (jsh-sandbox-run opts\\n (lambda () (run-cmd cmd)))]\\n [script\\n (jsh-sandbox-run opts\\n (lambda () (run-script script)))]\\n [else\"\n (string-append\n \" (cond\\n\"\n \" [(eq? script 'eval)\\n\"\n \" ;; Safe eval mode: evaluate in restricted environment\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda ()\\n\"\n \" (let ([result (jsh-safe-eval cmd)])\\n\"\n \" (when result (fprintf (current-output-port) \\\"~a~n\\\" result))\\n\"\n \" 0)))]\\n\"\n \" [cmd\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda () (run-cmd cmd)))]\\n\"\n \" [script\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda () (run-script script)))]\\n\"\n \" [else\"))\n\n ;; jsh-safe-eval export is in sandbox.sls directly (hand-written)\n\n (display \" Security patches applied (round 1 + round 2)\\n\")\n) ;; end security patches\n\n;; --- Stdlib integration patches ---\n;; Integrate jerboa stdlib modules: terminal, custodian, profile, memoize, config, fmt\n(display \"\\n--- Post-build: Applying stdlib integration patches ---\\n\")\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n ;; --- 1. Terminal stdlib in lineedit.sls ---\n ;; Add (std misc terminal) import\n (patch-file! \"src/jsh/lineedit.sls\"\n \"(except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\"\n \"(only (std misc terminal) cursor-up cursor-down cursor-forward cursor-back\\n clear-screen clear-line clear-to-end)\\n (except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\")\n ;; Replace hand-rolled ANSI escape functions with stdlib calls\n (patch-file! \"src/jsh/lineedit.sls\"\n (string-append\n \"(define (term-clear-to-eol port)\\n\"\n \" (display (string-append ESC-STR \\\"[K\\\") port))\\n\"\n \" (define (term-clear-line port)\\n\"\n \" (display (string-append \\\"\\\\r\\\" ESC-STR \\\"[K\\\") port))\\n\"\n \" (define (term-clear-screen port)\\n\"\n \" (display (string-append ESC-STR \\\"[H\\\" ESC-STR \\\"[2J\\\") port))\\n\"\n \" (define (term-move-up n port)\\n\"\n \" (when (> n 0) (fprintf port \\\"~a[~aA\\\" ESC-STR n)))\\n\"\n \" (define (term-move-down n port)\\n\"\n \" (when (> n 0) (fprintf port \\\"~a[~aB\\\" ESC-STR n)))\\n\"\n \" (define (term-cursor-forward n port)\\n\"\n \" (when (> n 0) (fprintf port \\\"~a[~aC\\\" ESC-STR n)))\\n\"\n \" (define (term-cursor-back n port)\\n\"\n \" (when (> n 0) (fprintf port \\\"~a[~aD\\\" ESC-STR n)))\")\n (string-append\n \"(define (term-clear-to-eol port)\\n\"\n \" (parameterize ([current-output-port port]) (clear-to-end)))\\n\"\n \" (define (term-clear-line port)\\n\"\n \" (parameterize ([current-output-port port]) (clear-line)))\\n\"\n \" (define (term-clear-screen port)\\n\"\n \" (parameterize ([current-output-port port]) (clear-screen)))\\n\"\n \" (define (term-move-up n port)\\n\"\n \" (when (> n 0) (parameterize ([current-output-port port]) (cursor-up n))))\\n\"\n \" (define (term-move-down n port)\\n\"\n \" (when (> n 0) (parameterize ([current-output-port port]) (cursor-down n))))\\n\"\n \" (define (term-cursor-forward n port)\\n\"\n \" (when (> n 0) (parameterize ([current-output-port port]) (cursor-forward n))))\\n\"\n \" (define (term-cursor-back n port)\\n\"\n \" (when (> n 0) (parameterize ([current-output-port port]) (cursor-back n))))\"))\n\n ;; --- 2. Custodian safety net in pipeline.sls ---\n ;; Add (std misc custodian) import\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\"\n \"(only (std misc custodian) make-custodian custodian-register!\\n custodian-shutdown-all)\\n (except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\")\n ;; Add *pipeline-custodian* parameter and custodian-aware make-pipes\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(define (make-pipes n)\"\n (string-append\n \"(define *pipeline-custodian* (make-parameter #f))\\n\"\n \" (define (pipeline-custodian-cleanup!)\\n\"\n \" (let ([c (*pipeline-custodian*)])\\n\"\n \" (when c (custodian-shutdown-all c))))\\n\"\n \" (define (make-pipes n)\"))\n ;; Register each pipe FD pair with the custodian after creation\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(loop (+ i 1) (cons (list read-fd write-fd) pipes))))\"\n (string-append\n \"(let ([pair (list read-fd write-fd)])\\n\"\n \" (let ([c (*pipeline-custodian*)])\\n\"\n \" (when c\\n\"\n \" (custodian-register! c pair\\n\"\n \" (lambda (p)\\n\"\n \" (when (>= (car p) 0)\\n\"\n \" (guard (__exn [#t (void __exn)])\\n\"\n \" (ffi-close-fd (car p)))\\n\"\n \" (set-car! p -1))\\n\"\n \" (when (>= (cadr p) 0)\\n\"\n \" (guard (__exn [#t (void __exn)])\\n\"\n \" (ffi-close-fd (cadr p)))\\n\"\n \" (set-car! (cdr p) -1))))))\\n\"\n \" (loop (+ i 1) (cons pair pipes)))))\"))\n ;; Wrap 3-arg branch: add custodian parameterize around pipeline body\n (patch-file! \"src/jsh/pipeline.sls\"\n \" [(commands env execute-fn)\\n (let* ([pipe-types #f])\\n (let ([ptypes (or pipe-types\\n (make-list (- (length commands) 1) 'PIPE))])\\n (parameterize ([*procsub-cleanups* (list)])\"\n \" [(commands env execute-fn)\\n (let* ([pipe-types #f])\\n (let ([ptypes (or pipe-types\\n (make-list (- (length commands) 1) 'PIPE))])\\n (parameterize ([*procsub-cleanups* (list)]\\n [*pipeline-custodian* (make-custodian)])\")\n ;; Add custodian cleanup after run-procsub-cleanups in 3-arg branch\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(ffi-sigchld-unblock)\\n (run-procsub-cleanups!)\\n exit-codes)))))))))))))))))]\"\n \"(ffi-sigchld-unblock)\\n (run-procsub-cleanups!)\\n (pipeline-custodian-cleanup!)\\n exit-codes)))))))))))))))))]\")\n ;; Wrap 4-arg branch: add custodian parameterize\n (patch-file! \"src/jsh/pipeline.sls\"\n \" [(commands env execute-fn pipe-types)\\n (let ([ptypes (or pipe-types\\n (make-list (- (length commands) 1) 'PIPE))])\\n (parameterize ([*procsub-cleanups* (list)])\"\n \" [(commands env execute-fn pipe-types)\\n (let ([ptypes (or pipe-types\\n (make-list (- (length commands) 1) 'PIPE))])\\n (parameterize ([*procsub-cleanups* (list)]\\n [*pipeline-custodian* (make-custodian)])\")\n ;; Add custodian cleanup after run-procsub-cleanups in 4-arg branch\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(ffi-sigchld-unblock)\\n (run-procsub-cleanups!)\\n exit-codes))))))))))))))))]))\n (define (make-pipes n)\"\n \"(ffi-sigchld-unblock)\\n (run-procsub-cleanups!)\\n (pipeline-custodian-cleanup!)\\n exit-codes))))))))))))))))]))\n (define (make-pipes n)\")\n\n ;; --- 3. Profile infrastructure in main.sls ---\n ;; Add (std misc profile) import\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh stage)\\n (jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\"\n \"(jsh stage)\\n (jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\\n (only (std misc profile) profiling-active? profile-report profile-reset! profile-data)\")\n ;; Add profile builtin after embed-fd builtin\n (patch-file! \"src/jsh/main.sls\"\n \"(builtin-register! \\\"embed-fd\\\"\"\n (string-append\n \";; Profile builtin — control profiling at shell level\\n\"\n \" (builtin-register! \\\"profile\\\"\\n\"\n \" (lambda (args env)\\n\"\n \" (cond\\n\"\n \" [(null? args)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"usage: profile on|off|reset|report~n\\\") 2]\\n\"\n \" [(string=? (car args) \\\"on\\\")\\n\"\n \" (profiling-active? #t)\\n\"\n \" (fprintf (current-error-port) \\\"profiling enabled~n\\\") 0]\\n\"\n \" [(string=? (car args) \\\"off\\\")\\n\"\n \" (profiling-active? #f)\\n\"\n \" (fprintf (current-error-port) \\\"profiling disabled~n\\\") 0]\\n\"\n \" [(string=? (car args) \\\"reset\\\")\\n\"\n \" (profile-reset!)\\n\"\n \" (fprintf (current-error-port) \\\"profile data reset~n\\\") 0]\\n\"\n \" [(string=? (car args) \\\"report\\\")\\n\"\n \" (profile-report) 0]\\n\"\n \" [else\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"profile: unknown subcommand '~a'~n\\\" (car args)) 2])))\\n\"\n \" ;; Activate profiling if JSH_PROFILE=1\\n\"\n \" (let ([v (getenv \\\"JSH_PROFILE\\\" #f)])\\n\"\n \" (when (and v (string=? v \\\"1\\\"))\\n\"\n \" (profiling-active? #t)))\\n\"\n \" (builtin-register! \\\"embed-fd\\\"\"))\n\n ;; --- 4. Config loading is wired directly in startup.ss ---\n ;; (jsh-config-load! is imported + called there; the old patch-file! anchors\n ;; targeted the pre-Jerboa `define` form and silently no-op'd, leaving\n ;; ~/.jsh/config unloaded. Sourcing it in startup.ss is the single source of\n ;; truth — no fragile string patch.)\n\n ;; --- 5. Memoize hostname in prompt.sls ---\n ;; Add (std misc memoize) import\n (patch-file! \"src/jsh/prompt.sls\"\n \"(except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\"\n \"(only (std misc memoize) memoize)\\n (except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\")\n ;; Wrap hostname-short with memoize (called many times, never changes)\n (patch-file! \"src/jsh/prompt.sls\"\n \"(define (hostname-short)\\n (guard (__exn [#t ((lambda (e) \\\"localhost\\\") __exn)])\\n (let ([port (open-input-process\\n (list 'path: \\\"/bin/hostname\\\"))])\\n (let ([result (get-line port)])\\n (close-port port)\\n (if (string? result) result \\\"localhost\\\")))))\"\n \"(define hostname-short\\n (memoize\\n (lambda ()\\n (guard (__exn [#t ((lambda (e) \\\"localhost\\\") __exn)])\\n (let ([port (open-input-process\\n (list 'path: \\\"/bin/hostname\\\"))])\\n (let ([result (get-line port)])\\n (close-port port)\\n (if (string? result) result \\\"localhost\\\")))))))\")\n\n ;; --- 6. fmt import in recorder.sls ---\n (patch-file! \"src/jsh/recorder.sls\"\n \"(except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\"\n \"(only (std misc fmt) fmt fmt/port)\\n (except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\")\n\n (display \" Stdlib integration patches applied\\n\")\n) ;; end stdlib patches\n\n;; --- Global cleanup: replace Jerboa reader syntax with Chez equivalents ---\n;; The transpiler outputs #!void which is valid Jerboa but not Chez reader syntax.\n;; Replace all occurrences in .sls files so pure Chez compilation works.\n;; Helper: string-contains for plain Chez (not in chezscheme, only in Jerboa prelude)\n(define (string-contains haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) i]\n [else (loop (+ i 1))]))))\n(for-each\n (lambda (path)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))])\n (when (string-contains content \"#!void\")\n (let loop ([i 0] [result \"\"])\n (cond\n ((> (+ i 6) (string-length content))\n (let ([final (string-append result (substring content i (string-length content)))])\n (call-with-output-file path\n (lambda (p) (display final p))\n 'replace)\n (printf \" Fixed #!void → (void) in ~a~n\" path)))\n ((string=? (substring content i (+ i 6)) \"#!void\")\n (loop (+ i 6) (string-append result \"(void)\")))\n (else\n (loop (+ i 1) (string-append result (substring content i (+ i 1))))))))))\n (let ([files '()])\n (for-each\n (lambda (name)\n (let ([path (string-append \"src/jsh/\" name \".sls\")])\n (when (file-exists? path) (set! files (cons path files)))))\n '(\"ast\" \"registry\" \"macros\" \"util\" \"environment\" \"lexer\" \"arithmetic\"\n \"glob\" \"fuzzy\" \"history\" \"recorder\" \"player\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\"\n \"lineedit\" \"fzf\" \"script\" \"startup\" \"main\" \"coreutils\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-server\" \"mux-client\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"sandbox\" \"rl\" \"limits\"\n \"ffi\" \"embed-data\" \"embed\"))\n files))\n\n;; Note: read-eval (#. syntax) patches not needed — Chez Scheme's reader\n;; does not support #. read-eval syntax at all, so there is no injection risk.\n\n(display \"\\n=== Build complete ===\\n\")\n"} {"text":";; FILE: jerboa-shell/fix-tests.md\n# Fix Plan: Jerboa-Shell Compat Test Parity with Legacy Shell\n\n## Current Status\n\n| Shell | Passed | Total | Rate | Timeouts |\n|-------|--------|-------|------|----------|\n| Jerboa-shell (Chez) | 1003 | 1179 | 85.1% | 0 |\n| Legacy shell | 1063 | 1179 | 90.2% | 0 |\n| **Delta** | **-60** | | **-5.1pp** | |\n\n## Regression Summary by Spec File\n\n| Spec File | Regressions | Priority |\n|-----------|-------------|----------|\n| builtin-read | 22 | P0 |\n| builtin-printf | 14 | P1 |\n| builtin-echo | 9 | P1 |\n| background | 9 | P1 |\n| redirect | 4 | P2 |\n| tilde | 4 | P2 |\n| builtin-cd | 4 | P2 |\n| builtin-trap | 3 | P2 |\n| builtin-bracket | 2 | P3 |\n| brace-expansion | 2 | P3 |\n| glob | 2 | P3 |\n| quote | 1 | P3 |\n| builtin-eval-source | 1 | P3 |\n| case_ | 1 | P3 |\n| loop | 1 | P3 |\n| builtin-misc | 1 | P3 |\n\n---\n\n## P0: `read` Builtin — 22 Regressions\n\n### Root Cause Analysis\n\nThe `read` builtin implementation in `jerboa-shell/builtins.ss:1008` has multiple issues centered around the Chez port's I/O layer differences. The core problem is that `port-or-fd-read-char` uses `fdread` for pipeline stdin, but many `read` options don't work correctly through this code path.\n\n### Issue 1: `read` with zero args returns status 1 (test #4)\n\n**File:** `jerboa-shell/builtins.ss:1215-1219`\n\nWhen `read` is called with no variable names, `vars` is `[]` and `array-name` is `#f`. The code falls into `use-reply?` path (line 1232) which stores into `$REPLY`. The success paths all return `(if got-eof? 1 0)`. However, when reading from a pipe with no trailing newline, `got-eof?` is set true and status 1 is returned even when non-empty data was successfully read. Bash returns 0 when it successfully reads data, even without a trailing newline, when no var names given.\n\n**Fix:** When `read` with no args successfully reads a non-empty line (even at EOF), return 0. Change the return logic to `(if (and got-eof? (string=? line \"\")) 1 0)` in the use-reply? path.\n\n### Issue 2: `read -n N` not reading from pipe (tests #5, #10, #11, #13, #52)\n\n**File:** `jerboa-shell/builtins.ss:1067-1118`\n\n`read -n N` uses `port-or-fd-read-char` which goes through `fdread` on the raw pipeline fd. The `fdread` path reads one byte at a time. When combined with `-d` (delimiter) the logic is correct in structure, but:\n\n1. The `pipe-fd` may be `#f` when input comes from a heredoc or pipe redirect (not pipeline stdin)\n2. `port-or-fd-read-char` falls back to `read-char` on the Gambit port, which may buffer differently on Chez\n3. `-n` combined with `-d` doesn't respect the delimiter correctly in some edge cases\n\n**Fix:**\n- Ensure `pipe-fd` is set correctly for all input sources (not just pipeline stdin)\n- When `read -n N` reads from a pipe, use `read-char` on the port (not `fdread`) since Chez ports handle pipe EOF correctly\n- Fix `-n` + `-d` interaction: delimiter should stop reading even when count not reached\n\n### Issue 3: `read -d ''` null delimiter (tests #31, #46)\n\n**File:** `jerboa-shell/builtins.ss:1069-1071, 1120-1123`\n\nThe code at lines 1069-1071 and 1120-1123 converts empty `-d ''` to NUL char:\n```scheme\n(delim-ch (if (string=? delim \"\")\n (integer->char 0) ;; Convert empty string to NUL char\n (string-ref delim 0)))\n```\n\n**Semantic mismatch:** POSIX/bash treats `-d ''` as \"read NUL-separated records\" (stop at NUL byte). The conversion to `(integer->char 0)` is correct in principle, but `port-or-fd-read-char` may not handle NUL bytes correctly through the Chez character port — Chez's `read-char` may skip or mishandle NUL.\n\n**Fix:** For null delimiter mode, use byte-level I/O (`get-u8` / `fdread`) instead of `read-char`, since NUL is a valid delimiter byte but problematic as a Scheme character in some implementations. Compare each byte to 0 rather than each char to `#\\nul`.\n\n### Issue 4: `read -s` from pipe (test #23)\n\n**File:** `jerboa-shell/builtins.ss:1030-1032`\n\n`-s` (silent) only disables echo on TTYs via `tty-mode-set!`. The test has `-s` reading from a pipe, which isn't a TTY, so `-s` should be a no-op. The actual failure is likely in the reading logic when `-s` interacts with pipe-fd. The test expects `read -s -n2 var` to read 2 chars from pipe — same as Issue 2.\n\n**Fix:** Same as Issue 2 — fix `-n` reading from pipes.\n\n### Issue 5: `read -p` prompt fd check (test #43)\n\n**File:** `jerboa-shell/builtins.ss:1019-1022`\n\n```scheme\n(when (and (> (string-length prompt) 0)\n (= (ffi-isatty (or fd 0)) 1))\n (display prompt (current-error-port))\n (force-output (current-error-port)))\n```\n\n**Problem:** The isatty check is on the wrong fd. It checks `ffi-isatty(0)` (stdin), but bash checks if **stderr** (fd 2, where the prompt is displayed) is a tty. The prompt should be shown if stderr is a tty, regardless of whether stdin is a pipe.\n\nThe actual test #43 mismatch (`got: 'hi\\nhi\\n'` vs `expected: 'hi\\nh\\n'`) also involves a second `read -n1` not reading correctly — related to Issue 2.\n\n**Fix:** Change isatty check to `(= (ffi-isatty 2) 1)` to check stderr instead of stdin. Also fix the -n reading issue (Issue 2).\n\n### Issue 6: `read` without -r, backslash handling inconsistency (tests #14, #54, #63, #64)\n\n**File:** `jerboa-shell/builtins.ss:1182-1211`\n\n**Critical inconsistency across modes:**\n\n- **Default line read** (lines 1204-1207): Keeps BOTH backslash and char: `(display #\\\\ buf) (display next buf)`\n- **`-d` mode** (line 1159-1160): Keeps only the char: `(display next buf)`\n- **`-n` mode** (line 1113-1114): Keeps only the char: `(display next buf)`\n\nThe default mode keeps backslashes because \"backslash removal happens during IFS split\" (comment at line 1205). But this means the `read-strip-backslashes` function (line 1423) must be called on the result before storing into variables. The issue is:\n- `read` without args → `use-reply?` path (line 1233) calls `read-strip-backslashes` ✓\n- `read` with vars → IFS split path (line 1245-1247) calls `read-ifs-split` which should handle backslashes ✓\n- But `-d` and `-n` modes strip backslashes during reading, then `read-strip-backslashes` is applied AGAIN, double-stripping\n\n**Fix:**\n- Make backslash handling consistent across all modes\n- Ensure `read-strip-backslashes` is applied exactly once in all non-raw paths\n- Fix backslash-newline continuation when `-d delim` is set: continuation should only apply when the delimiter is newline\n\n### Issue 7: IFS splitting with non-default delimiters (tests #56-59, #61-64)\n\n**File:** `jerboa-shell/builtins.ss:1440-1448` (`read-ifs-split-raw`)\n\nMultiple IFS edge cases fail:\n- `IFS='x '` with `read -a`: trailing delimiters should create empty fields\n- Multi-character IFS with mixed whitespace/non-whitespace\n- Backslash + IFS interactions in non-raw mode\n\nThe `read-ifs-split-raw` and `read-ifs-split` functions have subtle differences from bash's IFS splitting:\n- Non-whitespace IFS chars are \"hard\" delimiters (each one creates a field boundary)\n- Whitespace IFS chars are \"soft\" (collapse together)\n- When `max-fields` is 0 (unlimited, for `-a`), trailing non-whitespace delimiters should create empty trailing fields\n- Backslash-escaped IFS chars should not split\n\n**Fix:** Rewrite `read-ifs-split` and `read-ifs-split-raw` to match bash's exact IFS splitting semantics. Test edge cases with `IFS='x '` extensively.\n\n### Issue 8: Smooshed option parsing (test #45)\n\n**File:** `jerboa-shell/builtins.ss:1269-1415`\n\n`read -rn1` with smooshed flags: `-rn1` should set `raw?=#t` and `nchars=1`. The smooshed flag parser at line 1274 looks correct in structure but may have an issue with the order of flag processing when `-n` consumes the rest of the arg.\n\n**Fix:** Verify that `-rn1` correctly parses as `-r -n 1` and that the remaining characters after `-n` are treated as the count, not as more flags.\n\n### Implementation Plan\n\n1. Fix `port-or-fd-read-char` to work correctly on Chez for pipe input (biggest impact — fixes tests #5, #10, #11, #13, #23, #43, #45, #52)\n2. Fix null-delimiter byte-level reading (fixes #31, #46)\n3. Fix IFS splitting for non-default delimiters (fixes #56-59, #61-64)\n4. Fix backslash handling in non-raw mode (fixes #14, #54, #63, #64)\n5. Fix zero-args return status (fixes #4)\n\n**Files to modify:**\n- `jerboa-shell/builtins.ss` — read builtin and IFS split functions\n\n---\n\n## P1: `printf` — 14 Regressions\n\n### Issue 1: `%x` always outputs uppercase (tests #14, #16, #22, #23, #26, #29, #45, #62)\n\n**File:** `jerboa-shell/builtins.ss:3569-3570`\n\n```scheme\n(raw (number->string n 16))\n(raw (if (char=? spec #\\X) (string-upcase raw) raw))\n```\n\nThe code uses `number->string n 16` which on Chez Scheme produces uppercase hex digits (e.g., `\"2A\"` instead of `\"2a\"`). The code only calls `string-upcase` for `%X`, assuming `number->string` returns lowercase. On Chez, `number->string` with radix 16 returns uppercase.\n\n**Fix:** Add `(raw (string-downcase raw))` after `number->string` to normalize to lowercase, then upcase only for `%X`:\n\n```scheme\n(raw (string-downcase (number->string n 16)))\n(raw (if (char=? spec #\\X) (string-upcase raw) raw))\n```\n\n### Issue 2: `printf %c` crashes with status 1 (test #38)\n\n**File:** `jerboa-shell/builtins.ss:3581-3594`\n\nThe `%c` handler uses `open-output-u8vector` and `get-output-u8vector` which are Gambit-specific. On Chez (via jerboa compat), these may not exist or behave differently, causing an exception caught by the outer handler returning status 1.\n\n**Fix:** Replace Gambit-specific u8vector port operations with Chez-compatible bytevector I/O:\n```scheme\n((#\\c)\n (when (and (string? arg) (> (string-length arg) 0))\n (let* ((ch (string-ref arg 0))\n (cp (char->integer ch)))\n (write-u8 cp buf)))\n (values (+ i 1) rest))\n```\nFor multi-byte chars, use `string->utf8` from Chez to get the first byte.\n\n### Issue 3: `printf %c` with unicode only prints char, not first byte (test #39)\n\nRelated to Issue 2. Bash's `%c` outputs the first **byte** of the UTF-8 encoding. The fix for Issue 2 should handle this — extract first byte via `(bytevector-u8-ref (string->utf8 (string ch)) 0)`.\n\n### Issue 4: `printf %b` not handling `\\NNN` octal without leading 0 (tests #58, #59)\n\n**File:** `jerboa-shell/builtins.ss:3742-3787` (`printf-interpret-b-escapes`)\n\nThe `%b` handler delegates non-octal escapes to `printf-escape` (line 3784). `printf-escape` handles `\\NNN` (octal without leading 0) at lines 3728-3737. But the issue is that `printf-interpret-b-escapes` checks for `\\0NNN` and `\\1`-`\\7` starts — it does handle both forms. The actual bug may be in the `write-u8` call going to `buf` which is a u8vector output port, and the raw byte may not be flushed correctly.\n\n**Fix:** Verify that `write-u8` works correctly on Chez u8vector output ports. If not, adapt the byte writing to use Chez-compatible APIs.\n\n### Issue 5: `printf %b` with `\\044` (dollar sign) outputs empty (test #55)\n\nTest expects `printf '%b' '\\044'` to output `$`. The `\\0` prefix in `\\044` triggers the `\\0NNN` path which reads up to 3 octal digits after the 0: `44` → value 36 → byte 0x24 = `$`. This should work, but the `write-u8` on a u8vector port may fail on Chez.\n\n**Fix:** Same as Issue 4 — verify u8vector port `write-u8` compatibility.\n\n### Issue 6: Invalid UTF-8 byte handling in `printf '%c'` (test #27)\n\nThe test uses `printf '%x' \"'$byte\"` where `$byte` is a raw byte like `\\xce`. On Chez, single-byte characters above 127 may be treated differently. Chez's `char->integer` on invalid UTF-8 gives the Unicode replacement character U+FFFD.\n\n**Fix:** When the `'char` form encounters raw bytes (from `$'\\xNN'` syntax), extract the raw byte value rather than the Unicode code point. This may require checking for Private Use Area encoding used by the shell's raw-byte mechanism.\n\n### Implementation Plan\n\n1. Fix `number->string` lowercase for `%x` (fixes 8 tests — biggest impact)\n2. Fix `%c` to use Chez-compatible byte extraction (fixes 2 tests)\n3. Fix `%b` u8vector port compatibility (fixes 3 tests)\n4. Fix raw byte handling for `'char` printf argument (fixes 1 test)\n\n**Files to modify:**\n- `jerboa-shell/builtins.ss` — printf format handlers\n\n---\n\n## P1: `echo -e` — 9 Regressions\n\n### Root Cause: Raw byte output ordering\n\n**File:** `jerboa-shell/builtins.ss:3283-3375` (`echo-expand-escapes`, `display-raw-bytes`)\n\nAll 9 echo failures show the same pattern: the raw byte appears at the wrong position in the output. For example, `echo -e 'ab\\x63d'` outputs `abcdf\\ne` instead of `abcdef\\n`. The character 'e' (0x65) and 'f' appear swapped with the escaped character.\n\nThe `echo-expand-escapes` function returns a list of segments (strings and fixnums for raw bytes). The segments are accumulated in reverse order using `acc` and then reversed at the end. The issue is in how `flush-buf!` interacts with the accumulator:\n\n```scheme\n(loop j (cons byte (flush-buf! acc)))\n```\n\nThis flushes the current string buffer and then adds the byte. But `flush-buf!` returns `(cons current-string acc)` where `acc` is in reverse order. The byte gets consed onto this, so it ends up in the right position in the reversed list... but there may be a subtle bug in the `flush-buf!` + `cons` sequence when the `get-output-string` side-effects interact with Chez.\n\n**Confirmed Chez-specific issue:** `get-output-string` on Chez does NOT reset the output port. On Gambit, `get-output-string` drains the buffer and resets it. On Chez, subsequent writes to `buf` include old content, causing all raw-byte escapes to produce corrupted output — bytes appear at wrong positions because the string buffer accumulates text from before AND after the byte.\n\nThe `flush-buf!` function at line 3298 calls `get-output-string buf` but doesn't reset the port:\n```scheme\n(define (flush-buf! acc)\n (let ((s (get-output-string buf)))\n (if (string=? s \"\") acc (cons s acc))))\n```\n\nAfter this call, `buf` still contains the old text. When the loop continues and writes more chars to `buf`, they get prepended with the already-flushed content.\n\n**Fix:** After `get-output-string buf`, explicitly reinitialize `buf`:\n```scheme\n(define (flush-buf! acc)\n (let ((s (get-output-string buf)))\n (set! buf (open-output-string)) ;; reset — critical for Chez\n (if (string=? s \"\") acc (cons s acc))))\n```\n\nThis same pattern also affects `printf-escape` and `printf-interpret-b-escapes` which use `write-u8` to a u8vector output port — verify those ports also handle the Chez `get-output-string`/`get-output-u8vector` semantics correctly.\n\n### Implementation Plan\n\n1. Fix `get-output-string` buffer reset in `echo-expand-escapes` (fixes all 9 tests)\n\n**Files to modify:**\n- `jerboa-shell/builtins.ss` — `echo-expand-escapes` function\n- Possibly `src/compat/gambit.sls` — if `get-output-string` needs a compat wrapper\n\n---\n\n## P1: Background Jobs & `wait` — 9 Regressions\n\n### Issue 1: `wait` returns 0 instead of process exit status (tests #8, #16, #17, #21)\n\n**File:** `jerboa-shell/jobs.ss:268-341` (`job-wait`)\n\n`wait $PID` calls `job-table-get` to look up the job by PID. For external processes launched via `ffi-fork-exec`, the PID in the job table is the real PID. But `job-table-get` may not find the job if it was already cleaned up or if the PID format doesn't match (string vs number comparison).\n\nWhen `job-table-get` returns `#f`, the wait builtin returns 127 (line 1952). But tests show status 0, suggesting the job IS found but `job-wait` returns 0.\n\nRoot cause: In `job-wait`, the `ffi-waitpid-pid` call with `WNOHANG` may return 0 (child not yet exited) initially, then on retry the child has already been reaped by SIGCHLD handler. When `ffi-waitpid-pid` returns -1 (error, ECHILD because child already reaped), the code falls to the `else` branch (line 328) which sets `last-exit-code` to 0.\n\n**Fix:** When `waitpid` returns -1 (ECHILD), check if the job's thread has a saved exit status. Alternatively, save exit status in the job-process struct when SIGCHLD is received, so `job-wait` can retrieve it even after the process is reaped.\n\n### Issue 2: Builtins/compound commands in background produce no output (tests #9, #19)\n\n**File:** `jerboa-shell/executor.ss:1104-1120` (`launch-background`)\n\nFor non-simple commands (compound/builtins), `launch-background` spawns a thread:\n```scheme\n(th (spawn (lambda ()\n (parameterize ((*in-subshell* #t))\n (execute-command cmd child-env)))))\n```\n\nThe thread runs in the same process and inherits stdout via closure. However:\n1. **No `force-output` before thread exit** — output is buffered and lost when thread is GC'd\n2. **No explicit port parameterization** — unlike `launch-thread-piped` in `pipeline.ss:295-311` which explicitly creates and parameterizes output ports\n3. The `*in-subshell*` flag may cause output routing issues if it affects how builtins write\n\n**Contrast with correct pattern** in `pipeline.ss:311` which calls `force-output` before closing the port:\n```scheme\n(force-output out-port)\n(close-output-port out-port)\n```\n\n**Fix:**\n- Add `force-output` in the thread after `execute-command` returns (in a `dynamic-wind` cleanup)\n- Ensure background threads explicitly parameterize `current-output-port` to the parent's actual stdout\n- For background for-loops (`control.ss:39-88`), add `force-output` after each loop iteration body\n\n### Issue 3: `wait -n` returns 0 instead of first-finished exit status (test #18)\n\n**File:** `jerboa-shell/jobs.ss:346-417` (`job-wait-any`, `job-check-finished?`)\n\n`job-wait-any` polls running jobs via `job-check-finished?` (lines 377-417) which uses non-blocking `ffi-waitpid-pid ... WNOHANG`. **Two problems:**\n\n1. `job-check-finished?` detects completion but does NOT update the job's status — it only returns `#t`/`#f`\n2. After detecting completion, the code calls `job-wait` which does ANOTHER `waitpid` — but the process was already reaped by the first check, causing `waitpid` to return -1 (ECHILD), which falls to the `else` branch (line 328) that sets `last-exit-code` to 0\n\n**Race condition:** Between `job-check-finished?` (non-blocking check) and `job-wait` (blocking wait), the child is reaped by the first call, so the second call fails and returns 0.\n\n**Fix:** In `job-check-finished?`, when `waitpid` returns > 0, immediately save the exit status in the `job-process` struct. Then `job-wait` should check for the saved status before calling `waitpid` again. Alternatively, make `job-check-finished?` update the job status atomically when it discovers completion.\n\n### Issue 4: Trap not cleared in background subshell (trap test #26)\n\n**File:** `jerboa-shell/executor.ss:1108-1119`\n\nBash clears traps in child processes started with `&`. The thread-based \"subshell\" for compound commands doesn't clear the trap table in the cloned environment.\n\n**Fix:** In `launch-background`, after cloning the environment, clear `*trap-table*` in the child thread's dynamic scope.\n\n### Implementation Plan\n\n1. Fix `waitpid`/ECHILD handling to preserve exit status (fixes 4 tests)\n2. Fix background thread stdout routing (fixes 2 tests)\n3. Fix `wait -n` polling to actually check process status (fixes 1 test)\n4. Clear traps in background subshells (fixes 1 test, also fixes trap test)\n5. Fix `wait` for all background jobs to properly collect statuses (fixes 1 test)\n\n**Files to modify:**\n- `jerboa-shell/jobs.ss` — job-wait, job-wait-any\n- `jerboa-shell/executor.ss` — launch-background\n- `jerboa-shell/signals.ss` — trap clearing for subshells\n\n---\n\n## P2: Redirections — 4 Regressions\n\n### Issue 1: Writing to fd 3/4 multiple times loses first write (tests #17, #18)\n\n**Expected:** Two `echo` writes to `exec 3>file` should produce both lines.\n**Got:** Only the last line appears.\n\n**File:** `jerboa-shell/redirect.ss:173-175, 671-678`\n\nWhen `exec 3>file` is applied, `set-port-for-fd!` (line 671) opens the file and creates a NEW Gambit port. This port is set via `(current-output-port port)` parameterization. When another redirect targets the same fd (e.g., `echo foo >&3`), a new port is created that replaces the previous one. The old port's buffered content is lost.\n\n**Fix:** For persistent redirections (`exec N>file`), open the fd once and track it in the shell's fd table. Subsequent writes to fd N should reuse the existing fd/port rather than reopening the file. Ensure `set-port-for-fd!` checks for existing persistent fds before creating new ports.\n\n### Issue 2: `1>&2-` (move fd) not working (test #27)\n\nThe `N>&M-` syntax means \"dup fd M to fd N, then close M\". This may not be implemented in the Chez redirect layer.\n\n**Fix:** Implement fd-move semantics in the redirect handler: `dup2(M, N)` followed by `close(M)`.\n\n### Issue 3: `<>` read/write mode not preserving position (test #29)\n\n`<> file` opens file for both reading and writing. After reading, the write position should be where the read left off. The Chez port may open separate read/write ports or not handle bidirectional fd correctly.\n\n**Fix:** Use a single fd opened with `O_RDWR` and ensure read/write share the same file offset. May require FFI `open()` with `O_RDWR` flag.\n\n**Files to modify:**\n- `src/gsh/redirect.sls`\n- `jerboa-shell/redirect.ss`\n\n---\n\n## P2: Tilde Expansion — 4 Regressions\n\n### Issue 1: `~nonexistent` expands to $HOME instead of literal (test #6)\n\n**File:** `jerboa-shell/expander.ss:720-724`\n\n```scheme\n(else\n (with-catch\n (lambda (e) (values (substring str i end) end))\n (lambda ()\n (values (user-info-home (user-info prefix)) end))))\n```\n\nWhen `user-info` throws for a nonexistent user, the catch should return the literal `~nonexistent`. But on Chez, `user-info` might not throw — it might return a default or the current user's info. Or the Chez compat `user-info` implementation falls back differently.\n\n**Fix:** Check the `user-info` implementation in the compat layer. Ensure it throws when the user doesn't exist. If it silently returns current user info, add an explicit check.\n\n### Issue 2: `${x//~/~root}` not expanding tilde in replacement (test #8)\n\n**File:** `jerboa-shell/expander.ss`\n\nIn `${var//pattern/replacement}`, tilde in the replacement should expand. The current code may not run tilde expansion on the replacement string.\n\n**Fix:** Apply tilde expansion to the replacement string in `${var//pat/repl}` before substitution.\n\n### Issue 3: `x=foo:~` tilde in colon-separated values (test #9)\n\n**File:** `jerboa-shell/expander.ss:461-470`\n\nIn assignment context, `~` should expand after `:` in values like `foo:~`. The `expand-assignment-value` function should handle this. Test shows `foo:~,` is incorrectly expanding the tilde (expected `foo:~,` to NOT expand because `,` follows `~` without `/`).\n\n**Fix:** Tilde after `:` in assignment context should only expand when followed by `/` or end of string, not arbitrary chars.\n\n### Issue 4: Temp assignment `x=~` with `env` (test #14)\n\nTilde expansion in temp assignments before commands (e.g., `x=~root:~ env`) should expand `~root` to root's home. Related to user-info lookup (Issue 1).\n\n**Fix:** Same as Issue 1 — fix `user-info` compat to correctly resolve other users.\n\n**Files to modify:**\n- `jerboa-shell/expander.ss` — `expand-tilde-in`, `expand-assignment-value`\n- `src/compat/gambit.sls` — `user-info` implementation\n\n---\n\n## P2: `cd` Builtin — 4 Regressions\n\n### Issue 1: `cd` with strict_arg_parse (test #3)\n\n`cd --` should succeed (status 0), but returns 1. The option parser may treat `--` as an error when no directory follows, rather than as \"cd to $HOME\".\n\n**Fix:** In `cd` option parsing, `cd --` (with no further args) should cd to `$HOME`, same as bare `cd`.\n\n### Issue 2: `pwd` in symlinked dir (test #15)\n\nWhen the shell starts in a directory that's a symlink, `pwd` should show the symlink path (logical). The Chez port may resolve symlinks eagerly.\n\n**Fix:** Initialize `$PWD` from the environment's `PWD` variable (if it points to the correct directory) rather than using `current-directory` which may resolve symlinks.\n\n### Issue 3: `cd` with inherited PWD disagreement (tests #25, #26)\n\nWhen `PWD` is inherited but doesn't match the actual directory, `cd` should still work. The Chez port may not handle the case where `$PWD` disagrees with `getcwd()`.\n\n**Fix:** In `cd`, when the inherited `$PWD` disagrees with `getcwd()`, update `$PWD` to reflect the actual directory before attempting relative path resolution.\n\n**Files to modify:**\n- `jerboa-shell/builtins.ss` — cd builtin\n- `jerboa-shell/main.ss` — PWD initialization\n\n---\n\n## P2: `trap` — 3 Regressions\n\n### Issue 1: Traps not cleared in subshell via `&` (test #26)\n\nBackground subshells (`cmd &`) should start with an empty trap table. Currently thread-based subshells inherit the parent's `*trap-table*`.\n\n**Fix:** In `launch-background`, parameterize `*trap-table*` with a fresh hash-table in the child thread.\n\n### Issue 2: trap USR1 + sleep not working non-interactively (test #27)\n\n`trap 'echo usr1' USR1; kill -USR1 $$; sleep 0.1` should print \"usr1\". The signal may not be delivered or the trap handler may not execute during `sleep`.\n\n**Fix:** Ensure `pending-signals!` is checked after `sleep` completes, and that signal flag checking works for USR1 in non-interactive mode.\n\n### Issue 3: trap EXIT + sleep + SIGINT (test #29)\n\nSimilar to Issue 2 — EXIT trap should fire when the shell receives SIGINT during sleep.\n\n**Fix:** Ensure EXIT trap fires on all exit paths including signal-induced exits.\n\n**Files to modify:**\n- `jerboa-shell/signals.ss` — signal delivery in non-interactive mode\n- `jerboa-shell/executor.ss` — launch-background trap clearing\n\n---\n\n## P3: `file-info` Compat Layer — 4 Regressions (across bracket, cd)\n\n### Root Cause: Stubbed-out stat fields\n\n**File:** `src/compat/gambit.sls:282-284`\n\n```scheme\n(make-file-info-rec type (if (< size 0) 0 size) mode\n 0 0 ;; device/inode — use real stat if needed\n 0 0 ;; owner/group\n ...)\n```\n\n`device`, `inode`, `owner`, and `group` are all hardcoded to 0. This breaks:\n- `test -G` (effective group ownership) — always fails (test #37)\n- `test -O` (effective user ownership) — always fails (test #37)\n- `test -ef` (same file by device+inode) — can't compare because both are 0 (test #42)\n\n### Fix\n\nAdd FFI functions to the C shim (`ffi-shim.c`) to extract full stat fields:\n\n```c\nint ffi_file_uid(const char *path) { struct stat st; return stat(path, &st) == 0 ? st.st_uid : -1; }\nint ffi_file_gid(const char *path) { struct stat st; return stat(path, &st) == 0 ? st.st_gid : -1; }\nlong long ffi_file_dev(const char *path) { struct stat st; return stat(path, &st) == 0 ? (long long)st.st_dev : -1; }\nlong long ffi_file_ino(const char *path) { struct stat st; return stat(path, &st) == 0 ? (long long)st.st_ino : -1; }\n```\n\nThen update `file-info` to use them:\n\n```scheme\n(define c-ffi-file-uid (foreign-procedure \"ffi_file_uid\" (string) int))\n(define c-ffi-file-gid (foreign-procedure \"ffi_file_gid\" (string) int))\n(define c-ffi-file-dev (foreign-procedure \"ffi_file_dev\" (string) long-long))\n(define c-ffi-file-ino (foreign-procedure \"ffi_file_ino\" (string) long-long))\n\n(define (file-info path . follow?)\n (let* ((follow (if (pair? follow?) (car follow?) #t))\n (type-int (c-ffi-file-type path (if follow 1 0))))\n (if (= type-int -1)\n (error 'file-info \"cannot stat file\" path)\n (make-file-info-rec\n (file-type-int->symbol type-int)\n (let ((s (c-ffi-file-size path))) (if (< s 0) 0 s))\n (c-ffi-file-mode path)\n (c-ffi-file-dev path)\n (c-ffi-file-ino path)\n (c-ffi-file-uid path)\n (c-ffi-file-gid path)\n (make-time 'time-utc 0 (let ((m (c-ffi-file-mtime path))) (if (< m 0) 0 m)))\n (make-time 'time-utc 0 0)))))\n```\n\n**Files to modify:**\n- `ffi-shim.c` — add uid/gid/dev/ino FFI functions\n- `src/compat/gambit.sls` — update `file-info` to use them\n\n---\n\n## P3: Glob — 2 Regressions\n\n### Issue 1: Unicode char in glob pattern (test #31)\n\n`echo __?__` should match both `__a__` and `__μ__`. The `?` glob should match any single character, including multi-byte UTF-8 characters. The Chez glob implementation may treat `?` as matching a single byte instead of a single character.\n\n**Fix:** In `jerboa-shell/glob.ss`, ensure glob `?` matches a single Unicode character, not a single byte. The regex `[^/]` generated for `?` should be `[^/]` with Unicode mode enabled.\n\n### Issue 2: `shopt -u globskipdots` (test #39)\n\n`shopt -u globskipdots` should make `*` match `.` and `..`. This shopt option may not be implemented.\n\n**Fix:** Add `globskipdots` to the shopt handling. When disabled, glob patterns should include dotfiles including `.` and `..`.\n\n**Files to modify:**\n- `jerboa-shell/glob.ss`\n- `jerboa-shell/builtins.ss` — shopt handler\n\n---\n\n## P3: Brace Expansion — 2 Regressions\n\n### Issue 1: Tilde in brace expansion (test #30)\n\n`echo ~bob/src{,~root}` should expand to `/home/bob/src /root`. Tilde at the start of brace elements should expand.\n\n**Fix:** Apply tilde expansion to each brace-expanded result, not just the original word.\n\n### Issue 2: Side effect ordering in `{a,b,c}` (test #53)\n\n`echo {a,b,c}-$((i++))` should produce `a-0 b-1 c-2` (left-to-right evaluation). Currently produces `a-1 b-2 c-0`, suggesting the arithmetic expression is evaluated first for all expansions, then assigned.\n\n**Fix:** Evaluate `$((i++))` for each brace-expanded word in left-to-right order, not all at once.\n\n**Files to modify:**\n- `jerboa-shell/expander.ss` — brace expansion and tilde interaction\n\n---\n\n## P3: Quote — 1 Regression\n\n### Issue: `$'\\377'` octal in ANSI-C quoting (test #28)\n\n`$'\\377'` should produce byte 0xFF. The output shows the byte appears but at the wrong position, similar to the echo-e issue.\n\n**Fix:** Same root cause as echo-e — `get-output-string` buffer reset issue in the ANSI-C quote expander. Apply the same fix.\n\n**Files to modify:**\n- `jerboa-shell/expander.ss` — ANSI-C quoting handler\n\n---\n\n## P3: `source` Along PATH — 1 Regression\n\n### Issue: Source doesn't skip directories in PATH (test #23)\n\n`source myfile` should search PATH and skip entries that are directories. `find-file-in-path` at `jerboa-shell/util.ss:164` calls `file-directory?` which may not work correctly through the Chez compat layer.\n\n**Fix:** Verify `file-directory?` works correctly on Chez. It may need to use the stat-based FFI rather than Chez's built-in `file-directory?` which might have different semantics.\n\n**Files to modify:**\n- `jerboa-shell/util.ss` — `find-file-in-path`\n- `src/compat/gambit.sls` — verify `file-directory?`\n\n---\n\n## P3: `case` — 1 Regression\n\n### Issue: Matching byte 0xFF against empty string (test #10)\n\n`case $'\\xff' in '') echo a;; *) echo b;; esac` should match `*` (not empty), outputting `b`. Chez may represent the 0xFF byte differently, making the case variable appear empty.\n\n**Fix:** Ensure raw bytes from `$'\\xff'` are preserved through variable assignment and case matching. Check the PUA (Private Use Area) encoding scheme for raw bytes.\n\n**Files to modify:**\n- `jerboa-shell/expander.ss` — case pattern matching with raw bytes\n\n---\n\n## P3: `while` in Pipeline — 1 Regression\n\n### Issue: Variable not visible after while-in-pipe (test #12)\n\n`echo 1 2 3 | while read x; do ((n++)); done; echo $n` — expects `$n` to be 3. In bash with `lastpipe` enabled, the last command in a pipeline runs in the current shell. Without it, pipeline components run in subshells and variable changes are lost.\n\n**Fix:** Check if `lastpipe` shopt is enabled (it should be in this context). If the last pipeline component is a builtin/compound command, run it in the current shell rather than a subshell.\n\n**Files to modify:**\n- `jerboa-shell/executor.ss` — pipeline execution, lastpipe handling\n\n---\n\n## P3: `time` Pipeline — 1 Regression\n\n### Issue: `time` with pipeline returns status 1 (test #4)\n\n`time ls | cat` returns status 1 instead of 0. The `time` keyword wraps a pipeline, but the status may not propagate correctly from the timed pipeline.\n\n**Fix:** In `execute-time-command` (`jerboa-shell/executor.ss:946`), ensure the pipeline's exit status is returned, not an error status from the timing code. Check for exceptions in `fl-` or `cpu-time` on Chez.\n\n**Files to modify:**\n- `jerboa-shell/executor.ss` — `execute-time-command`\n\n---\n\n## Implementation Order (Recommended)\n\n### Phase 1: Quick Wins (26 tests, ~2 days)\n\n1. **printf %x lowercase** — Add `string-downcase` after `number->string` (8 tests)\n2. **echo-e buffer reset** — Fix `get-output-string` in `echo-expand-escapes` (9 tests)\n3. **file-info stat fields** — Add uid/gid/dev/ino FFI and update compat (4 tests)\n4. **printf %c** — Chez-compatible byte extraction (2 tests)\n5. **$'\\377' quoting** — Same buffer reset fix as echo-e (1 test)\n6. **source PATH directories** — Verify file-directory? compat (1 test)\n7. **time pipeline status** — Fix status propagation (1 test)\n\n### Phase 2: Medium Effort (22 tests, ~3 days)\n\n8. **read -n from pipe** — Fix port-or-fd-read-char on Chez (6 tests)\n9. **read IFS splitting** — Rewrite read-ifs-split for bash compat (8 tests)\n10. **read backslash handling** — Fix non-raw mode (4 tests)\n11. **read null delimiter** — Byte-level I/O for -d '' (2 tests)\n12. **read misc** — Zero args status, smooshed opts (2 tests)\n\n### Phase 3: Structural Fixes (12 tests, ~4 days)\n\n13. **Background job wait** — Fix waitpid/ECHILD, save exit status (4 tests)\n14. **Background stdout** — Fix thread output routing (2 tests)\n15. **Tilde expansion** — Fix ~user, assignment context, replacement (4 tests)\n16. **Redirect persistence** — Fix exec N>file fd management (3 tests)\n\n### Phase 4: Edge Cases (6 tests, ~2 days)\n\n17. **cd improvements** — PWD init, symlinks, arg parsing (4 tests)\n18. **wait -n** — Non-blocking poll for process completion (1 test)\n19. **trap in subshells** — Clear traps, signal delivery (3 tests)\n20. **glob unicode** — Fix ? to match chars not bytes (1 test)\n21. **brace+tilde** — Tilde in brace elements (1 test)\n22. **Misc** — case 0xff, while-in-pipe lastpipe, brace side-effects, globskipdots (4 tests)\n\n### Total: ~60 test regressions addressed across ~11 days of work\n\n---\n\n## Verification\n\nAfter each fix, run the comparison:\n\n```bash\npython3 /tmp/compare_compat.py\n```\n\nOr test a single spec:\n\n```bash\npython3 jerboa-shell/test/run_spec.py -v \\\n /home/jafourni/mine/jerboa-shell/_vendor/oils/spec/SPECNAME.test.sh \\\n /home/jafourni/mine/jerboa-shell/gsh\n```\n\nTarget: **1063/1179 (90.2%)** — parity with jerboa-shell.\n\n---\n\n## Improvements to Preserve\n\nJerboa-shell already passes 19 tests that jerboa-shell fails. These should not regress:\n\n| Spec File | Tests | Count |\n|-----------|-------|-------|\n| exit-status | #1, #3, #4, #7, #8 | 5 |\n| redirect-multi | #7, #12, #13 | 3 |\n| builtin-set | #6, #7, #8 | 3 |\n| pipeline | #6, #12, #23 | 3 |\n| builtin-process | #23, #26 | 2 |\n| smoke | #15 | 1 |\n| arith | #14 | 1 |\n| var-op-bash | #19 | 1 |\n\nThese represent areas where the Chez port has better behavior (likely due to different default behaviors in Chez's process handling, signal management, or numeric operations). Guard these with explicit regression tests.\n"} -{"text":";; FILE: jerboa-shell/build-jsh-freebsd.ss\n#!chezscheme\n;;; build-jsh-freebsd.ss — Build a fully static jsh binary on FreeBSD\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:... < build-jsh-freebsd.ss\n;;;\n;;; This script:\n;;; 1. Patches coreutils/awk/sed/ssl for static builds (no dlopen)\n;;; 2. Compiles jsh modules (using stock scheme)\n;;; 3. Creates boot file + optimized program .so\n;;; 4. Generates C files with embedded boot data\n;;; 5. Compiles C with cc (clang) against static Chez's scheme.h\n;;; 6. Links fully static binary with libkernel.a\n;;;\n;;; The resulting jsh-freebsd binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/home/freebsd\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-freebsd.sh so we know the repo root.\n(define vendor-dir\n (let ([script-dir (getenv \"SCRIPT_DIR\")])\n (if script-dir\n (format \"~a/vendor\" script-dir)\n (let ([cwd-vendor \"./vendor\"])\n (if (file-directory? cwd-vendor) cwd-vendor\n (format \"~a/jerboa-shell/vendor\" home-dir))))))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. Callers wrap with (or (getenv \"X\") (dep ...))\n;; to allow env var overrides from the shell script.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jsqlite/src\" home-dir)))\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (dep \"jerboa-coreutils\" \"lib\")))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n(define coreutils-shim\n (let ([upstream (dep-file \"jerboa-coreutils\" \"support/libcoreutils.c\")]\n [local \"patches/libcoreutils.c\"])\n (cond\n [(file-exists? upstream) upstream]\n [(file-exists? local) local]\n [else upstream])))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls).\n;; OpenSSL via load-shared-object cannot work in static builds and rustls is\n;; preferred for security.\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n;; Rust native library — resolve via vendor/ → ~/mine/ → ~/\n(define native-rs-dir\n (let* ([v (format \"~a/jerboa/jerboa-native-rs\" vendor-dir)]\n [m (format \"~a/mine/jerboa/jerboa-native-rs\" home-dir)]\n [h (format \"~a/jerboa/jerboa-native-rs\" home-dir)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n(define native-lib-path\n (format \"~a/target/release/libjerboa_native.a\" native-rs-dir))\n(define native-src-dir\n (format \"~a/src\" native-rs-dir))\n;; Sentinel file written after a successful native build without SQLite.\n;; If absent, the .a was built with default (tls-only) features — must rebuild.\n(define native-features-sentinel\n (format \"~a/target/release/.built-with-tls-crypto-no-sqlite\" native-rs-dir))\n(when (and (file-exists? native-src-dir)\n (or (not (file-exists? native-lib-path))\n ;; Features sentinel absent → stale build (wrong feature set)\n (not (file-exists? native-features-sentinel))\n ;; Check if any .rs file is newer than the .a\n (let ([lib-mtime (file-modification-time native-lib-path)])\n (let check ([files (directory-list native-src-dir)])\n (and (pair? files)\n (let ([f (format \"~a/~a\" native-src-dir (car files))])\n (or (and (> (string-length (car files)) 3)\n (string=? \".rs\" (substring (car files)\n (- (string-length (car files)) 3)\n (string-length (car files))))\n (time>? (file-modification-time f) lib-mtime))\n (check (cdr files)))))))))\n (printf \"~n[0/7] Rebuilding Rust native library (source newer than .a)...~n\")\n (let ([rc (safe-system (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote native-rs-dir)))])\n (unless (= rc 0)\n (fprintf (current-error-port) \"FATAL: cargo build --release --no-default-features --features tls,crypto failed~n\")\n (exit 1)))\n ;; Write sentinel so next build knows the right features were used\n (let ([port (open-output-file native-features-sentinel 'truncate)])\n (display \"tls,crypto,no-sqlite\\n\" port)\n (close-output-port port)))\n(when (and (file-exists? native-lib-path)\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(define has-native-lib? (file-exists? native-lib-path))\n\n;; Rust coreutils static library — check current dir first (container build), then home\n(define rust-coreutils-lib-path\n (let ([local (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" (current-directory))]\n [home-path (format \"~a/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\" home-dir)]\n [mine-path (format \"~a/mine/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\" home-dir)])\n (cond\n [(file-exists? local) local]\n [(file-exists? mine-path) mine-path]\n [else home-path])))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — coreutils builtins will be stubs~n\"))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\"))\n\n;; Chez Scheme static installation\n(define chez-ta6fb\n (or (getenv \"CHEZ_TA6FB\")\n (let ([dirs (directory-list \"/usr/local/lib\")])\n (let ([csv-dir (find (lambda (d) (string-prefix? \"csv\" d)) dirs)])\n (if csv-dir\n (format \"/usr/local/lib/~a/ta6fb\" csv-dir)\n (error 'build \"Cannot find Chez ta6fb directory in /usr/local/lib\"))))))\n\n(define scheme-h-dir chez-ta6fb)\n(define petite-boot-path (format \"~a/petite.boot\" chez-ta6fb))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-ta6fb))\n\n(printf \"Chez static: ~a~n\" chez-ta6fb)\n(printf \"Native lib: ~a~n\" (if has-native-lib? native-lib-path \"not found\"))\n(printf \"~n\")\n\n;; ========== Step 0: Patch coreutils for static builds ==========\n;; Coreutils modules call (load-shared-object #f) at library init time.\n;; In static builds, load-shared-object throws because dlopen is unavailable.\n;; Since FFI symbols are pre-registered via Sforeign_symbol, we patch these out.\n\n;; Detect sed -i syntax: FreeBSD uses `sed -i ''`, GNU sed uses `sed -i`\n(define sed-inplace\n (if (= 0 (system \"sed --version 2>/dev/null | head -1 | grep -q GNU\"))\n \"sed -i\" ;; GNU sed (Linux)\n \"sed -i ''\")) ;; BSD sed (FreeBSD/macOS)\n\n(printf \"[0/7] Patching coreutils for static build (no dlopen)...~n\")\n\n(define coreutils-stage (format \"~a/coreutils-stage\" (current-directory)))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(system (format \"mkdir -p '~a'\" coreutils-stage))\n\n(system (format \"cp -a '~a/jerboa-coreutils' '~a/'\"\n coreutils-dir coreutils-stage))\n;; Patch load-shared-object calls (incompatible with static linking)\n(system (format \"find '~a/jerboa-coreutils' -name '*.sls' -exec ~a 's/(load-shared-object #f)/(void)/g' {} +\"\n coreutils-stage sed-inplace))\n(system (format \"find '~a/jerboa-coreutils' -name '*.so' -delete\"\n coreutils-stage))\n(system (format \"find '~a/jerboa-coreutils' -name '*.wpo' -delete\"\n coreutils-stage))\n\n(printf \" Recompiling patched coreutils...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"common.sls\" \"common/version.sls\" \"common/io.sls\" \"common/security.sls\"))\n (for-each\n (lambda (name)\n (let ([sls (format \"~a/jerboa-coreutils/~a.sls\" coreutils-stage name)])\n (when (file-exists? sls)\n (compile-library sls))))\n '(\"basename\" \"dirname\" \"link\" \"unlink\" \"yes\" \"printenv\"\n \"sleep\" \"whoami\" \"logname\" \"hostname\" \"nproc\" \"tty\" \"sync\" \"hostid\"\n \"cat\" \"head\" \"tail\" \"tac\" \"tee\" \"wc\" \"nl\" \"fold\" \"expand\" \"unexpand\" \"fmt\"\n \"cut\" \"paste\" \"join\" \"comm\" \"sort\" \"uniq\" \"tr\" \"numfmt\"\n \"mkdir\" \"rmdir\" \"mktemp\" \"touch\" \"readlink\" \"realpath\" \"ln\" \"cp\" \"mv\" \"rm\"\n \"install\" \"shred\"\n \"ls\" \"chmod\" \"chown\" \"chgrp\" \"stat\" \"du\" \"df\" \"pathchk\"\n \"date\" \"id\" \"groups\" \"who\" \"users\" \"pinky\" \"uptime\" \"uname\" \"arch\"\n \"seq\" \"expr\" \"basenc\" \"base64\" \"base32\" \"od\"\n \"cksum\" \"md5sum\" \"sha1sum\" \"sha224sum\" \"sha256sum\" \"sha384sum\" \"sha512sum\"\n \"b2sum\" \"sum\"\n \"env\" \"timeout\" \"nice\" \"nohup\" \"chroot\" \"stdbuf\"\n \"truncate\" \"mkfifo\" \"mknod\" \"split\" \"csplit\" \"dd\" \"dircolors\"\n \"tsort\" \"shuf\" \"factor\" \"pr\" \"ptx\" \"stty\"\n \"chcon\" \"runcon\"\n \"dir\" \"vdir\" \"rev\" \"top\")))\n\n;; grep + Rust-backed PCRE2\n(let ([grep-pcre2-patch (format \"~a/patches/grep-pcre2.sls\" (current-directory))])\n (when (file-exists? grep-pcre2-patch)\n (system (format \"mkdir -p '~a/jerboa-coreutils/grep'\" coreutils-stage))\n (system (format \"cp '~a' '~a/jerboa-coreutils/grep/pcre2.sls'\"\n grep-pcre2-patch coreutils-stage))))\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (let ([pcre2-sls (format \"~a/jerboa-coreutils/grep/pcre2.sls\" coreutils-stage)])\n (when (file-exists? pcre2-sls)\n (printf \" Compiling grep/pcre2...~n\")\n (compile-library pcre2-sls)))\n (let ([grep-sls (format \"~a/jerboa-coreutils/grep.sls\" coreutils-stage)])\n (when (file-exists? grep-sls)\n (printf \" Compiling grep...~n\")\n (compile-library grep-sls))))\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: patch pcre2 to use Rust regex\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if has-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when has-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (library-directories))])\n (when has-aws?\n (printf \" Compiling jerboa-aws...~n\")\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0d: Stage jerboa-ssh for static build ==========\n(printf \"[0d/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build\n (system (format \"find '~a' -name '*.sls' -exec ~a 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage sed-inplace))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"~a 's/base64-encode/b64-encode/g' '~a'\" sed-inplace kh))\n (system (format \"~a 's/base64-decode/b64-decode/g' '~a'\" sed-inplace kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0e: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0e/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n(define vault-stage (format \"~a/vault-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" vault-stage vault-stage))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n(when has-jerboa-fuse?\n ;; Copy the jerboa-fuse library tree (chez/fuse/ and chez/vault/)\n (system (format \"mkdir -p '~a/chez/fuse' '~a/chez/vault'\" vault-stage vault-stage))\n ;; FUSE layer\n (system (format \"cp '~a/chez/fuse.sls' '~a/chez/fuse.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/constants.sls' '~a/chez/fuse/constants.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/types.sls' '~a/chez/fuse/types.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/codec.sls' '~a/chez/fuse/codec.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/mount.sls' '~a/chez/fuse/mount.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/access.sls' '~a/chez/fuse/access.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/secmem.sls' '~a/chez/fuse/secmem.sls'\" jerboa-fuse-dir vault-stage))\n ;; Vault layer\n (system (format \"cp '~a/chez/vault.sls' '~a/chez/vault.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/format.sls' '~a/chez/vault/format.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/crypto.sls' '~a/chez/vault/crypto.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/blockstore.sls' '~a/chez/vault/blockstore.sls'\" jerboa-fuse-dir vault-stage))\n ;; Patch out ALL load-shared-object calls (FUSE mount helper + libcrypto + libc)\n ;; Use (if #f #f) instead of (void) since some modules only import (rnrs)\n ;; Simple single-level calls:\n (system (format \"find '~a' -name '*.sls' -exec ~a 's/(load-shared-object[^)]*)/(if #f #f)/g' {} +\" vault-stage sed-inplace))\n ;; fuse.sls and blockstore.sls have multi-line (load-shared-object (case ...)) blocks\n ;; that the simple sed can't handle. Use Scheme to patch them out.\n (let ([str-has? (lambda (haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))])\n (for-each\n (lambda (file-path)\n (when (file-exists? file-path)\n (let* ([content (let ([p (open-input-file file-path)])\n (let loop ([lines '()])\n (let ([l (get-line p)])\n (if (eof-object? l)\n (begin (close-input-port p) (reverse lines))\n (loop (cons l lines))))))]\n [patched\n (let loop ([lines content] [acc '()] [skip 0])\n (if (null? lines)\n (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (or (str-has? line \"(define _libc-loaded\")\n (str-has? line \"(define libc-loaded\")))\n (let ([name (if (str-has? line \"_libc-loaded\")\n \"_libc-loaded\" \"libc-loaded\")])\n (loop (cdr lines)\n (cons (format \" (define ~a #t)\" name) acc)\n 1))]\n [(and (> skip 0) (str-has? line \"#t))\"))\n (loop (cdr lines) acc 0)]\n [(> skip 0)\n (loop (cdr lines) acc skip)]\n [else\n (loop (cdr lines) (cons line acc) 0)]))))])\n (let ([p (open-output-file file-path 'replace)])\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched)\n (close-output-port p)))))\n (list (format \"~a/chez/vault/blockstore.sls\" vault-stage)\n (format \"~a/chez/fuse.sls\" vault-stage)))) ;; close let\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up (format → crypto → secmem → mount → constants → types → codec → access → blockstore → fuse → vault)\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n ;; Format layer (no deps)\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n ;; FUSE foundation\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n ;; Secure memory + access control (depend on mount)\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n ;; Vault crypto (depends on format + libcrypto)\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n ;; Vault blockstore (depends on format + crypto + secmem)\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n ;; FUSE main (depends on all fuse sub-modules)\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n ;; Vault main (depends on everything)\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"~n[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(or (not (file-exists? so))\n (time>? (file-modification-time sls) (file-modification-time so)))\n (printf \" Compiling ~a...~n\" sls)\n (compile-library sls)]\n [else\n (printf \" (up to date) ~a~n\" sls)])))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n ;; Compat layer\n (compile-jsh-module \"../compat/gambit\")\n (for-each compile-jsh-module '(\"ffi\"))\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n (for-each compile-jsh-module '(\"conditions\" \"ast\" \"registry\"))\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-freebsd \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(parameterize ([compile-imported-libraries #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n (compile-program \"jsh-generated.ss\"))\n\n;; Verify jsh-generated.so was created\n(unless (file-exists? \"jsh-generated.so\")\n (fprintf (current-error-port) \"FATAL: jsh-generated.so was not created by compile-program~n\")\n (fprintf (current-error-port) \"Check for compilation errors above.~n\")\n (exit 1))\n\n;; ========== Step 3: Skip WPO ==========\n(printf \"[3/7] Skipping WPO (using jsh-generated.so directly)...~n\")\n(define program-so \"jsh-generated.so\")\n\n;; ========== Step 3.5: Pre-compile boot-file dependencies ==========\n\n(let ([boot-jerboa-modules\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\")])\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 2]\n [generate-inspector-information #f])\n (for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [so (format \"~a/~a.so\" jerboa-dir m)])\n (when (and (file-exists? sls) (not (file-exists? so)))\n (printf \" Pre-compiling ~a~n\" sls)\n (compile-library sls))))\n boot-jerboa-modules)))\n\n;; ========== Step 4: Create libs-only boot file ==========\n\n(printf \"[4/7] Creating libs-only boot file...~n\")\n\n;; Helper to filter existing .so files\n(define (existing-sos dir modules)\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" dir m)) modules)))\n\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (append\n ;; Jerboa runtime + stdlib\n (existing-sos jerboa-dir\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\"\n \"std/regex\"\n \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\"\n \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"))\n ;; Local compat layer\n (list \"src/compat/gambit.so\")\n ;; Additional jerboa stdlib\n (existing-sos jerboa-dir\n '(\"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n ;; Networking: rustls TLS + HTTP/HTTPS client (used by jerboa-aws)\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\"\n \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; jerboa-ssh (agent + client + sub-libraries)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" ssh-stage))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" ssh-stage m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" ssh-stage)))\n '())\n ;; Patched coreutils\n (existing-sos coreutils-stage\n '(\"jerboa-coreutils/common\" \"jerboa-coreutils/common/version\"\n \"jerboa-coreutils/common/security\"\n \"jerboa-coreutils/basename\" \"jerboa-coreutils/dirname\"\n \"jerboa-coreutils/link\" \"jerboa-coreutils/unlink\"\n \"jerboa-coreutils/yes\" \"jerboa-coreutils/printenv\"\n \"jerboa-coreutils/sleep\" \"jerboa-coreutils/whoami\"\n \"jerboa-coreutils/logname\" \"jerboa-coreutils/hostname\"\n \"jerboa-coreutils/nproc\" \"jerboa-coreutils/tty\"\n \"jerboa-coreutils/sync\" \"jerboa-coreutils/hostid\"\n \"jerboa-coreutils/cat\" \"jerboa-coreutils/head\"\n \"jerboa-coreutils/tail\" \"jerboa-coreutils/tac\"\n \"jerboa-coreutils/tee\" \"jerboa-coreutils/wc\"\n \"jerboa-coreutils/nl\" \"jerboa-coreutils/fold\"\n \"jerboa-coreutils/expand\" \"jerboa-coreutils/unexpand\"\n \"jerboa-coreutils/fmt\"\n \"jerboa-coreutils/cut\" \"jerboa-coreutils/paste\"\n \"jerboa-coreutils/join\" \"jerboa-coreutils/comm\"\n \"jerboa-coreutils/sort\" \"jerboa-coreutils/uniq\"\n \"jerboa-coreutils/tr\" \"jerboa-coreutils/numfmt\"\n \"jerboa-coreutils/mkdir\" \"jerboa-coreutils/rmdir\"\n \"jerboa-coreutils/mktemp\" \"jerboa-coreutils/touch\"\n \"jerboa-coreutils/readlink\" \"jerboa-coreutils/realpath\"\n \"jerboa-coreutils/ln\" \"jerboa-coreutils/cp\"\n \"jerboa-coreutils/mv\" \"jerboa-coreutils/rm\"\n \"jerboa-coreutils/install\" \"jerboa-coreutils/shred\"\n \"jerboa-coreutils/ls\" \"jerboa-coreutils/chmod\"\n \"jerboa-coreutils/chown\" \"jerboa-coreutils/chgrp\"\n \"jerboa-coreutils/stat\" \"jerboa-coreutils/du\"\n \"jerboa-coreutils/df\" \"jerboa-coreutils/pathchk\"\n \"jerboa-coreutils/date\" \"jerboa-coreutils/id\"\n \"jerboa-coreutils/groups\" \"jerboa-coreutils/who\"\n \"jerboa-coreutils/users\" \"jerboa-coreutils/pinky\"\n \"jerboa-coreutils/uptime\" \"jerboa-coreutils/uname\"\n \"jerboa-coreutils/arch\"\n \"jerboa-coreutils/seq\" \"jerboa-coreutils/expr\"\n \"jerboa-coreutils/basenc\" \"jerboa-coreutils/base64\"\n \"jerboa-coreutils/base32\" \"jerboa-coreutils/od\"\n \"jerboa-coreutils/cksum\" \"jerboa-coreutils/md5sum\"\n \"jerboa-coreutils/sha1sum\" \"jerboa-coreutils/sha224sum\"\n \"jerboa-coreutils/sha256sum\" \"jerboa-coreutils/sha384sum\"\n \"jerboa-coreutils/sha512sum\" \"jerboa-coreutils/b2sum\"\n \"jerboa-coreutils/sum\"\n \"jerboa-coreutils/env\" \"jerboa-coreutils/timeout\"\n \"jerboa-coreutils/nice\" \"jerboa-coreutils/nohup\"\n \"jerboa-coreutils/chroot\" \"jerboa-coreutils/stdbuf\"\n \"jerboa-coreutils/truncate\" \"jerboa-coreutils/mkfifo\"\n \"jerboa-coreutils/mknod\" \"jerboa-coreutils/split\"\n \"jerboa-coreutils/csplit\" \"jerboa-coreutils/dd\"\n \"jerboa-coreutils/dircolors\"\n \"jerboa-coreutils/tsort\" \"jerboa-coreutils/shuf\"\n \"jerboa-coreutils/factor\" \"jerboa-coreutils/pr\"\n \"jerboa-coreutils/ptx\" \"jerboa-coreutils/stty\"\n \"jerboa-coreutils/chcon\" \"jerboa-coreutils/runcon\"\n \"jerboa-coreutils/dir\" \"jerboa-coreutils/vdir\"\n \"jerboa-coreutils/rev\" \"jerboa-coreutils/top\"\n \"jerboa-coreutils/grep/pcre2\" \"jerboa-coreutils/grep\"))\n ;; jerboa-awk\n (existing-sos awk-stage\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed\n (existing-sos sed-stage\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-ssl + jerboa-https removed — jerboa-aws now uses (std net request) (rustls)\n ;; jerboa-fuse (vault)\n (if has-jerboa-fuse?\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" vault-stage m))\n '(\"chez/vault/format\" \"chez/fuse/constants\" \"chez/fuse/types\"\n \"chez/fuse/mount\" \"chez/fuse/codec\" \"chez/fuse/secmem\" \"chez/fuse/access\"\n \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\")))\n '())\n ;; jerboa-aws (if available)\n (if has-aws?\n (existing-sos aws-stage\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\" \"jerboa-aws/time\"\n \"jerboa-aws/crypto\" \"jerboa-aws/creds\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/request\" \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jsh modules\n (map (lambda (m) (format \"src/jsh/~a.so\" m))\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\" \"config\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"\n \"coreutils\"))))\n\n;; Verify jsh.boot was created\n(unless (file-exists? \"jsh.boot\")\n (fprintf (current-error-port) \"FATAL: jsh.boot was not created by make-boot-file~n\")\n (fprintf (current-error-port) \"Check for compilation/boot errors above.~n\")\n (exit 1))\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-freebsd-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n;; JSH_CROSS_CC overrides the compiler for cross-compilation (e.g. from a container)\n(define gcc (or (getenv \"JSH_CROSS_CC\") \"cc\"))\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" home-dir \"=~\"))\n\n;; Helper: write file as C byte array directly to output port (avoids O(n^2) string-append)\n(define (write-c-array filepath varname out)\n (let* ([bv (call-with-port (open-file-input-port filepath) get-bytevector-all)]\n [len (bytevector-length bv)]\n [hex \"0123456789abcdef\"])\n (fprintf out \"static const unsigned char ~a[] = {~n\" varname)\n (do ([i 0 (+ i 1)])\n ((= i len))\n (when (and (> i 0) (= (mod i 16) 0)) (display \",\\n\" out))\n (when (and (> i 0) (not (= (mod i 16) 0))) (display \",\" out))\n (display \"0x\" out)\n (let ([b (bytevector-u8-ref bv i)])\n (display (string-ref hex (fxsrl b 4)) out)\n (display (string-ref hex (fxand b 15)) out)))\n (fprintf out \"~n};~nstatic const unsigned int ~a_len = ~a;~n\" varname len)))\n\n;; Generate static_boot.c\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n (write-c-array petite-boot-path \"petite_boot\" out) (newline out)\n (write-c-array scheme-boot-path \"scheme_boot\" out) (newline out)\n (write-c-array \"jsh.boot\" \"jsh_boot\" out) (newline out)\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", jsh_boot, jsh_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n;; FFI symbol whitelist — auto-generated from ffi-shim.c plus a small set of\n;; non-ffi_ helpers (Cage/Landlock wrappers, Rust-native shims).\n(define ffi-shim-symbols\n (append (read-symbol-list \"ffi-shim-symbols.list\")\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\")))\n\n(define native-symbols\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\"\n \"jerboa_x509_cert_fingerprint\"\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n\n(define native-int-symbols\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n\n;; High-level jsh_* coreutils commands (from Rust jerboa-coreutils).\n;; On FreeBSD these are stubbed out — the Rust coreutils lib is not yet built.\n(define jsh-coreutils-commands\n '(\"jsh_arch\" \"jsh_b2sum\" \"jsh_base32\" \"jsh_base64\" \"jsh_basename\" \"jsh_basenc\"\n \"jsh_cat\" \"jsh_chgrp\" \"jsh_chmod\" \"jsh_chown\" \"jsh_chroot\" \"jsh_cksum\"\n \"jsh_comm\" \"jsh_cp\" \"jsh_csplit\" \"jsh_cu_realpath\" \"jsh_cut\" \"jsh_date\"\n \"jsh_dd\" \"jsh_df\" \"jsh_dir\" \"jsh_dircolors\" \"jsh_dirname\" \"jsh_du\"\n \"jsh_echo\" \"jsh_env\" \"jsh_expand\" \"jsh_expr\" \"jsh_factor\" \"jsh_fmt\"\n \"jsh_fold\" \"jsh_grep\" \"jsh_groups\" \"jsh_head\" \"jsh_hostid\" \"jsh_hostname\"\n \"jsh_id\" \"jsh_install\" \"jsh_join\" \"jsh_kill\" \"jsh_link\" \"jsh_ln\"\n \"jsh_logname\" \"jsh_ls\" \"jsh_md5sum\" \"jsh_mkdir\" \"jsh_mkfifo\" \"jsh_mknod\"\n \"jsh_mktemp\" \"jsh_mv\" \"jsh_nice\" \"jsh_nl\" \"jsh_nohup\" \"jsh_nproc\"\n \"jsh_numfmt\" \"jsh_od\" \"jsh_paste\" \"jsh_pathchk\" \"jsh_pinky\" \"jsh_pr\"\n \"jsh_printenv\" \"jsh_printf\" \"jsh_ptx\" \"jsh_pwd\" \"jsh_readlink\" \"jsh_rm\"\n \"jsh_rmdir\" \"jsh_seq\" \"jsh_sha1sum\" \"jsh_sha224sum\" \"jsh_sha256sum\"\n \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_shred\" \"jsh_shuf\" \"jsh_sleep\"\n \"jsh_sort\" \"jsh_split\" \"jsh_stat\" \"jsh_stty\" \"jsh_sum\" \"jsh_sync\"\n \"jsh_tac\" \"jsh_tail\" \"jsh_tee\" \"jsh_test\" \"jsh_timeout\" \"jsh_touch\"\n \"jsh_tr\" \"jsh_truncate\" \"jsh_tsort\" \"jsh_tty\" \"jsh_uname\" \"jsh_unexpand\"\n \"jsh_uniq\" \"jsh_unlink\" \"jsh_uptime\" \"jsh_users\" \"jsh_vdir\" \"jsh_wc\"\n \"jsh_who\" \"jsh_whoami\" \"jsh_yes\"))\n\n(define coreutils-symbols\n '(\"coreutils_chmod\" \"coreutils_lstat_mode\" \"coreutils_stat_isdir\"\n \"coreutils_chown\" \"coreutils_lchown\"\n \"coreutils_getpwnam_uid\" \"coreutils_getgrnam_gid\"\n \"coreutils_stat_call\" \"coreutils_stat_get\"\n \"coreutils_uid_to_name\" \"coreutils_gid_to_name\"\n \"coreutils_du_stat\" \"coreutils_statvfs\" \"coreutils_statvfs_get\"\n \"coreutils_test_access\" \"coreutils_test_stat\"\n \"coreutils_ls_lstat\" \"coreutils_ls_stat_get\" \"coreutils_ls_readlink\"\n \"coreutils_isatty\" \"coreutils_time_format\"\n \"coreutils_terminal_width\" \"coreutils_terminal_height\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_cp_lstat\" \"coreutils_cp_stat_get\" \"coreutils_cp_readlink\"\n \"coreutils_symlink\" \"coreutils_link\" \"coreutils_utime\"\n \"coreutils_mkdir\" \"coreutils_lstat_type\"\n \"coreutils_unlink\" \"coreutils_rmdir\" \"coreutils_access_w\"\n \"coreutils_rename\" \"coreutils_stat_get_mode\"\n \"coreutils_stat_atime\" \"coreutils_stat_mtime\"\n \"coreutils_file_size\" \"coreutils_fsync\"\n \"coreutils_chgrp_chown\" \"coreutils_chgrp_lchown\"\n \"coreutils_mkstemp\" \"coreutils_mkstemp_get_path\"\n \"coreutils_mkdtemp\" \"coreutils_readlink\" \"coreutils_realpath\"\n \"coreutils_stat_size\" \"coreutils_fsync_path\"))\n\n(define ssh-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n\n;; jerboa_ssh_crypto.c symbols (used by ssh/transport sub-library)\n(define ssh-crypto-symbols\n '(\"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\" \"jerboa_ssh_sha512\"\n \"jerboa_ssh_hmac_sha256\" \"jerboa_ssh_hmac_sha512\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_aes256_ctr_init\" \"jerboa_ssh_aes256_ctr_process\" \"jerboa_ssh_aes256_ctr_free\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_ed25519_sign\" \"jerboa_ssh_ed25519_derive_pubkey\"\n \"jerboa_ssh_tcp_connect\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_write\"\n \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_set_nodelay\"))\n\n;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n(define vault-fuse-symbols\n '(;; Secure memory\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n ;; Process tree\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n ;; FUSE device + mount\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n\n;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n;; jerboa_aead_seal, jerboa_aead_open — all in libjerboa_native (ring). No libcrypto needed.\n;; POSIX symbols needed by vault code (pread/pwrite for file I/O, fsync, uid/gid)\n(define vault-crypto-symbols\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\"))\n\n;; Generate jsh_main_freebsd.c\n(define program-c (format \"~a/jsh_main_freebsd.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include <dlfcn.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n\n ;; Embed program .so\n (write-c-array program-so \"jsh_program_data\" out)\n (newline out)\n\n ;; Declare static_boot_init\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n\n ;; Declare FFI symbols\n (display \"/* FFI symbols from ffi-shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n\n ;; Rust native symbols\n (when has-native-lib?\n (display \"\\n#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n native-symbols)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n native-int-symbols)\n (display \"#endif\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n\n ;; Coreutils FFI\n (display \"\\n/* FFI symbols from libcoreutils.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n coreutils-symbols)\n\n ;; High-level jsh_* coreutils commands (from Rust libjsh_coreutils.a)\n (display \"\\n/* jsh_* coreutils commands */\\n\" out)\n (if has-rust-coreutils?\n (begin\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n jsh-coreutils-commands))\n (begin\n (display \"/* Stubs — Rust coreutils not built */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"int ~a(int ac, const char **av) { return 127; }\\n\" name))\n jsh-coreutils-commands)\n (display \"void jsh_coreutils_init(int a, char **b) { }\\n\" out)))\n\n ;; jerboa-ssh (shim only; crypto symbols resolved lazily)\n (display \"\\n/* FFI symbols from jerboa_ssh_shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ssh-symbols)\n\n ;; jerboa-fuse vault (crypto symbols now from libjerboa_native via ring)\n (display \"/* FFI symbols for vault (from ffi-shim.c + libjerboa_native) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n vault-fuse-symbols)\n (newline out)\n\n ;; POSIX wrappers\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n\n ;; FreeBSD errno compatibility — __errno_location doesn't exist on FreeBSD\n (display \"/* FreeBSD errno compatibility */\\n\" out)\n (display \"static int *freebsd_errno_location(void) { return &errno; }\\n\\n\" out)\n\n ;; Stubs for symbols not available in FreeBSD native lib\n ;; (regex extended, epoll, inotify, landlock, seccomp)\n (display \"/* Stubs for Linux-only / missing native symbols */\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n (display \"void *jerboa_regex_compile_ex(const char *p, int f) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_find_at(void *r, const char *s, int o, int *ms, int *me) { return 0; }\\n\" out)\n (display \"char *jerboa_regex_captures(void *r, const char *s, int n) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_group_count(void *r) { return 0; }\\n\" out)\n (display \"int jerboa_epoll_create(void) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_ctl(int e, int o, int f, int ev) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_wait(int e, void *ev, int m, int t) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_close(int e) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_init(void) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_add_watch(int f, const char *p, int m) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_rm_watch(int f, int w) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_read(int f, void *b, int s) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_close(int f) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_create_ruleset(void) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_path_rule(int r, const char *p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_net_rule(int r, int p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_enforce(int r) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_available(void) { return 0; }\\n\" out)\n (display \"int jerboa_seccomp_lock(void) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_lock_strict(void) { return -1; }\\n\\n\" out)\n\n ;; register_ffi_symbols\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Rust native\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append native-symbols native-int-symbols))\n (display \"#endif\\n\" out))\n ;; POSIX\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"readlink\" \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"\n ;; vault blockstore\n \"flock\" \"pread\" \"pwrite\" \"fsync\"\n ;; top builtin\n \"setpriority\"))\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\" \"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; FreeBSD: __errno_location and __error → our FreeBSD wrapper\n ;; __errno_location is Linux glibc; __error is FreeBSD libc\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)freebsd_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)freebsd_errno_location);\\n\" out)\n ;; Register stub symbols for Linux-only / missing native functionality\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n ;; coreutils\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n coreutils-symbols)\n ;; jsh_* coreutils commands (stubs)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n jsh-coreutils-commands)\n (fprintf out \" Sforeign_symbol(\\\"jsh_coreutils_init\\\", (void*)jsh_coreutils_init);\\n\")\n ;; jerboa-ssh (shim symbols only; crypto symbols resolved lazily at runtime)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ssh-symbols)\n ;; jerboa-fuse vault\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-fuse-symbols)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-crypto-symbols)\n ;; Sockets\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"htons\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"))\n (display \"}\\n\\n\" out)\n\n ;; Custom main — FreeBSD\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; FreeBSD: sysctl for exe path\n (display \" /* Resolve exe path via sysctl (FreeBSD) */\\n\" out)\n (display \" {\\n\" out)\n (display \" int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" size_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (sysctl(mib, 4, exe_buf, &exe_len, NULL, 0) == 0) {\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; FreeBSD: Always use tmpfile since fdescfs (/dev/fd/) may not be mounted.\n ;; memfd_create exists on FreeBSD 13+ but /dev/fd/N requires fdescfs.\n (display \" /* FreeBSD: extract program .so to tmpfile */\\n\" out)\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-program-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program_data, 1, jsh_program_data_len, fp) != jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"fwrite tmpfile\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C ==========\n\n(printf \"[6/7] Compiling C with cc (clang)...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-freebsd \"Command failed\" cmd)))\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_freebsd.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_freebsd.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; landlock-shim.c — Landlock is Linux-only; always use stub on FreeBSD\n(begin\n (printf \" Landlock is Linux-only, generating stub for FreeBSD~n\")\n (system (format \"echo 'int ffi_landlock_abi_version(void) { return -1; } int ffi_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int ffi_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, int p) { return -1; } int ffi_landlock_create_ruleset(void) { return -1; } int ffi_landlock_add_path_rule(int a, const char *b, int c) { return -1; } int ffi_landlock_add_net_rule(int a, int b, int c) { return -1; } int ffi_landlock_enforce(int a) { return -1; } int jerboa_landlock_abi_version(void) { return -1; } int jerboa_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int jerboa_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, unsigned long long p) { return -1; }' | ~a -c -x c ~a -o '~a/landlock-shim.o' -\"\n gcc harden-cflags build-dir)))\n\n;; coreutils FFI shim\n(if (file-exists? coreutils-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir coreutils-shim))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; jerboa-ssh shim\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Use standalone ed25519 backend (Rust libjerboa_native provides the symbols)\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c — no longer compiled (OpenSSL removed);\n ;; SSH crypto symbols resolved lazily at runtime if SSH is used.\n ;; Generate empty .o placeholder for the linker.\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir)))\n\n (begin\n (printf \" Warning: jerboa-ssh shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-ssl shim — no longer compiled; TLS now via jerboa_tls_* (rustls)\n(printf \" [skip] jerboa-ssl shim — replaced by jerboa_tls_* (rustls)~n\")\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking static jsh-freebsd binary...~n\")\n\n;; FreeBSD static link compat: Rust coreutils references readdir_r@FBSD_1.5\n;; (versioned symbol from shared libc) but libc.a only has the unversioned symbol.\n;; Generate a small compat .o that provides the versioned symbol.\n(let ([compat-c (format \"~a/fbsd_compat.c\" build-dir)]\n [compat-o (format \"~a/fbsd_compat.o\" build-dir)])\n (call-with-output-file compat-c\n (lambda (out)\n (display \"#include <dirent.h>\\n\" out)\n (display \"__asm__(\\\".symver readdir_r_impl, readdir_r@FBSD_1.5\\\");\\n\" out)\n (display \"int readdir_r_impl(DIR *dirp, struct dirent *entry, struct dirent **result) {\\n\" out)\n (display \" return readdir_r(dirp, entry, result);\\n\" out)\n (display \"}\\n\" out)))\n (run-cmd (format \"~a -c -O2 -w -o '~a' '~a'\" gcc compat-o compat-c)))\n\n(let* ([objs (format \"~a/jsh_main_freebsd.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/coreutils-ffi.o ~a/landlock-shim.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o ~a/fbsd_compat.o\"\n build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir)]\n [native-flag (if has-native-lib? (format \" ~a\" native-lib-path) \"\")]\n [coreutils-flag (if has-rust-coreutils? (format \" ~a\" rust-coreutils-lib-path) \"\")]\n ;; Cross-compilation sysroot: when JSH_CROSS_CC is set, libraries are\n ;; under /freebsd/usr/lib/ instead of /usr/lib/\n [syslib (if (getenv \"JSH_CROSS_CC\") \"/freebsd/usr/lib\" \"/usr/lib\")]\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n [cxx-libs (if has-native-lib?\n (format \" ~a/libc++.a ~a/libcxxrt.a\" syslib syslib)\n \"\")]\n [link-libs (format \"-L~a -L~a -L/usr/local/lib -lkernel -lz -lm -lthr -liconv -lncursesw -luuid -llz4 -lutil\"\n chez-ta6fb syslib)]\n ;; Static link: libgcc_s has no .a — use libgcc.a + libgcc_eh.a instead\n ;; On cross-build with clang, these may not exist — clang uses compiler-rt\n [gcc-static (let ([gcc-a (format \"~a/libgcc.a\" syslib)])\n (if (file-exists? gcc-a)\n (format \" ~a/libgcc.a ~a/libgcc_eh.a\" syslib syslib)\n \"\"))]\n [link-cmd (format \"~a -static -o jsh-freebsd ~a~a~a~a ~a~a -Wl,--allow-multiple-definition\"\n gcc objs native-flag coreutils-flag cxx-libs link-libs gcc-static)])\n (printf \" ~a~n\" link-cmd)\n (run-cmd link-cmd))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? \"jsh-freebsd\")\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-freebsd\"))])\n (run-cmd \"strip jsh-freebsd\")\n (let ([post-size (file-length (open-file-input-port \"jsh-freebsd\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n ;; Compute SHA-256 integrity hash\n (printf \"[harden] Computing integrity hash...~n\")\n ;; FreeBSD uses sha256 -q (not sha256sum)\n (system \"sha256 -q jsh-freebsd | tr -d '\\\\n' > /tmp/_jsh_hash.txt 2>/dev/null || sha256sum jsh-freebsd | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\")\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-freebsd.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-freebsd.sha256 (32 bytes)~n\")))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n;; ssl-stage removed — jerboa-ssl/jerboa-https no longer used (rustls replaces them)\n(when has-aws? (system (format \"rm -rf '~a'\" aws-stage)))\n(system (format \"rm -rf '~a'\" awk-stage))\n(system (format \"rm -rf '~a'\" sed-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Static binary created: jsh-freebsd~n~n\")\n(system \"ls -lh jsh-freebsd\")\n(printf \"~n\")\n(system \"file jsh-freebsd\")\n(printf \"~nTest: ./jsh-freebsd -c 'echo Hello from static jsh'~n\")\n"} +{"text":";; FILE: jerboa-shell/build-jsh-freebsd.ss\n#!chezscheme\n;;; build-jsh-freebsd.ss — Build a fully static jsh binary on FreeBSD\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:... < build-jsh-freebsd.ss\n;;;\n;;; This script:\n;;; 1. Patches coreutils/awk/sed/ssl for static builds (no dlopen)\n;;; 2. Compiles jsh modules (using stock scheme)\n;;; 3. Creates boot file + optimized program .so\n;;; 4. Generates C files with embedded boot data\n;;; 5. Compiles C with cc (clang) against static Chez's scheme.h\n;;; 6. Links fully static binary with libkernel.a\n;;;\n;;; The resulting jsh-freebsd binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/home/freebsd\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-freebsd.sh so we know the repo root.\n(define vendor-dir\n (let ([script-dir (getenv \"SCRIPT_DIR\")])\n (if script-dir\n (format \"~a/vendor\" script-dir)\n (let ([cwd-vendor \"./vendor\"])\n (if (file-directory? cwd-vendor) cwd-vendor\n (format \"~a/jerboa-shell/vendor\" home-dir))))))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. Callers wrap with (or (getenv \"X\") (dep ...))\n;; to allow env var overrides from the shell script.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jerboa-sqlite/src\" home-dir)))\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (dep \"jerboa-coreutils\" \"lib\")))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n(define coreutils-shim\n (let ([upstream (dep-file \"jerboa-coreutils\" \"support/libcoreutils.c\")]\n [local \"patches/libcoreutils.c\"])\n (cond\n [(file-exists? upstream) upstream]\n [(file-exists? local) local]\n [else upstream])))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls).\n;; OpenSSL via load-shared-object cannot work in static builds and rustls is\n;; preferred for security.\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n;; Rust native library — resolve via vendor/ → ~/mine/ → ~/\n(define native-rs-dir\n (let* ([v (format \"~a/jerboa/jerboa-native-rs\" vendor-dir)]\n [m (format \"~a/mine/jerboa/jerboa-native-rs\" home-dir)]\n [h (format \"~a/jerboa/jerboa-native-rs\" home-dir)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n(define native-lib-path\n (format \"~a/target/release/libjerboa_native.a\" native-rs-dir))\n(define native-src-dir\n (format \"~a/src\" native-rs-dir))\n;; Sentinel file written after a successful native build without SQLite.\n;; If absent, the .a was built with default (tls-only) features — must rebuild.\n(define native-features-sentinel\n (format \"~a/target/release/.built-with-tls-crypto-no-sqlite\" native-rs-dir))\n(when (and (file-exists? native-src-dir)\n (or (not (file-exists? native-lib-path))\n ;; Features sentinel absent → stale build (wrong feature set)\n (not (file-exists? native-features-sentinel))\n ;; Check if any .rs file is newer than the .a\n (let ([lib-mtime (file-modification-time native-lib-path)])\n (let check ([files (directory-list native-src-dir)])\n (and (pair? files)\n (let ([f (format \"~a/~a\" native-src-dir (car files))])\n (or (and (> (string-length (car files)) 3)\n (string=? \".rs\" (substring (car files)\n (- (string-length (car files)) 3)\n (string-length (car files))))\n (time>? (file-modification-time f) lib-mtime))\n (check (cdr files)))))))))\n (printf \"~n[0/7] Rebuilding Rust native library (source newer than .a)...~n\")\n (let ([rc (safe-system (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote native-rs-dir)))])\n (unless (= rc 0)\n (fprintf (current-error-port) \"FATAL: cargo build --release --no-default-features --features tls,crypto failed~n\")\n (exit 1)))\n ;; Write sentinel so next build knows the right features were used\n (let ([port (open-output-file native-features-sentinel 'truncate)])\n (display \"tls,crypto,no-sqlite\\n\" port)\n (close-output-port port)))\n(when (and (file-exists? native-lib-path)\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(define has-native-lib? (file-exists? native-lib-path))\n\n;; Rust coreutils static library — check current dir first (container build), then home\n(define rust-coreutils-lib-path\n (let ([local (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" (current-directory))]\n [home-path (format \"~a/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\" home-dir)]\n [mine-path (format \"~a/mine/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\" home-dir)])\n (cond\n [(file-exists? local) local]\n [(file-exists? mine-path) mine-path]\n [else home-path])))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — coreutils builtins will be stubs~n\"))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\"))\n\n;; Chez Scheme static installation\n(define chez-ta6fb\n (or (getenv \"CHEZ_TA6FB\")\n (let ([dirs (directory-list \"/usr/local/lib\")])\n (let ([csv-dir (find (lambda (d) (string-prefix? \"csv\" d)) dirs)])\n (if csv-dir\n (format \"/usr/local/lib/~a/ta6fb\" csv-dir)\n (error 'build \"Cannot find Chez ta6fb directory in /usr/local/lib\"))))))\n\n(define scheme-h-dir chez-ta6fb)\n(define petite-boot-path (format \"~a/petite.boot\" chez-ta6fb))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-ta6fb))\n\n(printf \"Chez static: ~a~n\" chez-ta6fb)\n(printf \"Native lib: ~a~n\" (if has-native-lib? native-lib-path \"not found\"))\n(printf \"~n\")\n\n;; ========== Step 0: Patch coreutils for static builds ==========\n;; Coreutils modules call (load-shared-object #f) at library init time.\n;; In static builds, load-shared-object throws because dlopen is unavailable.\n;; Since FFI symbols are pre-registered via Sforeign_symbol, we patch these out.\n\n;; Detect sed -i syntax: FreeBSD uses `sed -i ''`, GNU sed uses `sed -i`\n(define sed-inplace\n (if (= 0 (system \"sed --version 2>/dev/null | head -1 | grep -q GNU\"))\n \"sed -i\" ;; GNU sed (Linux)\n \"sed -i ''\")) ;; BSD sed (FreeBSD/macOS)\n\n(printf \"[0/7] Patching coreutils for static build (no dlopen)...~n\")\n\n(define coreutils-stage (format \"~a/coreutils-stage\" (current-directory)))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(system (format \"mkdir -p '~a'\" coreutils-stage))\n\n(system (format \"cp -a '~a/jerboa-coreutils' '~a/'\"\n coreutils-dir coreutils-stage))\n;; Patch load-shared-object calls (incompatible with static linking)\n(system (format \"find '~a/jerboa-coreutils' -name '*.sls' -exec ~a 's/(load-shared-object #f)/(void)/g' {} +\"\n coreutils-stage sed-inplace))\n(system (format \"find '~a/jerboa-coreutils' -name '*.so' -delete\"\n coreutils-stage))\n(system (format \"find '~a/jerboa-coreutils' -name '*.wpo' -delete\"\n coreutils-stage))\n\n(printf \" Recompiling patched coreutils...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"common.sls\" \"common/version.sls\" \"common/io.sls\" \"common/security.sls\"))\n (for-each\n (lambda (name)\n (let ([sls (format \"~a/jerboa-coreutils/~a.sls\" coreutils-stage name)])\n (when (file-exists? sls)\n (compile-library sls))))\n '(\"basename\" \"dirname\" \"link\" \"unlink\" \"yes\" \"printenv\"\n \"sleep\" \"whoami\" \"logname\" \"hostname\" \"nproc\" \"tty\" \"sync\" \"hostid\"\n \"cat\" \"head\" \"tail\" \"tac\" \"tee\" \"wc\" \"nl\" \"fold\" \"expand\" \"unexpand\" \"fmt\"\n \"cut\" \"paste\" \"join\" \"comm\" \"sort\" \"uniq\" \"tr\" \"numfmt\"\n \"mkdir\" \"rmdir\" \"mktemp\" \"touch\" \"readlink\" \"realpath\" \"ln\" \"cp\" \"mv\" \"rm\"\n \"install\" \"shred\"\n \"ls\" \"chmod\" \"chown\" \"chgrp\" \"stat\" \"du\" \"df\" \"pathchk\"\n \"date\" \"id\" \"groups\" \"who\" \"users\" \"pinky\" \"uptime\" \"uname\" \"arch\"\n \"seq\" \"expr\" \"basenc\" \"base64\" \"base32\" \"od\"\n \"cksum\" \"md5sum\" \"sha1sum\" \"sha224sum\" \"sha256sum\" \"sha384sum\" \"sha512sum\"\n \"b2sum\" \"sum\"\n \"env\" \"timeout\" \"nice\" \"nohup\" \"chroot\" \"stdbuf\"\n \"truncate\" \"mkfifo\" \"mknod\" \"split\" \"csplit\" \"dd\" \"dircolors\"\n \"tsort\" \"shuf\" \"factor\" \"pr\" \"ptx\" \"stty\"\n \"chcon\" \"runcon\"\n \"dir\" \"vdir\" \"rev\" \"top\")))\n\n;; grep + Rust-backed PCRE2\n(let ([grep-pcre2-patch (format \"~a/patches/grep-pcre2.sls\" (current-directory))])\n (when (file-exists? grep-pcre2-patch)\n (system (format \"mkdir -p '~a/jerboa-coreutils/grep'\" coreutils-stage))\n (system (format \"cp '~a' '~a/jerboa-coreutils/grep/pcre2.sls'\"\n grep-pcre2-patch coreutils-stage))))\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (let ([pcre2-sls (format \"~a/jerboa-coreutils/grep/pcre2.sls\" coreutils-stage)])\n (when (file-exists? pcre2-sls)\n (printf \" Compiling grep/pcre2...~n\")\n (compile-library pcre2-sls)))\n (let ([grep-sls (format \"~a/jerboa-coreutils/grep.sls\" coreutils-stage)])\n (when (file-exists? grep-sls)\n (printf \" Compiling grep...~n\")\n (compile-library grep-sls))))\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: patch pcre2 to use Rust regex\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if has-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when has-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (library-directories))])\n (when has-aws?\n (printf \" Compiling jerboa-aws...~n\")\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0d: Stage jerboa-ssh for static build ==========\n(printf \"[0d/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build\n (system (format \"find '~a' -name '*.sls' -exec ~a 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage sed-inplace))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"~a 's/base64-encode/b64-encode/g' '~a'\" sed-inplace kh))\n (system (format \"~a 's/base64-decode/b64-decode/g' '~a'\" sed-inplace kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0e: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0e/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n(define vault-stage (format \"~a/vault-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" vault-stage vault-stage))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n(when has-jerboa-fuse?\n ;; Copy the jerboa-fuse library tree (chez/fuse/ and chez/vault/)\n (system (format \"mkdir -p '~a/chez/fuse' '~a/chez/vault'\" vault-stage vault-stage))\n ;; FUSE layer\n (system (format \"cp '~a/chez/fuse.sls' '~a/chez/fuse.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/constants.sls' '~a/chez/fuse/constants.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/types.sls' '~a/chez/fuse/types.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/codec.sls' '~a/chez/fuse/codec.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/mount.sls' '~a/chez/fuse/mount.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/access.sls' '~a/chez/fuse/access.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/secmem.sls' '~a/chez/fuse/secmem.sls'\" jerboa-fuse-dir vault-stage))\n ;; Vault layer\n (system (format \"cp '~a/chez/vault.sls' '~a/chez/vault.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/format.sls' '~a/chez/vault/format.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/crypto.sls' '~a/chez/vault/crypto.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/blockstore.sls' '~a/chez/vault/blockstore.sls'\" jerboa-fuse-dir vault-stage))\n ;; Patch out ALL load-shared-object calls (FUSE mount helper + libcrypto + libc)\n ;; Use (if #f #f) instead of (void) since some modules only import (rnrs)\n ;; Simple single-level calls:\n (system (format \"find '~a' -name '*.sls' -exec ~a 's/(load-shared-object[^)]*)/(if #f #f)/g' {} +\" vault-stage sed-inplace))\n ;; fuse.sls and blockstore.sls have multi-line (load-shared-object (case ...)) blocks\n ;; that the simple sed can't handle. Use Scheme to patch them out.\n (let ([str-has? (lambda (haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))])\n (for-each\n (lambda (file-path)\n (when (file-exists? file-path)\n (let* ([content (let ([p (open-input-file file-path)])\n (let loop ([lines '()])\n (let ([l (get-line p)])\n (if (eof-object? l)\n (begin (close-input-port p) (reverse lines))\n (loop (cons l lines))))))]\n [patched\n (let loop ([lines content] [acc '()] [skip 0])\n (if (null? lines)\n (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (or (str-has? line \"(define _libc-loaded\")\n (str-has? line \"(define libc-loaded\")))\n (let ([name (if (str-has? line \"_libc-loaded\")\n \"_libc-loaded\" \"libc-loaded\")])\n (loop (cdr lines)\n (cons (format \" (define ~a #t)\" name) acc)\n 1))]\n [(and (> skip 0) (str-has? line \"#t))\"))\n (loop (cdr lines) acc 0)]\n [(> skip 0)\n (loop (cdr lines) acc skip)]\n [else\n (loop (cdr lines) (cons line acc) 0)]))))])\n (let ([p (open-output-file file-path 'replace)])\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched)\n (close-output-port p)))))\n (list (format \"~a/chez/vault/blockstore.sls\" vault-stage)\n (format \"~a/chez/fuse.sls\" vault-stage)))) ;; close let\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up (format → crypto → secmem → mount → constants → types → codec → access → blockstore → fuse → vault)\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n ;; Format layer (no deps)\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n ;; FUSE foundation\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n ;; Secure memory + access control (depend on mount)\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n ;; Vault crypto (depends on format + libcrypto)\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n ;; Vault blockstore (depends on format + crypto + secmem)\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n ;; FUSE main (depends on all fuse sub-modules)\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n ;; Vault main (depends on everything)\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"~n[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(or (not (file-exists? so))\n (time>? (file-modification-time sls) (file-modification-time so)))\n (printf \" Compiling ~a...~n\" sls)\n (compile-library sls)]\n [else\n (printf \" (up to date) ~a~n\" sls)])))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n ;; Compat layer\n (compile-jsh-module \"../compat/gambit\")\n (for-each compile-jsh-module '(\"ffi\"))\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n (for-each compile-jsh-module '(\"conditions\" \"ast\" \"registry\"))\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-freebsd \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(parameterize ([compile-imported-libraries #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n (compile-program \"jsh-generated.ss\"))\n\n;; Verify jsh-generated.so was created\n(unless (file-exists? \"jsh-generated.so\")\n (fprintf (current-error-port) \"FATAL: jsh-generated.so was not created by compile-program~n\")\n (fprintf (current-error-port) \"Check for compilation errors above.~n\")\n (exit 1))\n\n;; ========== Step 3: Skip WPO ==========\n(printf \"[3/7] Skipping WPO (using jsh-generated.so directly)...~n\")\n(define program-so \"jsh-generated.so\")\n\n;; ========== Step 3.5: Pre-compile boot-file dependencies ==========\n\n(let ([boot-jerboa-modules\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\")])\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 2]\n [generate-inspector-information #f])\n (for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [so (format \"~a/~a.so\" jerboa-dir m)])\n (when (and (file-exists? sls) (not (file-exists? so)))\n (printf \" Pre-compiling ~a~n\" sls)\n (compile-library sls))))\n boot-jerboa-modules)))\n\n;; ========== Step 4: Create libs-only boot file ==========\n\n(printf \"[4/7] Creating libs-only boot file...~n\")\n\n;; Helper to filter existing .so files\n(define (existing-sos dir modules)\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" dir m)) modules)))\n\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (append\n ;; Jerboa runtime + stdlib\n (existing-sos jerboa-dir\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\"\n \"std/regex\"\n \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\"\n \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"))\n ;; Local compat layer\n (list \"src/compat/gambit.so\")\n ;; Additional jerboa stdlib\n (existing-sos jerboa-dir\n '(\"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n ;; Networking: rustls TLS + HTTP/HTTPS client (used by jerboa-aws)\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\"\n \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; jerboa-ssh (agent + client + sub-libraries)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" ssh-stage))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" ssh-stage m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" ssh-stage)))\n '())\n ;; Patched coreutils\n (existing-sos coreutils-stage\n '(\"jerboa-coreutils/common\" \"jerboa-coreutils/common/version\"\n \"jerboa-coreutils/common/security\"\n \"jerboa-coreutils/basename\" \"jerboa-coreutils/dirname\"\n \"jerboa-coreutils/link\" \"jerboa-coreutils/unlink\"\n \"jerboa-coreutils/yes\" \"jerboa-coreutils/printenv\"\n \"jerboa-coreutils/sleep\" \"jerboa-coreutils/whoami\"\n \"jerboa-coreutils/logname\" \"jerboa-coreutils/hostname\"\n \"jerboa-coreutils/nproc\" \"jerboa-coreutils/tty\"\n \"jerboa-coreutils/sync\" \"jerboa-coreutils/hostid\"\n \"jerboa-coreutils/cat\" \"jerboa-coreutils/head\"\n \"jerboa-coreutils/tail\" \"jerboa-coreutils/tac\"\n \"jerboa-coreutils/tee\" \"jerboa-coreutils/wc\"\n \"jerboa-coreutils/nl\" \"jerboa-coreutils/fold\"\n \"jerboa-coreutils/expand\" \"jerboa-coreutils/unexpand\"\n \"jerboa-coreutils/fmt\"\n \"jerboa-coreutils/cut\" \"jerboa-coreutils/paste\"\n \"jerboa-coreutils/join\" \"jerboa-coreutils/comm\"\n \"jerboa-coreutils/sort\" \"jerboa-coreutils/uniq\"\n \"jerboa-coreutils/tr\" \"jerboa-coreutils/numfmt\"\n \"jerboa-coreutils/mkdir\" \"jerboa-coreutils/rmdir\"\n \"jerboa-coreutils/mktemp\" \"jerboa-coreutils/touch\"\n \"jerboa-coreutils/readlink\" \"jerboa-coreutils/realpath\"\n \"jerboa-coreutils/ln\" \"jerboa-coreutils/cp\"\n \"jerboa-coreutils/mv\" \"jerboa-coreutils/rm\"\n \"jerboa-coreutils/install\" \"jerboa-coreutils/shred\"\n \"jerboa-coreutils/ls\" \"jerboa-coreutils/chmod\"\n \"jerboa-coreutils/chown\" \"jerboa-coreutils/chgrp\"\n \"jerboa-coreutils/stat\" \"jerboa-coreutils/du\"\n \"jerboa-coreutils/df\" \"jerboa-coreutils/pathchk\"\n \"jerboa-coreutils/date\" \"jerboa-coreutils/id\"\n \"jerboa-coreutils/groups\" \"jerboa-coreutils/who\"\n \"jerboa-coreutils/users\" \"jerboa-coreutils/pinky\"\n \"jerboa-coreutils/uptime\" \"jerboa-coreutils/uname\"\n \"jerboa-coreutils/arch\"\n \"jerboa-coreutils/seq\" \"jerboa-coreutils/expr\"\n \"jerboa-coreutils/basenc\" \"jerboa-coreutils/base64\"\n \"jerboa-coreutils/base32\" \"jerboa-coreutils/od\"\n \"jerboa-coreutils/cksum\" \"jerboa-coreutils/md5sum\"\n \"jerboa-coreutils/sha1sum\" \"jerboa-coreutils/sha224sum\"\n \"jerboa-coreutils/sha256sum\" \"jerboa-coreutils/sha384sum\"\n \"jerboa-coreutils/sha512sum\" \"jerboa-coreutils/b2sum\"\n \"jerboa-coreutils/sum\"\n \"jerboa-coreutils/env\" \"jerboa-coreutils/timeout\"\n \"jerboa-coreutils/nice\" \"jerboa-coreutils/nohup\"\n \"jerboa-coreutils/chroot\" \"jerboa-coreutils/stdbuf\"\n \"jerboa-coreutils/truncate\" \"jerboa-coreutils/mkfifo\"\n \"jerboa-coreutils/mknod\" \"jerboa-coreutils/split\"\n \"jerboa-coreutils/csplit\" \"jerboa-coreutils/dd\"\n \"jerboa-coreutils/dircolors\"\n \"jerboa-coreutils/tsort\" \"jerboa-coreutils/shuf\"\n \"jerboa-coreutils/factor\" \"jerboa-coreutils/pr\"\n \"jerboa-coreutils/ptx\" \"jerboa-coreutils/stty\"\n \"jerboa-coreutils/chcon\" \"jerboa-coreutils/runcon\"\n \"jerboa-coreutils/dir\" \"jerboa-coreutils/vdir\"\n \"jerboa-coreutils/rev\" \"jerboa-coreutils/top\"\n \"jerboa-coreutils/grep/pcre2\" \"jerboa-coreutils/grep\"))\n ;; jerboa-awk\n (existing-sos awk-stage\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed\n (existing-sos sed-stage\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-ssl + jerboa-https removed — jerboa-aws now uses (std net request) (rustls)\n ;; jerboa-fuse (vault)\n (if has-jerboa-fuse?\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" vault-stage m))\n '(\"chez/vault/format\" \"chez/fuse/constants\" \"chez/fuse/types\"\n \"chez/fuse/mount\" \"chez/fuse/codec\" \"chez/fuse/secmem\" \"chez/fuse/access\"\n \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\")))\n '())\n ;; jerboa-aws (if available)\n (if has-aws?\n (existing-sos aws-stage\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\" \"jerboa-aws/time\"\n \"jerboa-aws/crypto\" \"jerboa-aws/creds\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/request\" \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jsh modules\n (map (lambda (m) (format \"src/jsh/~a.so\" m))\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\" \"config\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"\n \"coreutils\"))))\n\n;; Verify jsh.boot was created\n(unless (file-exists? \"jsh.boot\")\n (fprintf (current-error-port) \"FATAL: jsh.boot was not created by make-boot-file~n\")\n (fprintf (current-error-port) \"Check for compilation/boot errors above.~n\")\n (exit 1))\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-freebsd-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n;; JSH_CROSS_CC overrides the compiler for cross-compilation (e.g. from a container)\n(define gcc (or (getenv \"JSH_CROSS_CC\") \"cc\"))\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" home-dir \"=~\"))\n\n;; Helper: write file as C byte array directly to output port (avoids O(n^2) string-append)\n(define (write-c-array filepath varname out)\n (let* ([bv (call-with-port (open-file-input-port filepath) get-bytevector-all)]\n [len (bytevector-length bv)]\n [hex \"0123456789abcdef\"])\n (fprintf out \"static const unsigned char ~a[] = {~n\" varname)\n (do ([i 0 (+ i 1)])\n ((= i len))\n (when (and (> i 0) (= (mod i 16) 0)) (display \",\\n\" out))\n (when (and (> i 0) (not (= (mod i 16) 0))) (display \",\" out))\n (display \"0x\" out)\n (let ([b (bytevector-u8-ref bv i)])\n (display (string-ref hex (fxsrl b 4)) out)\n (display (string-ref hex (fxand b 15)) out)))\n (fprintf out \"~n};~nstatic const unsigned int ~a_len = ~a;~n\" varname len)))\n\n;; Generate static_boot.c\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n (write-c-array petite-boot-path \"petite_boot\" out) (newline out)\n (write-c-array scheme-boot-path \"scheme_boot\" out) (newline out)\n (write-c-array \"jsh.boot\" \"jsh_boot\" out) (newline out)\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", jsh_boot, jsh_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n;; FFI symbol whitelist — auto-generated from ffi-shim.c plus a small set of\n;; non-ffi_ helpers (Cage/Landlock wrappers, Rust-native shims).\n(define ffi-shim-symbols\n (append (read-symbol-list \"ffi-shim-symbols.list\")\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\")))\n\n(define native-symbols\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\"\n \"jerboa_x509_cert_fingerprint\"\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n\n(define native-int-symbols\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n\n;; High-level jsh_* coreutils commands (from Rust jerboa-coreutils).\n;; On FreeBSD these are stubbed out — the Rust coreutils lib is not yet built.\n(define jsh-coreutils-commands\n '(\"jsh_arch\" \"jsh_b2sum\" \"jsh_base32\" \"jsh_base64\" \"jsh_basename\" \"jsh_basenc\"\n \"jsh_cat\" \"jsh_chgrp\" \"jsh_chmod\" \"jsh_chown\" \"jsh_chroot\" \"jsh_cksum\"\n \"jsh_comm\" \"jsh_cp\" \"jsh_csplit\" \"jsh_cu_realpath\" \"jsh_cut\" \"jsh_date\"\n \"jsh_dd\" \"jsh_df\" \"jsh_dir\" \"jsh_dircolors\" \"jsh_dirname\" \"jsh_du\"\n \"jsh_echo\" \"jsh_env\" \"jsh_expand\" \"jsh_expr\" \"jsh_factor\" \"jsh_fmt\"\n \"jsh_fold\" \"jsh_grep\" \"jsh_groups\" \"jsh_head\" \"jsh_hostid\" \"jsh_hostname\"\n \"jsh_id\" \"jsh_install\" \"jsh_join\" \"jsh_kill\" \"jsh_link\" \"jsh_ln\"\n \"jsh_logname\" \"jsh_ls\" \"jsh_md5sum\" \"jsh_mkdir\" \"jsh_mkfifo\" \"jsh_mknod\"\n \"jsh_mktemp\" \"jsh_mv\" \"jsh_nice\" \"jsh_nl\" \"jsh_nohup\" \"jsh_nproc\"\n \"jsh_numfmt\" \"jsh_od\" \"jsh_paste\" \"jsh_pathchk\" \"jsh_pinky\" \"jsh_pr\"\n \"jsh_printenv\" \"jsh_printf\" \"jsh_ptx\" \"jsh_pwd\" \"jsh_readlink\" \"jsh_rm\"\n \"jsh_rmdir\" \"jsh_seq\" \"jsh_sha1sum\" \"jsh_sha224sum\" \"jsh_sha256sum\"\n \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_shred\" \"jsh_shuf\" \"jsh_sleep\"\n \"jsh_sort\" \"jsh_split\" \"jsh_stat\" \"jsh_stty\" \"jsh_sum\" \"jsh_sync\"\n \"jsh_tac\" \"jsh_tail\" \"jsh_tee\" \"jsh_test\" \"jsh_timeout\" \"jsh_touch\"\n \"jsh_tr\" \"jsh_truncate\" \"jsh_tsort\" \"jsh_tty\" \"jsh_uname\" \"jsh_unexpand\"\n \"jsh_uniq\" \"jsh_unlink\" \"jsh_uptime\" \"jsh_users\" \"jsh_vdir\" \"jsh_wc\"\n \"jsh_who\" \"jsh_whoami\" \"jsh_yes\"))\n\n(define coreutils-symbols\n '(\"coreutils_chmod\" \"coreutils_lstat_mode\" \"coreutils_stat_isdir\"\n \"coreutils_chown\" \"coreutils_lchown\"\n \"coreutils_getpwnam_uid\" \"coreutils_getgrnam_gid\"\n \"coreutils_stat_call\" \"coreutils_stat_get\"\n \"coreutils_uid_to_name\" \"coreutils_gid_to_name\"\n \"coreutils_du_stat\" \"coreutils_statvfs\" \"coreutils_statvfs_get\"\n \"coreutils_test_access\" \"coreutils_test_stat\"\n \"coreutils_ls_lstat\" \"coreutils_ls_stat_get\" \"coreutils_ls_readlink\"\n \"coreutils_isatty\" \"coreutils_time_format\"\n \"coreutils_terminal_width\" \"coreutils_terminal_height\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_cp_lstat\" \"coreutils_cp_stat_get\" \"coreutils_cp_readlink\"\n \"coreutils_symlink\" \"coreutils_link\" \"coreutils_utime\"\n \"coreutils_mkdir\" \"coreutils_lstat_type\"\n \"coreutils_unlink\" \"coreutils_rmdir\" \"coreutils_access_w\"\n \"coreutils_rename\" \"coreutils_stat_get_mode\"\n \"coreutils_stat_atime\" \"coreutils_stat_mtime\"\n \"coreutils_file_size\" \"coreutils_fsync\"\n \"coreutils_chgrp_chown\" \"coreutils_chgrp_lchown\"\n \"coreutils_mkstemp\" \"coreutils_mkstemp_get_path\"\n \"coreutils_mkdtemp\" \"coreutils_readlink\" \"coreutils_realpath\"\n \"coreutils_stat_size\" \"coreutils_fsync_path\"))\n\n(define ssh-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n\n;; jerboa_ssh_crypto.c symbols (used by ssh/transport sub-library)\n(define ssh-crypto-symbols\n '(\"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\" \"jerboa_ssh_sha512\"\n \"jerboa_ssh_hmac_sha256\" \"jerboa_ssh_hmac_sha512\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_aes256_ctr_init\" \"jerboa_ssh_aes256_ctr_process\" \"jerboa_ssh_aes256_ctr_free\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_ed25519_sign\" \"jerboa_ssh_ed25519_derive_pubkey\"\n \"jerboa_ssh_tcp_connect\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_write\"\n \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_set_nodelay\"))\n\n;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n(define vault-fuse-symbols\n '(;; Secure memory\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n ;; Process tree\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n ;; FUSE device + mount\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n\n;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n;; jerboa_aead_seal, jerboa_aead_open — all in libjerboa_native (ring). No libcrypto needed.\n;; POSIX symbols needed by vault code (pread/pwrite for file I/O, fsync, uid/gid)\n(define vault-crypto-symbols\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\"))\n\n;; Generate jsh_main_freebsd.c\n(define program-c (format \"~a/jsh_main_freebsd.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include <dlfcn.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n\n ;; Embed program .so\n (write-c-array program-so \"jsh_program_data\" out)\n (newline out)\n\n ;; Declare static_boot_init\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n\n ;; Declare FFI symbols\n (display \"/* FFI symbols from ffi-shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n\n ;; Rust native symbols\n (when has-native-lib?\n (display \"\\n#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n native-symbols)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n native-int-symbols)\n (display \"#endif\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n\n ;; Coreutils FFI\n (display \"\\n/* FFI symbols from libcoreutils.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n coreutils-symbols)\n\n ;; High-level jsh_* coreutils commands (from Rust libjsh_coreutils.a)\n (display \"\\n/* jsh_* coreutils commands */\\n\" out)\n (if has-rust-coreutils?\n (begin\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n jsh-coreutils-commands))\n (begin\n (display \"/* Stubs — Rust coreutils not built */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"int ~a(int ac, const char **av) { return 127; }\\n\" name))\n jsh-coreutils-commands)\n (display \"void jsh_coreutils_init(int a, char **b) { }\\n\" out)))\n\n ;; jerboa-ssh (shim only; crypto symbols resolved lazily)\n (display \"\\n/* FFI symbols from jerboa_ssh_shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ssh-symbols)\n\n ;; jerboa-fuse vault (crypto symbols now from libjerboa_native via ring)\n (display \"/* FFI symbols for vault (from ffi-shim.c + libjerboa_native) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n vault-fuse-symbols)\n (newline out)\n\n ;; POSIX wrappers\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n\n ;; FreeBSD errno compatibility — __errno_location doesn't exist on FreeBSD\n (display \"/* FreeBSD errno compatibility */\\n\" out)\n (display \"static int *freebsd_errno_location(void) { return &errno; }\\n\\n\" out)\n\n ;; Stubs for symbols not available in FreeBSD native lib\n ;; (regex extended, epoll, inotify, landlock, seccomp)\n (display \"/* Stubs for Linux-only / missing native symbols */\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n (display \"void *jerboa_regex_compile_ex(const char *p, int f) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_find_at(void *r, const char *s, int o, int *ms, int *me) { return 0; }\\n\" out)\n (display \"char *jerboa_regex_captures(void *r, const char *s, int n) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_group_count(void *r) { return 0; }\\n\" out)\n (display \"int jerboa_epoll_create(void) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_ctl(int e, int o, int f, int ev) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_wait(int e, void *ev, int m, int t) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_close(int e) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_init(void) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_add_watch(int f, const char *p, int m) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_rm_watch(int f, int w) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_read(int f, void *b, int s) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_close(int f) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_create_ruleset(void) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_path_rule(int r, const char *p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_net_rule(int r, int p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_enforce(int r) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_available(void) { return 0; }\\n\" out)\n (display \"int jerboa_seccomp_lock(void) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_lock_strict(void) { return -1; }\\n\\n\" out)\n\n ;; register_ffi_symbols\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Rust native\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append native-symbols native-int-symbols))\n (display \"#endif\\n\" out))\n ;; POSIX\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"readlink\" \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"\n ;; vault blockstore\n \"flock\" \"pread\" \"pwrite\" \"fsync\"\n ;; top builtin\n \"setpriority\"))\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\" \"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; FreeBSD: __errno_location and __error → our FreeBSD wrapper\n ;; __errno_location is Linux glibc; __error is FreeBSD libc\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)freebsd_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)freebsd_errno_location);\\n\" out)\n ;; Register stub symbols for Linux-only / missing native functionality\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n ;; coreutils\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n coreutils-symbols)\n ;; jsh_* coreutils commands (stubs)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n jsh-coreutils-commands)\n (fprintf out \" Sforeign_symbol(\\\"jsh_coreutils_init\\\", (void*)jsh_coreutils_init);\\n\")\n ;; jerboa-ssh (shim symbols only; crypto symbols resolved lazily at runtime)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ssh-symbols)\n ;; jerboa-fuse vault\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-fuse-symbols)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-crypto-symbols)\n ;; Sockets\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"htons\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"))\n (display \"}\\n\\n\" out)\n\n ;; Custom main — FreeBSD\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; FreeBSD: sysctl for exe path\n (display \" /* Resolve exe path via sysctl (FreeBSD) */\\n\" out)\n (display \" {\\n\" out)\n (display \" int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" size_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (sysctl(mib, 4, exe_buf, &exe_len, NULL, 0) == 0) {\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; FreeBSD: Always use tmpfile since fdescfs (/dev/fd/) may not be mounted.\n ;; memfd_create exists on FreeBSD 13+ but /dev/fd/N requires fdescfs.\n (display \" /* FreeBSD: extract program .so to tmpfile */\\n\" out)\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-program-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program_data, 1, jsh_program_data_len, fp) != jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"fwrite tmpfile\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C ==========\n\n(printf \"[6/7] Compiling C with cc (clang)...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-freebsd \"Command failed\" cmd)))\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_freebsd.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_freebsd.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; landlock-shim.c — Landlock is Linux-only; always use stub on FreeBSD\n(begin\n (printf \" Landlock is Linux-only, generating stub for FreeBSD~n\")\n (system (format \"echo 'int ffi_landlock_abi_version(void) { return -1; } int ffi_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int ffi_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, int p) { return -1; } int ffi_landlock_create_ruleset(void) { return -1; } int ffi_landlock_add_path_rule(int a, const char *b, int c) { return -1; } int ffi_landlock_add_net_rule(int a, int b, int c) { return -1; } int ffi_landlock_enforce(int a) { return -1; } int jerboa_landlock_abi_version(void) { return -1; } int jerboa_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int jerboa_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, unsigned long long p) { return -1; }' | ~a -c -x c ~a -o '~a/landlock-shim.o' -\"\n gcc harden-cflags build-dir)))\n\n;; coreutils FFI shim\n(if (file-exists? coreutils-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir coreutils-shim))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; jerboa-ssh shim\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Use standalone ed25519 backend (Rust libjerboa_native provides the symbols)\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c — no longer compiled (OpenSSL removed);\n ;; SSH crypto symbols resolved lazily at runtime if SSH is used.\n ;; Generate empty .o placeholder for the linker.\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir)))\n\n (begin\n (printf \" Warning: jerboa-ssh shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-ssl shim — no longer compiled; TLS now via jerboa_tls_* (rustls)\n(printf \" [skip] jerboa-ssl shim — replaced by jerboa_tls_* (rustls)~n\")\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking static jsh-freebsd binary...~n\")\n\n;; FreeBSD static link compat: Rust coreutils references readdir_r@FBSD_1.5\n;; (versioned symbol from shared libc) but libc.a only has the unversioned symbol.\n;; Generate a small compat .o that provides the versioned symbol.\n(let ([compat-c (format \"~a/fbsd_compat.c\" build-dir)]\n [compat-o (format \"~a/fbsd_compat.o\" build-dir)])\n (call-with-output-file compat-c\n (lambda (out)\n (display \"#include <dirent.h>\\n\" out)\n (display \"__asm__(\\\".symver readdir_r_impl, readdir_r@FBSD_1.5\\\");\\n\" out)\n (display \"int readdir_r_impl(DIR *dirp, struct dirent *entry, struct dirent **result) {\\n\" out)\n (display \" return readdir_r(dirp, entry, result);\\n\" out)\n (display \"}\\n\" out)))\n (run-cmd (format \"~a -c -O2 -w -o '~a' '~a'\" gcc compat-o compat-c)))\n\n(let* ([objs (format \"~a/jsh_main_freebsd.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/coreutils-ffi.o ~a/landlock-shim.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o ~a/fbsd_compat.o\"\n build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir)]\n [native-flag (if has-native-lib? (format \" ~a\" native-lib-path) \"\")]\n [coreutils-flag (if has-rust-coreutils? (format \" ~a\" rust-coreutils-lib-path) \"\")]\n ;; Cross-compilation sysroot: when JSH_CROSS_CC is set, libraries are\n ;; under /freebsd/usr/lib/ instead of /usr/lib/\n [syslib (if (getenv \"JSH_CROSS_CC\") \"/freebsd/usr/lib\" \"/usr/lib\")]\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n [cxx-libs (if has-native-lib?\n (format \" ~a/libc++.a ~a/libcxxrt.a\" syslib syslib)\n \"\")]\n [link-libs (format \"-L~a -L~a -L/usr/local/lib -lkernel -lz -lm -lthr -liconv -lncursesw -luuid -llz4 -lutil\"\n chez-ta6fb syslib)]\n ;; Static link: libgcc_s has no .a — use libgcc.a + libgcc_eh.a instead\n ;; On cross-build with clang, these may not exist — clang uses compiler-rt\n [gcc-static (let ([gcc-a (format \"~a/libgcc.a\" syslib)])\n (if (file-exists? gcc-a)\n (format \" ~a/libgcc.a ~a/libgcc_eh.a\" syslib syslib)\n \"\"))]\n [link-cmd (format \"~a -static -o jsh-freebsd ~a~a~a~a ~a~a -Wl,--allow-multiple-definition\"\n gcc objs native-flag coreutils-flag cxx-libs link-libs gcc-static)])\n (printf \" ~a~n\" link-cmd)\n (run-cmd link-cmd))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? \"jsh-freebsd\")\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-freebsd\"))])\n (run-cmd \"strip jsh-freebsd\")\n (let ([post-size (file-length (open-file-input-port \"jsh-freebsd\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n ;; Compute SHA-256 integrity hash\n (printf \"[harden] Computing integrity hash...~n\")\n ;; FreeBSD uses sha256 -q (not sha256sum)\n (system \"sha256 -q jsh-freebsd | tr -d '\\\\n' > /tmp/_jsh_hash.txt 2>/dev/null || sha256sum jsh-freebsd | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\")\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-freebsd.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-freebsd.sha256 (32 bytes)~n\")))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n;; ssl-stage removed — jerboa-ssl/jerboa-https no longer used (rustls replaces them)\n(when has-aws? (system (format \"rm -rf '~a'\" aws-stage)))\n(system (format \"rm -rf '~a'\" awk-stage))\n(system (format \"rm -rf '~a'\" sed-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Static binary created: jsh-freebsd~n~n\")\n(system \"ls -lh jsh-freebsd\")\n(printf \"~n\")\n(system \"file jsh-freebsd\")\n(printf \"~nTest: ./jsh-freebsd -c 'echo Hello from static jsh'~n\")\n"} {"text":";; FILE: jerboa-shell/codex-findings.md\n# Codex Security Findings\n\nScope: manual source review plus targeted static scans of the `jerboa-shell` repository. I focused on the active shell, mux, embed, FFI, history, config, sandbox, redirection, and pipeline code paths. I did not include vendored or generated noise unless it is used by the runtime build.\n\nThis report intentionally references embedded sensitive file names but does not copy secret contents.\n\n## Critical Findings\n\n### 1. Private keys and operational secrets are embedded into generated source and likely the binary\n\nEvidence:\n- `src/jsh/embed-data.sls:800` embeds `.ssh/id_rsa`\n- `src/jsh/embed-data.sls:801` embeds `.ssh/id_rsa.pub`\n- `src/jsh/embed-data.sls:802` embeds `.ssh/known_hosts`\n- `src/jsh/embed-data.sls:806` embeds `keys/cert.pem`\n- `src/jsh/embed-data.sls:807` embeds `keys/key.pem`\n- `src/jsh/embed-data.sls:809` embeds `mullvad_account.txt`\n- `src/jsh/embed-data.sls:810` embeds `mullvad_wireguard_linux_cy_nic.zip`\n- `src/jsh/embed-data.sls:811` embeds `record.key`\n\nImpact: anyone who can read the generated source, build artifacts, crash dumps, backups, or final binary can recover private SSH keys, TLS key material, VPN credentials, and application keys. This is immediate credential compromise, not just a defense-in-depth issue.\n\nRecommendation: remove all private material from embed generation, rotate every exposed credential, and make embed packaging fail closed on sensitive path patterns such as `.ssh/`, `id_rsa`, `key.pem`, `record.key`, account files, and VPN archives. Public assets can remain embedded, but private runtime secrets should be provisioned separately through OS keychains, restricted config files, or explicit user-supplied paths.\n\n### 2. The mux mTLS trust model embeds the signing key used to mint trusted certificates\n\nEvidence:\n- `src/jsh/mux-transport.sls:527` reads embedded CA certificate data.\n- `src/jsh/mux-transport.sls:528` reads embedded CA key data.\n- `src/jsh/mux-transport.sls:533` to `src/jsh/mux-transport.sls:535` generates an ephemeral leaf certificate from that embedded CA key and connects using the embedded CA.\n- `src/jsh/mux-transport.sls:586` to `src/jsh/mux-transport.sls:600` documents the design: clients generate per-instance leaves signed by the embedded CA.\n\nImpact: possession of one binary containing the embedded CA private key is enough to mint certificates trusted by every peer using the same embedded CA. mTLS authenticates only \"has a copy of the binary/key,\" not a unique local identity.\n\nRecommendation: never ship the CA private key in the client. Generate per-install identities locally, store the private key with strict permissions, pin peer certificates or trust a local CA generated during first setup, and rotate the current CA. If mux is local-only, consider replacing this with Unix socket permissions plus explicit auth tokens instead of self-signing with a shared embedded key.\n\n## High Findings\n\n### 3. Non-Linux startup extracts executable code to a predictable temp path\n\nEvidence:\n- `jsh-main.c:129` to `jsh-main.c:142` builds `$TMPDIR/.jsh-program-<pid>.so` and writes it with `fopen`.\n- `jsh-main.c:162` unlinks the path after loading.\n\nImpact: on fallback platforms, a predictable filename plus `fopen(\"wb\")` follows symlinks and can race with another same-user process. Depending on directory ownership and permissions, this can overwrite unintended files or allow replacement of the extracted code before load.\n\nRecommendation: use `mkstemp` or `open` with `O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC`, mode `0600`, and operate through the file descriptor. Prefer unlinking immediately after opening where the platform supports it. Verify ownership and mode of the temporary directory before use.\n\n### 4. Mux server names are not validated before being used in socket and pidfile paths\n\nEvidence:\n- `jsh.ss:1131` to `jsh.ss:1134` accepts `--name` for remote mux invocation.\n- `jsh.ss:3320` to `jsh.ss:3321` accepts `--name` for mux server startup.\n- `src/jsh/mux-server.sls:65` to `src/jsh/mux-server.sls:69` constructs socket and pidfile paths with raw `name`.\n- `src/jsh/mux-server.sls:338` to `src/jsh/mux-server.sls:340` writes the pidfile and listens on the computed socket path.\n\nImpact: names containing `/`, `..`, control characters, or odd path syntax can escape the intended mux directory or collide with unintended files. This amplifies the socket unlink and pidfile issues below.\n\nRecommendation: centralize mux-name validation and reject anything except a small portable set such as `[A-Za-z0-9_.-]+`. Reject empty names, path separators, `.`/`..`, leading dashes if names are later passed to commands, and overly long names. Apply validation before computing any path.\n\n### 5. Unix socket listener unconditionally unlinks the requested path\n\nEvidence:\n- `ffi-shim.c:2204` defines `ffi_stream_listen_unix`.\n- `ffi-shim.c:2205` calls `unlink(path)` before binding.\n\nImpact: with unvalidated mux names, this can remove arbitrary user-writable paths reachable through path traversal. Even with validation, blindly unlinking is risky if the mux directory is compromised or if a stale path is not actually a socket.\n\nRecommendation: validate mux names first. Before unlinking a stale path, `lstat` it and require that it is a Unix socket owned by the current uid. Do not unlink regular files, symlinks, directories, or files owned by another uid. Use `O_NOFOLLOW`/`lstat` style checks consistently for companion files.\n\n### 6. Mux passwords are passed through the process environment\n\nEvidence:\n- `jsh.ss:3278` to `jsh.ss:3287` stores the password in `_JSH_MUX_PW`, forks/execs the child, then clears the parent environment.\n\nImpact: environment variables can be exposed through process inspection, crash reports, debugging tools, shell wrappers, and child process inheritance. Clearing the parent after fork does not remove the secret from the child environment.\n\nRecommendation: pass secrets over an inherited pipe, socketpair, or other fd-based channel. Mark unrelated fds close-on-exec and scrub buffers after use. If an environment fallback remains, warn clearly and make it opt-in.\n\n### 7. Embedded certificate and key material can be written back to disk with predictable names\n\nEvidence:\n- `jsh.ss:1071` to `jsh.ss:1096` defines `mux-resolve-embed-to-file`.\n- `jsh.ss:1087` constructs a hidden output path from the embedded file name.\n- `jsh.ss:1089` to `jsh.ss:1094` writes embedded bytes to that path without exclusive creation, permission setting, or cleanup.\n\nImpact: private certificate or key material can land on disk under predictable names such as a hidden file in the mux directory. The code relies on surrounding directory permissions but does not itself enforce safe file creation semantics.\n\nRecommendation: avoid writing private embedded material to disk. If a library requires paths, write to a unique `0600` file created with exclusive/no-follow semantics and delete it reliably. Prefer fd-based APIs or in-memory TLS configuration where possible.\n\n### 8. Sandbox safe-eval reads input before disabling reader features\n\nEvidence:\n- `src/jsh/sandbox.sls:252` to `src/jsh/sandbox.sls:255` parses untrusted expression text with `read` and then evaluates it in `*safe-eval-bindings*`.\n\nImpact: the restricted evaluation environment only applies after reading. If read-time evaluation or unsafe reader extensions are enabled, code may execute or allocate unexpectedly before the sandbox binding set is used.\n\nRecommendation: wrap reads of untrusted expressions with `(parameterize ([read-eval #f]) ...)` if available in this runtime, or use a safe reader/parser that only accepts the intended expression subset. Apply the same hardening to any helper that reads user-provided Scheme text before validation.\n\n## Medium Findings\n\n### 9. Interactive Scheme eval is intentionally powerful but should be treated as unsandboxed\n\nEvidence:\n- `src/jsh/script.sls:100` to `src/jsh/script.sls:118` reads and evaluates comma-prefixed Scheme input.\n\nImpact: this appears to be an intentional shell feature. It is safe only when the input is fully trusted. It should not be reachable through scripts, mux commands, or automation channels that handle untrusted input.\n\nRecommendation: document this as trusted-local-code execution. If the feature can be invoked remotely or through mux, gate it behind an explicit unsafe mode or route it through the hardened safe evaluator.\n\n### 10. Blocking FFI calls are declared without collect-safe wrappers\n\nEvidence:\n- `src/jsh/mux-transport.sls:110` to `src/jsh/mux-transport.sls:126` declares blocking TLS connect, accept, read, and write calls as plain `foreign-procedure`.\n- `src/jsh/ffi.sls:696` to `src/jsh/ffi.sls:700` declares bytevector write/send/recv wrappers as plain `foreign-procedure`.\n- `src/jsh/rl.sls:18` to `src/jsh/rl.sls:21` declares `waitpid`, `read`, and `write` as plain `foreign-procedure`.\n\nImpact: long-running or peer-controlled blocking calls can pin the Chez runtime and interfere with GC or other fibers/threads. For mux and network-facing code, slow clients can become a denial-of-service vector.\n\nRecommendation: use collect-safe foreign calls for blocking C APIs where supported, or move blocking operations to nonblocking/evented wrappers. Add timeouts around peer-controlled TLS and stream operations.\n\n### 11. PID files are written with permissive mode and symlink-following semantics\n\nEvidence:\n- `ffi-shim.c:2812` to `ffi-shim.c:2822` opens pidfile paths with `O_WRONLY | O_CREAT | O_TRUNC` and mode `0644`.\n\nImpact: pidfiles may disclose process information and can be clobbered through symlink/path manipulation if the containing directory or mux name is unsafe. This also compounds the unvalidated mux-name issue.\n\nRecommendation: use `0600`, `O_NOFOLLOW`, `O_CLOEXEC`, and preferably atomic write-plus-rename. Validate the containing directory owner and permissions before writing.\n\n### 12. Existing mux runtime directories are not validated before use\n\nEvidence:\n- `src/jsh/mux-server.sls:58` to `src/jsh/mux-server.sls:63` derives the mux socket directory from `XDG_RUNTIME_DIR` or `HOME`.\n- `src/jsh/mux-server.sls:336` creates the directory with mode `0700`, but this does not prove an already-existing directory is safe.\n\nImpact: if the directory already exists with unsafe ownership or permissions, socket and pidfile operations may happen in a location controlled or observable by another user/process.\n\nRecommendation: after creating or discovering the directory, `stat` it and require directory type, current uid ownership, and no group/world permissions. Fail closed if the checks do not pass.\n\n### 13. Command history can persist secrets typed as arguments\n\nEvidence:\n- `jsh.ss:2533` to `jsh.ss:2534` parses `ssh --password`.\n- `jsh.ss:2744` to `jsh.ss:2745` parses `scp --password`.\n- `src/jsh/history.sls:548` to `src/jsh/history.sls:568` persists history entries to the configured history file.\n\nImpact: commands containing passwords, tokens, API keys, or one-off secrets may be stored in plaintext history. The history file is intended to be chmodded to `0600`, but persistence still expands the lifetime of secrets and exposes them to backups and local compromise.\n\nRecommendation: add history redaction and ignore rules for secret-bearing flags and environment assignments. Prefer prompting for passwords instead of accepting them on the command line. Do not store entries containing `--password`, `token=`, `AWS_SECRET_ACCESS_KEY=`, private key material, or similar patterns.\n\n### 14. Sandbox fallback appears to continue without sandboxing when platform support is missing\n\nEvidence:\n- `ffi-shim.c:1371` to `ffi-shim.c:1379` attempts to apply Landlock and logs that unsupported kernels continue without sandbox enforcement.\n\nImpact: a caller requesting sandboxing may receive an unsandboxed process on unsupported platforms or kernels. That is a policy bypass if users or higher-level code rely on sandbox success for safety.\n\nRecommendation: fail closed by default when sandbox setup is requested but unavailable. Provide an explicit option such as `--allow-unsandboxed-fallback` for compatibility workflows.\n\n## Low Findings and Hardening Items\n\n### 15. Debug logging can write to an environment-selected path\n\nEvidence:\n- `src/jsh/mux-server.sls:37` to `src/jsh/mux-server.sls:46` opens the path from `MUX_SERVER_DEBUG` for append logging.\n\nImpact: this is mostly user-controlled behavior, but it can leak mux metadata to unintended files and follows normal path semantics, including symlinks.\n\nRecommendation: treat this as a development-only feature. If retained, create logs with safe file-open flags where possible and avoid logging secrets or authentication material.\n\n### 16. Redirection and pipeline code should use exception-safe cleanup uniformly\n\nEvidence:\n- `redirect.ss:249` to `redirect.ss:253` opens redirection ports and mutates current ports.\n- `redirect.ss:263` to `redirect.ss:266` restores current ports after executing the command.\n- `redirect.ss:735` to `redirect.ss:740` and `redirect.ss:750` to `redirect.ss:752` contain similar open/restore patterns.\n- `pipeline.ss:123` to `pipeline.ss:143` and `pipeline.ss:288` to `pipeline.ss:318` open `/dev/fd/N` ports and close them after command execution.\n\nImpact: many paths close ports explicitly, but not all mutations are visibly protected by `dynamic-wind`, `unwind-protect`, or `with-resource` at the same abstraction level. Exceptions during setup or command execution can leave ports/fds open or current ports temporarily wrong.\n\nRecommendation: wrap every fd/port acquisition and current-port mutation in a single exception-safe helper. Jerboa already has `unwind-protect` and `with-resource`; using those consistently would make leaks and restoration bugs easier to audit.\n\n### 17. Config file loading silently ignores all errors\n\nEvidence:\n- `src/jsh/config.sls:33` to `src/jsh/config.sls:39` loads `~/.jsh/config` under a broad guard that suppresses every condition.\n\nImpact: malformed config, permission problems, or unsafe config behavior can fail silently. Security-relevant config may appear to be active when it was ignored.\n\nRecommendation: distinguish \"file absent\" from parse/runtime errors. Warn on malformed config and consider rejecting group/world-writable config files before loading.\n\n## Suggested Remediation Order\n\n1. Remove and rotate embedded private material immediately. Redesign mux identity so no shared CA private key ships in the binary.\n2. Validate mux names and harden all socket, pidfile, and temp-file creation before any more mux features are added.\n3. Replace environment-based password passing with fd-based transfer.\n4. Harden all untrusted Scheme reads with read-time-eval disabled or a restricted parser.\n5. Add collect-safe or nonblocking wrappers for blocking FFI calls used by mux and stream operations.\n6. Add history redaction for secret-bearing commands and make password prompts the preferred path.\n7. Normalize fd/port cleanup with `with-resource`, `unwind-protect`, or a local helper.\n\n## Notes\n\n- `src/jsh/embed-data.sls` is generated-style Scheme, but it is part of the runtime source tree and currently contains sensitive bytevectors. The correct fix is upstream in the embed generation inputs and filters, not manual editing of only the generated file.\n- The comma Scheme evaluator appears intentional. I counted it as a trust-boundary warning rather than a standalone vulnerability.\n- Several issues compound: unvalidated mux names make unlink, pidfile, and embedded-file extraction behaviors materially more dangerous than they would be in a strictly private, validated runtime directory.\n"} {"text":";; FILE: jerboa-shell/prompt.ss\n;;; prompt.ss — Prompt expansion (PS1/PS2/PS4) for gsh\n\n(export #t)\n(import :std/sugar\n :std/format\n :jsh/ffi\n :jsh/util\n (only-in :jsh/expander find-matching-paren))\n\n;;; --- Git branch helper (reads .git/HEAD directly, no external commands) ---\n\n(def (git-branch-name)\n (let loop ([dir (current-directory)])\n (let ([head-path (string-append dir \"/.git/HEAD\")])\n (if (file-exists? head-path)\n (with-catch\n (lambda (e) #f)\n (lambda ()\n (let* ([content (call-with-input-file head-path read-line)]\n [prefix \"ref: refs/heads/\"])\n (if (and (string? content)\n (>= (string-length content) (string-length prefix))\n (string=? prefix (substring content 0 (string-length prefix))))\n (substring content (string-length prefix) (string-length content))\n ;; Detached HEAD — show short hash\n (if (and (string? content) (>= (string-length content) 7))\n (substring content 0 7)\n #f)))))\n ;; Walk up to parent directory\n (let ([parent (path-directory dir)])\n (if (or (not parent) (string=? parent dir) (string=? parent \"/\"))\n #f\n (loop parent)))))))\n\n;;; --- Public interface ---\n\n;; Expand prompt escape sequences in a PS string\n;; env-get: (lambda (name) -> string or #f)\n;; job-count: number of active jobs\n;; cmd-number: command number\n;; history-number: history number\n;; cmd-exec-fn: optional (lambda (cmd-string) -> output-string) for $(...) expansion\n(def (expand-prompt ps-string env-get\n (job-count 0)\n (cmd-number 0)\n (history-number 0)\n (cmd-exec-fn #f)\n (ssh-key-count 0))\n (let ([len (string-length ps-string)]\n [out (open-output-string)])\n (let loop ([i 0])\n (cond\n ((>= i len)\n (get-output-string out))\n ;; Command substitution $(...)\n ((and cmd-exec-fn\n (char=? (string-ref ps-string i) #\\$)\n (< (+ i 1) len)\n (char=? (string-ref ps-string (+ i 1)) #\\())\n (let ([close (find-matching-paren ps-string (+ i 2))])\n (if close\n (let* ([cmd-str (substring ps-string (+ i 2) close)]\n [output (with-catch\n (lambda (e) \"\") ;; Silently ignore errors in prompt commands\n (lambda () (cmd-exec-fn cmd-str)))])\n (display output out)\n (loop (+ close 1)))\n ;; No matching ) - output literally\n (begin\n (display \"$(\" out)\n (loop (+ i 2))))))\n ;; Backslash escape sequence\n ((and (char=? (string-ref ps-string i) #\\\\)\n (< (+ i 1) len))\n (let ([ch (string-ref ps-string (+ i 1))])\n (case ch\n ;; Username\n ((#\\u)\n (display (or (env-get \"USER\") (user-name)) out)\n (loop (+ i 2)))\n ;; Hostname (short)\n ((#\\h)\n (let* ([host (or (env-get \"HOSTNAME\")\n (with-catch (lambda (e) \"localhost\")\n (lambda () (hostname-short))))]\n [dot (string-index host #\\.)])\n (display (if dot (substring host 0 dot) host) out)\n (loop (+ i 2))))\n ;; Hostname (full)\n ((#\\H)\n (display (or (env-get \"HOSTNAME\")\n (with-catch (lambda (e) \"localhost\")\n (lambda () (hostname-short))))\n out)\n (loop (+ i 2)))\n ;; Working directory with ~ for home\n ((#\\w)\n (let* ([pwd (or (env-get \"PWD\") (current-directory))]\n [home (or (env-get \"HOME\") \"\")]\n [display-pwd (if (and (> (string-length home) 0)\n (string-prefix? home pwd))\n (string-append \"~\" (substring pwd (string-length home)\n (string-length pwd)))\n pwd)])\n (display display-pwd out)\n (loop (+ i 2))))\n ;; Basename of working directory\n ((#\\W)\n (let* ([pwd (or (env-get \"PWD\") (current-directory))]\n [home (or (env-get \"HOME\") \"\")])\n (if (string=? pwd home)\n (display \"~\" out)\n (display (path-basename pwd) out))\n (loop (+ i 2))))\n ;; Date\n ((#\\d)\n ;; Simplified: just show date\n (display (date-string) out)\n (loop (+ i 2)))\n ;; Time formats\n ((#\\t) ;; 24h HH:MM:SS\n (display (time-string-24h) out)\n (loop (+ i 2)))\n ((#\\T) ;; 12h HH:MM:SS\n (display (time-string-12h) out)\n (loop (+ i 2)))\n ((#\\@) ;; 12h am/pm\n (display (time-string-ampm) out)\n (loop (+ i 2)))\n ((#\\A) ;; 24h HH:MM\n (display (time-string-hhmm) out)\n (loop (+ i 2)))\n ;; Newline / carriage return\n ((#\\n) (display \"\\n\" out) (loop (+ i 2)))\n ((#\\r) (display \"\\r\" out) (loop (+ i 2)))\n ;; Shell name\n ((#\\s)\n (display \"jsh\" out)\n (loop (+ i 2)))\n ;; Shell version\n ((#\\v)\n (display (or (env-get \"JSH_VERSION_SHORT\") \"0.2\") out)\n (loop (+ i 2)))\n ((#\\V)\n (display (or (env-get \"JSH_VERSION\") \"0.2.0\") out)\n (loop (+ i 2)))\n ;; Number of jobs\n ((#\\j)\n (display (number->string job-count) out)\n (loop (+ i 2)))\n ;; Terminal basename\n ((#\\l)\n (display \"tty\" out)\n (loop (+ i 2)))\n ;; Command number\n ((#\\#)\n (display (number->string cmd-number) out)\n (loop (+ i 2)))\n ;; History number\n ((#\\!)\n (display (number->string history-number) out)\n (loop (+ i 2)))\n ;; $ or # (root check)\n ((#\\$)\n (display (if (= (ffi-geteuid) 0) \"#\" \"$\") out)\n (loop (+ i 2)))\n ;; Bell\n ((#\\a)\n (display \"\\007\" out)\n (loop (+ i 2)))\n ;; Literal backslash\n ((#\\\\)\n (display \"\\\\\" out)\n (loop (+ i 2)))\n ;; strftime format \\D{format}\n ((#\\D)\n (if (and (< (+ i 2) len) (char=? (string-ref ps-string (+ i 2)) #\\{))\n (let ([close (string-index-from ps-string #\\} (+ i 3))])\n (if close\n (begin\n ;; Simplified: just show ISO date\n (display (date-string) out)\n (loop (+ close 1)))\n (begin\n (display \"\\\\D\" out)\n (loop (+ i 2)))))\n (begin\n (display \"\\\\D\" out)\n (loop (+ i 2)))))\n ((#\\]) (loop (+ i 2)))\n ;; \\g = bare branch name, \\G = \" (branch)\" with decoration\n ((#\\g)\n (let ([branch (git-branch-name)])\n (when branch (display branch out)))\n (loop (+ i 2)))\n ((#\\G)\n (let ([branch (git-branch-name)])\n (when branch\n (display \" (\" out)\n (display branch out)\n (display \")\" out)))\n (loop (+ i 2)))\n ;; SSH key count (\\K) — N when N>0 keys in agent, empty string when 0\n ((#\\K)\n (when (> ssh-key-count 0)\n (display (number->string ssh-key-count) out))\n (loop (+ i 2)))\n ;; Non-printing delimiters (bash \\[ \\]) — skip\n ((#\\[) (loop (+ i 2)))\n ((#\\]) (loop (+ i 2)))\n ;; Unknown escape: output literally\n (else\n (display \"\\\\\" out)\n (display (string ch) out)\n (loop (+ i 2))))))\n ;; Regular character\n (else\n (display (string (string-ref ps-string i)) out)\n (loop (+ i 1)))))))\n\n;; Calculate visible width of a prompt (excluding \\(...\\) non-printing sequences)\n(def (prompt-width prompt-string)\n (let ([len (string-length prompt-string)])\n (let loop ([i 0] [width 0] [in-escape? #f])\n (cond\n ((>= i len) width)\n ;; ANSI escape sequence: \\e(...m\n ((and (not in-escape?)\n (char=? (string-ref prompt-string i) #\\escape))\n (loop (+ i 1) width #t))\n (in-escape?\n (if (char-alphabetic? (string-ref prompt-string i))\n (loop (+ i 1) width #f) ;; end of escape\n (loop (+ i 1) width #t)))\n (else\n (loop (+ i 1) (+ width 1) #f))))))\n\n;;; --- Time/date helpers ---\n\n(def (current-time-values)\n ;; Returns (seconds minutes hours day month year weekday)\n ;; Using Gambit's time->seconds and manual calculation\n (let* ([t (time->seconds (current-time))]\n [secs (inexact->exact (floor t))])\n ;; Simple approach: use process to get date components\n ;; For now, return approximate values\n (values (modulo secs 60)\n (modulo (quotient secs 60) 60)\n (modulo (quotient secs 3600) 24)\n 0 0 0 0)))\n\n(def (time-string-24h)\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (format \"~2,'0d:~2,'0d:~2,'0d\" h m s)))\n\n(def (time-string-12h)\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (let ([h12 (cond ((= h 0) 12) ((> h 12) (- h 12)) (else h))])\n (format \"~2,'0d:~2,'0d:~2,'0d\" h12 m s))))\n\n(def (time-string-ampm)\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (let ([h12 (cond ((= h 0) 12) ((> h 12) (- h 12)) (else h))]\n [ampm (if (>= h 12) \"PM\" \"AM\")])\n (format \"~2,'0d:~2,'0d ~a\" h12 m ampm))))\n\n(def (time-string-hhmm)\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (format \"~2,'0d:~2,'0d\" h m)))\n\n(def (date-string)\n ;; Simplified date string\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (format \"~a\" (seconds->date-string (time->seconds (current-time))))))\n\n(def (seconds->date-string secs)\n ;; Very simplified - will be improved later with proper date library\n (with-catch\n (lambda (e) \"???\")\n (lambda ()\n (let ([port (open-input-process (list path: \"/bin/date\" arguments: (list \"+%a %b %d\")))])\n (let ([result (read-line port)])\n (close-port port)\n (if (string? result) result \"???\"))))))\n\n(def (hostname-short)\n (with-catch\n (lambda (e) \"localhost\")\n (lambda ()\n (let ([port (open-input-process (list path: \"/bin/hostname\"))])\n (let ([result (read-line port)])\n (close-port port)\n (if (string? result) result \"localhost\"))))))\n\n(def (path-basename path)\n (let loop ([i (- (string-length path) 1)])\n (cond\n ((< i 0) path)\n ((char=? (string-ref path i) #\\/)\n (substring path (+ i 1) (string-length path)))\n (else (loop (- i 1))))))\n\n(def (string-prefix? prefix str)\n (and (>= (string-length str) (string-length prefix))\n (string=? (substring str 0 (string-length prefix)) prefix)))\n\n(def (string-index-from str ch start)\n (let loop ([i start])\n (cond\n ((>= i (string-length str)) #f)\n ((char=? (string-ref str i) ch) i)\n (else (loop (+ i 1))))))\n"} {"text":";; FILE: jerboa-shell/asciinema.md\n# Jerboa Shell Session Recording — Implementation Plan\n\n## Overview\n\nIntegrate asciicast v2-compatible terminal session recording directly into jerboa-shell,\neliminating the need for external tools like asciinema or goasciinema. The shell itself\nbecomes the recorder — it already owns the REPL, the PTY, and the executor pipeline.\n\n## Why Build It In?\n\nExternal recorders wrap the shell in a PTY and capture raw bytes flowing through it.\nThey have no knowledge of what the shell is doing — they cannot distinguish a prompt\nfrom command output, or tag events with the command that produced them. Jerboa already\nhas structured access to:\n\n- **Input lines** (after line-edit, before parse)\n- **Expanded commands** (after parameter/glob expansion)\n- **Redirections and pipelines** (fd graph)\n- **Exit status** (per-command and PIPESTATUS)\n- **Timestamps and CWD** (history entries)\n- **Terminal dimensions** (SIGWINCH handling)\n\nA built-in recorder can produce **enriched asciicast** files with semantic annotations\nthat no external tool can provide.\n\n---\n\n## Format: Asciicast v2 + Extensions\n\nBase format is standard asciicast v2 for compatibility with asciinema players:\n\n```\n{\"version\":2,\"width\":120,\"height\":40,\"timestamp\":1710806400,\"env\":{\"SHELL\":\"/usr/bin/jsh\",\"TERM\":\"xterm-256color\"}}\n[0.0, \"o\", \"$ \"]\n[0.5, \"i\", \"ls -la\\r\"]\n[0.6, \"o\", \"total 42\\r\\ndrwxr-xr-x ...\\r\\n\"]\n[1.2, \"o\", \"$ \"]\n```\n\n### Extended Event Types (jerboa-specific)\n\nStandard players ignore unknown event types, so we add:\n\n| Type | Meaning | Data |\n|------|---------|------|\n| `\"o\"` | stdout output | raw bytes (standard) |\n| `\"i\"` | stdin input | raw bytes (standard) |\n| `\"r\"` | terminal resize | `\"COLSxROWS\"` (standard) |\n| `\"m\"` | marker | label string (standard) |\n| `\"c\"` | command | `{\"cmd\":\"ls -la\",\"cwd\":\"/home/user\",\"expanded\":\"ls -la\"}` |\n| `\"x\"` | exit status | `{\"status\":0,\"pipestatus\":[0]}` |\n| `\"e\"` | env change | `{\"var\":\"PATH\",\"op\":\"set\",\"val\":\"/usr/bin:...\"}` |\n| `\"d\"` | duration | `{\"cmd\":\"ls -la\",\"wall_ms\":42,\"user_ms\":12,\"sys_ms\":8}` |\n\nThis means recordings are playable in any asciicast v2 player (extra events ignored)\nwhile jerboa-aware tools can reconstruct full session semantics.\n\n---\n\n## Architecture\n\n### Phase 1: Core Recording (Tap the I/O Layer)\n\n**Goal**: Record sessions to `~/console-logs/` in asciicast v2 format.\n\n**Implementation**: Insert recording hooks at three points in the existing I/O path:\n\n```\n┌──────────────────────────────────────────────────────────┐\n│ LINEEDIT (raw terminal bytes) │\n│ ┌──────────┐ │\n│ │ keystroke │──→ [TAP: \"i\" event] ──→ line buffer │\n│ └──────────┘ │\n└──────────────────────────────────────────────────────────┘\n │ completed line\n ▼\n┌──────────────────────────────────────────────────────────┐\n│ EXECUTOR │\n│ ┌──────────┐ │\n│ │ dispatch │──→ [TAP: \"c\" event with cmd/cwd/expanded] │\n│ └──────────┘ │\n│ ┌──────────┐ │\n│ │ complete │──→ [TAP: \"x\" event with exit status] │\n│ └──────────┘ │\n└──────────────────────────────────────────────────────────┘\n │ output bytes\n ▼\n┌──────────────────────────────────────────────────────────┐\n│ OUTPUT (write to real fd 1/2) │\n│ ┌──────────┐ │\n│ │ write() │──→ [TAP: \"o\" event] ──→ terminal │\n│ └──────────┘ │\n└──────────────────────────────────────────────────────────┘\n```\n\n#### New Files\n\n| File | Purpose |\n|------|---------|\n| `jerboa-shell/recorder.ss` | Recording state, event buffering, file writer |\n| `ffi-shim.c` additions | `ffi-clock-monotonic-ns` for high-resolution timestamps |\n\n#### recorder.ss API\n\n```scheme\n;; State\n(define *recording?* (make-parameter #f))\n(define *recorder* (make-parameter #f))\n\n;; Control\n(recorder-start! filename) ; open file, write header, set *recording?*\n(recorder-stop!) ; flush, close file, clear state\n(recorder-toggle!) ; pause/resume\n\n;; Event emission (called from existing modules)\n(recorder-emit! type data) ; generic: writes [elapsed, type, data] line\n(recorder-output! bytes) ; shorthand for \"o\" event\n(recorder-input! bytes) ; shorthand for \"i\" event\n(recorder-command! cmd-info) ; shorthand for \"c\" event\n(recorder-exit-status! status) ; shorthand for \"x\" event\n(recorder-resize! cols rows) ; shorthand for \"r\" event\n```\n\n#### Integration Points (minimal patches)\n\n1. **lineedit.ss**: After each raw byte read, `(when (*recording?*) (recorder-input! byte))`\n2. **executor.ss**: Before dispatch, emit \"c\" event. After completion, emit \"x\" event.\n3. **pipeline.ss / redirect.ss**: Wrap `ffi-write` (or the write path to fd 1/2) to tee bytes to recorder.\n4. **signals.ss**: On SIGWINCH, emit \"r\" event.\n\n#### Output Tapping Strategy\n\nThe tricky part is capturing output from external commands. Two approaches:\n\n**Option A — PTY Wrapper (like asciinema)**:\nSpawn a PTY for the session and capture master-side output. This is what external\nrecorders do. Downside: adds PTY overhead and complexity.\n\n**Option B — fd-level tee via splice/tee(2) or write interception**:\nAdd `ffi-tee-fd` that duplicates output bytes to a pipe before they reach the terminal.\nOn Linux, `tee(2)` syscall can do this in-kernel with zero-copy. Alternatively, replace\ndirect fd writes with a wrapper that also feeds the recorder.\n\n**Recommendation**: Option B. Jerboa already manages all fd writes through its\nredirect/pipeline layer. We add a thin wrapper:\n\n```c\n// ffi-shim.c addition\nssize_t ffi_write_tee(int fd, const char *buf, size_t len, int record_fd) {\n if (record_fd >= 0) {\n // Write to recording pipe (non-blocking, drop on EAGAIN)\n write(record_fd, buf, len);\n }\n return write(fd, buf, len);\n}\n```\n\nThe record_fd is a pipe whose read end is consumed by the recorder thread/fiber.\n\n#### Storage\n\n```\n~/console-logs/\n├── 2026-03-19_14-30-00.cast # asciicast v2 format\n├── 2026-03-19_14-30-00.meta # optional: enriched metadata (JSON)\n└── index.db # optional: SQLite index (Phase 3)\n```\n\nFilename format: `YYYY-MM-DD_HH-MM-SS.cast` (matches goasciinema convention).\n\n#### Shell Builtins\n\n```bash\nrecord start [filename] # begin recording (default: auto-named in ~/console-logs/)\nrecord stop # end recording\nrecord pause # toggle pause\nrecord mark \"label\" # insert marker event\nrecord status # show recording state, elapsed time, file size\n```\n\nAlternatively, use `set -o recording` / `set +o recording` to fit shell conventions.\n\n---\n\n### Phase 2: Playback & Search\n\n**Goal**: Play back recordings and search across sessions.\n\n#### Playback (`play` builtin)\n\n```bash\nplay ~/console-logs/2026-03-19_14-30-00.cast # replay with timing\nplay --speed 2.0 last # 2x speed, most recent\nplay --cat last # dump output without timing\nplay --commands last # show only commands + exit status\n```\n\nImplementation: Read asciicast v2 events, sleep between timestamps, write \"o\" events\nto stdout. Use `jerboa-pcre2` for pattern filtering. The playback engine is simple —\n~100 lines of Scheme.\n\n#### Search (leveraging jsqlite)\n\n```bash\nrecord search \"podman build\" # search commands, cwd, and output\nrecord search --failed \"make\" # commands that exited non-zero\nrecord search --cwd /home/user/project \"test\" # scoped to directory\nrecord list # list all recordings with metadata\nrecord stats # total sessions, hours, commands\n```\n\nUses pure Jerboa `jsqlite` to maintain an index database:\n\n```sql\nCREATE TABLE sessions (\n id INTEGER PRIMARY KEY,\n filename TEXT UNIQUE,\n started_at INTEGER, -- unix timestamp\n duration_ms INTEGER,\n cols INTEGER,\n rows INTEGER,\n command_count INTEGER,\n cwd_at_start TEXT\n);\n\nCREATE TABLE commands (\n id INTEGER PRIMARY KEY,\n session_id INTEGER REFERENCES sessions(id),\n timestamp_ms INTEGER, -- offset from session start\n command TEXT,\n expanded TEXT,\n cwd TEXT,\n exit_status INTEGER,\n duration_ms INTEGER\n);\n\nCREATE VIRTUAL TABLE command_fts USING fts5(command, expanded, cwd);\n```\n\n---\n\n### Phase 3: Real-Time Consolidation via Chez Threads\n\n**Goal**: Decouple recording from the main shell loop for zero-overhead capture.\n\nJerboa runs on Chez Scheme which has native OS threads. The recording system uses\na **producer-consumer architecture**:\n\n```\nMain Thread (shell REPL) Recording Thread\n┌─────────────────────┐ ┌──────────────────────┐\n│ lineedit / executor │ │ event consumer │\n│ │ ring │ │\n│ recorder-emit! ──────┼──buffer──┼──→ write to .cast │\n│ │ │ update SQLite index │\n│ │ │ flush periodically │\n└─────────────────────┘ └──────────────────────┘\n```\n\n#### Ring Buffer (lock-free)\n\nUse a fixed-size bytevector ring buffer with atomic CAS for the write cursor.\nEvents are length-prefixed: `[u16:len][bytes:payload]`. The recorder thread\nreads from the tail, writes to disk, and advances the read cursor.\n\n```scheme\n(define-record-type ring-buffer\n (fields\n (mutable data) ; bytevector\n (mutable write-pos) ; atomic\n (mutable read-pos) ; atomic\n (mutable capacity)))\n```\n\nIf the ring fills up (recorder can't keep up), events are dropped — recording\nfidelity degrades gracefully rather than blocking the shell.\n\n#### Why Not Actors?\n\nChez Scheme (jerboa's runtime) doesn't have a built-in actor system like the old runtime did.\nWhile we could build one, it's unnecessary overhead for this use case. A single\ndedicated thread with a ring buffer is simpler, faster, and sufficient. The recording\nthread is a pure consumer — no bidirectional messaging needed.\n\nIf jerboa later adopts an actor/fiber system, the recorder thread can be trivially\nwrapped as an actor that receives event messages.\n\n---\n\n### Phase 4: Network Streaming & Collaboration (IMPLEMENTED)\n\n**Goal**: Stream sessions to remote observers or aggregation servers.\n\n**Status**: Implemented using jerboa's native actor transport with cookie-authenticated\nTCP and FASL serialization. No TLS — uses plain TCP via `(std net tcp-raw)` (POSIX\nsockets, no external library dependency). AES encryption can be layered via\n`(std crypto cipher)` for sensitive deployments.\n\n#### Architecture\n\n```\n┌─────────────┐ ┌──────────────┐ ┌─────────────────┐\n│ jerboa shell │────→│ recorder.ss │────→│ local .cast file │\n│ (session 1) │ │ (actor node) │ └─────────────────┘\n└─────────────┘ │ │────→│ actor send │\n └──────────────┘ └────────┬──────────┘\n │ (list 'events file lines)\n┌─────────────┐ ┌──────────────┐ │\n│ jerboa shell │────→│ recorder.ss │────→──────────┤\n│ (session 2) │ │ (actor node) │ │\n└─────────────┘ └──────────────┘ │\n ▼\n ┌─────────────────┐\n │ aggregator actor │\n │ (id 1, remote │\n │ node at host:port)│\n └─────────────────┘\n │\n ┌─────────────────┐\n │ viewer actors │\n │ (play --live) │\n │ spawn-actor with │\n │ display behavior │\n └─────────────────┘\n```\n\n#### Transport: Actor Model\n\nUses jerboa's `(std actor core)` + `(std actor transport)`:\n\n- **Cookie authentication**: FNV-1a hash handshake on connect\n- **FASL serialization**: Native Chez Scheme binary format for messages\n- **TCP via `(std net tcp-raw)`**: Pure POSIX sockets, no jerboa-ssl dependency\n- **Fire-and-forget**: Recorder sends events without blocking on response\n- **Ephemeral ports**: Shell nodes bind to port 0 (OS-assigned)\n\nURL format: `node://host:port/cookie` (cookie defaults to `jsh-default-cookie`)\n\n#### Actor Messages\n\n| Message | Direction | Purpose |\n|---------|-----------|---------|\n| `(session-start file cols rows epoch)` | recorder → aggregator | New recording session |\n| `(events file lines)` | recorder → aggregator | Batch of event lines |\n| `(session-end file)` | recorder → aggregator | Recording stopped |\n| `(subscribe viewer-ref)` | viewer → aggregator | Subscribe to live events |\n| `(event line)` | aggregator → viewer | Forward event to viewer |\n\n#### Live Tailing\n\n```bash\n# On machine A (recording)\nrecord start --stream node://aggregator:9876/my-secret-cookie\n\n# On machine B (watching)\nplay --live node://aggregator:9876/my-secret-cookie\n# or\nplay --follow ~/console-logs/2026-03-19_14-30-00.cast # tail -f equivalent\n```\n\n#### Implementation Files\n\n| File | Changes |\n|------|---------|\n| `recorder.ss` | Actor streaming: `parse-stream-url`, `stream-connect!`, `stream-disconnect!`, `stream-send-events!` |\n| `player.ss` | Actor viewer: `play-live` spawns local viewer actor, subscribes to aggregator |\n| `jerboa/lib/std/net/tcp-raw.sls` | New: fd-based plain TCP, jerboa-ssl-compatible API, no external deps |\n| `jerboa/lib/std/actor/transport.sls` | Changed: imports `(std net tcp-raw)` instead of `(std net ssl)` |\n| `build-binary-jsh.ss` | Added actor/mpsc/tcp-raw modules to boot file |\n| `build-jsh-musl.ss` | Added actor modules to boot file + POSIX socket symbol registration |\n| `Makefile` | Added `JERBOA_SSL` variable to LIBDIRS_JSH |\n\n---\n\n### Phase 5: Enriched Session Intelligence\n\n**Goal**: Leverage the shell's semantic knowledge for advanced features.\n\n#### Command-Aware Recording\n\nSince jerboa owns the parser, we can annotate recordings with:\n\n- **Command boundaries**: Know exactly where each command's output starts/ends\n- **Variable mutations**: Track `export`, `unset`, assignment side effects\n- **Function definitions**: Record when shell functions are defined/redefined\n- **Pipeline topology**: Which commands piped to which, with individual timings\n- **Error context**: Stderr output tagged to the specific command that produced it\n\n#### Session Analytics\n\n```bash\nrecord report last # summary: commands run, error rate, time distribution\nrecord report --slow last # commands that took > 1s\nrecord report --errors last # all non-zero exits with context\n```\n\n#### Reproducibility\n\nBecause we capture expanded commands with their CWD and environment:\n\n```bash\nrecord replay --execute last # re-execute all commands from a recording\nrecord replay --dry-run last # show what would be executed\nrecord export --script last # generate a shell script from the recording\n```\n\n---\n\n## Implementation Order\n\n| Phase | Scope | Dependencies | Status |\n|-------|-------|--------------|--------|\n| **1a** | `recorder.ss` + file writer + `record start/stop` builtins | None | DONE |\n| **1b** | Output tapping (fd-level tee in ffi-shim.c) | 1a | DONE |\n| **1c** | Input tapping (lineedit.ss hooks) | 1a | DONE |\n| **1d** | Command/status events (executor.ss hooks) | 1a | DONE |\n| **1e** | SIGWINCH resize events | 1a | DONE |\n| **2a** | `play` builtin (timing-aware playback) | 1a-1e | DONE |\n| **2b** | SQLite-compatible index + `record search/list` | `jsqlite` | DONE |\n| **3** | Buffered recording (in-memory event buffer, batch flush) | 1a-1e | DONE |\n| **4** | Actor-based network streaming (`std actor transport` + `std net tcp-raw`) | 3 | DONE |\n| **5** | Enriched analytics (env change events, duration, session report, export) | 2b | DONE |\n\n**Phase 1 is the critical path** — everything else builds on it. Phase 1a-1e can be\ndone in a single session. The recording format is designed so that Phase 1 recordings\nremain compatible as features are added in later phases.\n\n---\n\n## Libraries Used\n\n| Library | Phase | Purpose |\n|---------|-------|---------|\n| `jsqlite` | 2+ | Session index, command/output search, analytics |\n| `(std actor core)` | 4 | Actor spawn, send, lifecycle |\n| `(std actor transport)` | 4 | Cookie-authenticated TCP transport, FASL framing |\n| `(std net tcp-raw)` | 4 | Plain POSIX TCP sockets (no jerboa-ssl dependency) |\n| `(std actor mpsc)` | 4 | Lock-free mailbox for actors |\n\n---\n\n## Configuration\n\n```bash\n# ~/.jshrc or environment variables\nJSH_RECORD=1 # auto-record all sessions\nJSH_RECORD_DIR=~/console-logs # recording directory\nJSH_RECORD_STDIN=1 # include input events\nJSH_RECORD_IDLE_LIMIT=5 # cap idle time to 5s\nJSH_RECORD_STREAM=tcp://host:9876 # stream to aggregator\nJSH_RECORD_COMMANDS_ONLY=0 # if 1, only emit c/x events (compact)\n```\n\nOr via shell options:\n\n```bash\nset -o recording # enable\nset +o recording # disable\nshopt -s record_stdin # include input\nshopt -s record_commands # include enriched command events\n```\n\n---\n\n## Comparison with External Recorders\n\n| Feature | asciinema | goasciinema | jerboa (this plan) |\n|---------|-----------|-------------|-------------------|\n| Format | asciicast v2 | asciicast v2 | asciicast v2 + extensions |\n| Recording | PTY wrapper | PTY wrapper | Native (no PTY overhead) |\n| Input capture | Optional | Optional | Optional |\n| Command boundaries | No | No | **Yes** |\n| Exit status per cmd | No | No | **Yes** |\n| CWD tracking | No | No | **Yes** |\n| Variable tracking | No | No | **Yes** (Phase 5) |\n| Pipeline topology | No | No | **Yes** (Phase 5) |\n| Search | Basic (goasciinema) | SQLite FTS | SQLite FTS + semantic |\n| Streaming | Upload after | Upload after | **Real-time** (Phase 4) |\n| Always-on recording | Possible but awkward | Possible | **Native** (`set -o recording`) |\n| Playback | Separate tool | Separate tool | **Built-in** |\n| Binary overhead | Separate install | Separate install | **Zero** (compiled in) |\n| Format compatible | Yes | Yes | Yes (extended events ignored) |\n"} @@ -2027,12 +2027,12 @@ {"text":";; FILE: jerboa-shell/master-router.md\n# Centralized Mux Router\n\n## Concept\n\n```\nPhone (client) VPS (router) MacBook (mux)\n───────────── ───────────── ──────────────\n,mux attach uap.tf:443 ──TLS──> Router Master <──TLS── mux-server (registered)\n │\n ← session menu ←────────────────────┘\n → select \"macbook\" ──────────────────> relay messages ──────> mux-server\n (transparent pipe)\n```\n\n## Three New Components\n\n### 1. `mux-router.ss` — runs on the VPS\n\n- Listens on 443 with TLS (reuses existing `mux-transport` + HTTP camouflage)\n- Maintains a registry of connected mux servers (name, host, capabilities)\n- When a **mux server** connects, it sends a `MSG-REGISTER` identifying itself — the router holds this connection open as a **control channel**\n- When a **client** connects with `MSG-ATTACH`, the router sends back a `MSG-SESSION-LIST` (new message type) — a menu of registered muxes\n- After the client picks one, the router either:\n - **Option A (simple relay):** Bridges the client transport to the mux server's transport — every byte forwarded transparently. The router becomes a dumb pipe after handoff.\n - **Option B (multiplexed control channel):** The router tells the mux server \"open a new data channel back to me for client X.\" The mux server dials a second TLS connection tagged with a session token. The router pairs them.\n\nOption B is better — it means the mux server only needs one persistent outbound connection (the control channel), and data channels are opened on demand. This is how ngrok/Cloudflare Tunnel work.\n\n### 2. `mux-register.ss` — logic in the mux server\n\n- On `,mux server --router uap.tf:443 --name macbook`, the mux server:\n 1. Dials TLS to `uap.tf:443`\n 2. Sends `MSG-REGISTER` with its name, available sessions\n 3. Keeps the connection alive (ping/pong already exists)\n 4. When it receives `MSG-OPEN-CHANNEL {token}` from the router, it dials a **new** TLS connection to the router, sends `MSG-CHANNEL-READY {token}`, and that connection becomes a normal mux client transport\n\n### 3. New protocol messages\n\n```\nMSG-REGISTER (0x30) mux→router {name, host-label, session-count}\nMSG-REGISTERED (0x31) router→mux {ack}\nMSG-SESSION-LIST (0x32) router→client {list of (name, host, sessions)}\nMSG-SELECT-MUX (0x33) client→router {mux-name}\nMSG-OPEN-CHANNEL (0x34) router→mux {token}\nMSG-CHANNEL-READY(0x35) mux→router {token}\nMSG-ROUTE-OK (0x36) router→client {ack, proceed with normal mux proto}\n```\n\n## Connection Flow\n\n```\n1. MacBook boots jsh:\n ,mux server --router uap.tf:443 --name macbook --password hunter2\n\n2. MacBook → VPS: TLS connect, MSG-REGISTER {name:\"macbook\"}\n VPS → MacBook: MSG-REGISTERED {ok}\n (control channel stays open, ping/pong keeps it alive)\n\n3. Phone:\n ,mux attach uap.tf:443\n\n4. Phone → VPS: TLS connect, MSG-ATTACH\n VPS → Phone: MSG-SESSION-LIST [{name:\"macbook\", host:\"home\", sessions:2},\n {name:\"workbox\", host:\"office\", sessions:1}]\n\n5. Phone → VPS: MSG-SELECT-MUX {name:\"macbook\"}\n\n6. VPS → MacBook: MSG-OPEN-CHANNEL {token:\"abc123\"}\n MacBook → VPS: new TLS conn, MSG-CHANNEL-READY {token:\"abc123\"}\n\n7. VPS pairs Phone↔MacBook data channel\n VPS → Phone: MSG-ROUTE-OK\n\n8. From here on, Phone↔MacBook talk normal mux protocol.\n VPS just copies bytes between the two TLS connections.\n```\n\n## Where It Fits in the Codebase\n\n| New/Modified | File | What |\n|---|---|---|\n| **New** | `src/jsh/mux-router.ss` | Router server (runs on VPS) |\n| **New** | `src/jsh/mux-relay.ss` | Relay/bridge logic — pairing two transports |\n| **Modify** | `src/jsh/mux-proto.ss` | Add message types 0x30-0x36 |\n| **Modify** | `src/jsh/mux-server.ss` | Add `--router` flag, registration logic, `MSG-OPEN-CHANNEL` handler |\n| **Modify** | `src/jsh/mux-client.ss` | Handle `MSG-SESSION-LIST` menu, `MSG-SELECT-MUX` |\n| **Modify** | `src/jsh/main.ss` | Add `,mux router` command, `--router` flag to `,mux server` |\n\n## Authentication Model\n\n- **Mux→Router auth:** The mux server authenticates to the router using the existing challenge-response (scrypt + HMAC). The router has a config of allowed mux names + password hashes.\n- **Client→Router auth:** Client authenticates to the router first (proves they're allowed to see the menu). Then after routing, they authenticate to the actual mux server through the relay (the existing auth handshake flows through transparently).\n- Two layers: router-level access control + per-mux passwords. A client needs both.\n\n## Key Design Decisions\n\n### 1. Router as dumb relay vs. smart proxy\n\nRecommendation: **dumb relay**. After pairing, the router just copies bytes. This means encryption between client and mux is end-to-end (the router can't read session content even if compromised). The router only sees the initial handshake to route.\n\n### 2. Heartbeat/reconnect\n\nWhen the MacBook's control channel drops (laptop sleep, network change), it should auto-reconnect and re-register. The router marks it offline until it reconnects.\n\n### 3. NAT traversal\n\nSince the mux server initiates all connections outward, no port forwarding needed on the home network. This is the whole point.\n\n### 4. Multiple routers\n\nA mux server could register with multiple routers for redundancy. Each registration is just an outbound TLS connection.\n\n---\n\n## Implementation Guide — Step by Step\n\nThis section is a complete, ordered implementation plan. Each step produces a buildable, testable increment. Follow the order exactly — later steps depend on earlier ones.\n\n### Prerequisites\n\n- Read and understand these files before starting:\n - `src/jsh/mux-proto.sls` — wire protocol (message framing, encode/decode)\n - `src/jsh/mux-transport.sls` — transport abstraction (FD, TLS, WebSocket)\n - `src/jsh/mux-auth.sls` — challenge-response auth + encryption\n - `src/jsh/mux-server.sls` — server event loop, client handling\n - `src/jsh/mux-client.sls` — client attach, relay loop\n - `src/jsh/mux-session.sls` — session/window/pane data model\n - `build-jerboa.ss` — build system (.ss → .sls compilation)\n\n- Remember: **only edit `.ss` source files**. The `.sls` files are generated by `build-jerboa.ss`. If a `.ss` file doesn't exist yet for a module, that means the `.sls` IS the source (legacy). For new files, create `.ss` files.\n\n---\n\n### Step 1: Add Router Protocol Messages to `mux-proto`\n\n**File:** `src/jsh/mux-proto.sls` (this one is edited directly — it has no `.ss` source)\n\n**What to do:**\n\n1. Add these constants after the existing `MSG-ENCRYPTED` definition (line 45):\n\n```scheme\n;; Router protocol messages\n(define MSG-REGISTER #x30) ;; mux→router: register this mux server\n(define MSG-REGISTERED #x31) ;; router→mux: registration acknowledged\n(define MSG-ROUTER-LIST #x32) ;; router→client: list of available muxes\n(define MSG-SELECT-MUX #x33) ;; client→router: select a mux by name\n(define MSG-OPEN-CHANNEL #x34) ;; router→mux: open a data channel (token)\n(define MSG-CHANNEL-READY #x35) ;; mux→router: data channel connected (token)\n(define MSG-ROUTE-OK #x36) ;; router→client: routing established, proceed\n(define MSG-ROUTER-AUTH #x37) ;; router→mux/client: router-level auth challenge\n```\n\n2. Add all new names to the `(export ...)` list at the top (lines 8-19).\n\n**Note:** `MSG-SESSION-LIST` (#x0C) already exists in the protocol and is used for intra-mux session listing. The router uses `MSG-ROUTER-LIST` (#x32) to avoid collision — the payload format is different (mux names vs session IDs).\n\n**Verify:** `make jsh-musl` builds clean.\n\n---\n\n### Step 2: Create the Relay/Bridge Module — `src/jsh/mux-relay.ss`\n\n**File:** New file `src/jsh/mux-relay.ss`\n\n**Purpose:** A bidirectional byte copier that bridges two transports. After the router pairs a client with a mux data channel, it hands both transports to this relay. The relay copies bytes in both directions until one side disconnects.\n\n**What to implement:**\n\n```scheme\n(import (jerboa prelude))\n;; This becomes (library (jsh mux-relay) ...) after build-jerboa.ss processes it\n\n;; The relay needs access to:\n;; - transport-read, transport-write, transport-close, transport-ready?\n;; from (jsh mux-transport)\n;; - ffi-nanosleep-us from (jsh ffi)\n```\n\n**Core function — `relay-bridge`:**\n\n```\n(relay-bridge transport-a transport-b) → void (returns when either side disconnects)\n```\n\nThe function runs a poll loop (matching the style of `server-event-loop` in `mux-server.sls:474`):\n\n```\nloop:\n if transport-a ready?:\n read from a into buf (4096 bytes, non-blocking style)\n if read <= 0: break (a disconnected)\n write all bytes to b\n if write fails: break (b disconnected)\n if transport-b ready?:\n read from b into buf\n if read <= 0: break\n write all bytes to a\n if write fails: break\n sleep 500µs (matches relay-loop sleep at mux-client.sls:585)\n loop\n```\n\n**Implementation details:**\n\n- Use `transport-read-fn` / `transport-write-fn` / `transport-id` — same pattern as `mux-client.sls:251-252`.\n- Write must handle partial writes (loop until all bytes sent), same pattern as `mux-write-message` in `mux-proto.sls:87-95`.\n- On exit, close both transports via `transport-close`.\n- Use `(ffi-nanosleep-us 500)` for the sleep — same as `mux-client.sls:585`.\n- Wrap the entire loop in `(guard (e [#t ...])` so transport errors don't crash the router.\n\n**Also export:** `relay-bridge-async` — a variant that spawns the relay in a Chez `fork-thread` so the router event loop isn't blocked:\n\n```scheme\n(define (relay-bridge-async tr-a tr-b on-done)\n (fork-thread\n (lambda ()\n (guard (e [#t (void)])\n (relay-bridge tr-a tr-b))\n (when on-done (on-done)))))\n```\n\n**Add to build chain:** Add `(jsh mux-relay)` to the module list in `build-jsh.ss` (look for where other `jsh mux-*` modules are listed, around lines 35-57).\n\n**Verify:** `make jsh-musl` builds clean.\n\n---\n\n### Step 3: Create the Router Server — `src/jsh/mux-router.ss`\n\n**File:** New file `src/jsh/mux-router.ss`\n\nThis is the largest new component. Model it closely on `mux-server.sls` — it's structurally the same (listen, accept, event loop) but instead of managing PTYs, it manages a registry of mux servers and routes clients to them.\n\n#### 3a. Define Router Data Structures\n\n**Registered mux record:**\n\n```scheme\n(define-record-type registered-mux\n (fields\n [immutable name] ;; string: \"macbook\", \"workbox\", etc.\n [immutable host-label] ;; string: human-readable origin label\n [immutable transport] ;; transport: the control channel to this mux\n [mutable session-count] ;; fixnum: how many sessions available\n [mutable alive?] ;; boolean: control channel still up\n [mutable last-ping] ;; fixnum: epoch seconds of last ping/pong\n ))\n```\n\n**Pending channel record** (for in-flight client↔mux pairing):\n\n```scheme\n(define-record-type pending-channel\n (fields\n [immutable token] ;; bytevector: 32-byte random token\n [immutable client-tr] ;; transport: the waiting client\n [immutable mux-name] ;; string: which mux this is for\n [immutable created-at] ;; fixnum: epoch seconds (for timeout)\n ))\n```\n\n**Router client record** (someone who connected but hasn't been routed yet):\n\n```scheme\n(define-record-type router-client\n (fields\n [immutable transport]\n [mutable state] ;; 'pending-auth | 'pending-select | 'routed | 'failed\n [mutable auth-challenge] ;; bytevector or #f\n [mutable fail-count]\n ))\n```\n\n**Router state:**\n\n```scheme\n(define-record-type router-state\n (fields\n [immutable listen-fds]\n [mutable muxes] ;; list of registered-mux\n [mutable clients] ;; list of router-client\n [mutable pending-channels] ;; list of pending-channel\n [mutable running?]\n [immutable tls-ctx]\n [immutable password-hash] ;; router-level auth (optional)\n [immutable password-salt]\n ))\n```\n\n#### 3b. Implement the Router Event Loop\n\nModel on `server-event-loop` (`mux-server.sls:474-506`):\n\n```\nrouter-event-loop:\n check-signals! (SIGTERM, SIGCHLD not needed here)\n accept-new-connections!\n handle-mux-control-channels! ;; read from registered muxes\n handle-client-input! ;; read from unrouted clients\n expire-pending-channels! ;; timeout stale pairings (30s)\n cleanup-dead-muxes! ;; remove disconnected muxes\n cleanup-dead-clients! ;; remove disconnected clients\n sleep 1ms\n loop\n```\n\n#### 3c. Accept and Classify Connections\n\nWhen a new TLS connection arrives, the router doesn't know yet if it's a mux registering or a client attaching. Use the same HTTP detection pattern as `mux-server.sls:579-617`:\n\n1. Accept TCP fd, TLS handshake (reuse `wrap-accepted-fd` pattern)\n2. If HTTP Upgrade → WebSocket (for browser clients, future)\n3. If HTTP probe → nginx camouflage (copy `handle-http-probe!`)\n4. If raw mux protocol → read first message:\n - `MSG-REGISTER` → it's a mux server registering (go to 3d)\n - `MSG-ATTACH` → it's a client wanting to connect (go to 3e)\n - `MSG-CHANNEL-READY` → it's a mux opening a data channel (go to 3f)\n\n**Critical subtlety:** The router must do a **non-blocking first-message read** to classify. But unlike the existing server (which speaks first with `MSG-AUTH-REQUIRED`), here **both** muxes and clients speak first. So after TLS + WebSocket detection, do a blocking read of the first mux message — the connecting party always sends its intent immediately.\n\n#### 3d. Handle MSG-REGISTER (Mux Server Registration)\n\nWhen a mux sends `MSG-REGISTER`:\n\n1. **Parse payload:** The payload format should be a simple length-prefixed structure:\n ```\n [name-len:2B big-endian][name:UTF-8][host-len:2B][host-label:UTF-8][session-count:2B]\n ```\n\n2. **Authenticate the mux** (if router has a password configured):\n - Send `MSG-ROUTER-AUTH` with salt + challenge (same format as `MSG-AUTH-REQUIRED`, payload = salt(32) || challenge(32))\n - Wait for `MSG-AUTH-RESPONSE` (same proof computation as existing auth)\n - Verify with `mux-auth-verify-proof`\n - On failure: send `MSG-AUTH-FAIL`, close\n\n3. **Register:**\n - Create `registered-mux` record\n - Add to `router-state-muxes`\n - Send `MSG-REGISTERED` (empty payload = success)\n - Keep transport open as control channel\n\n4. **Control channel maintenance:**\n - In the event loop, periodically send `MSG-PING` on each control channel\n - If no `MSG-PONG` within 30s, mark mux as dead\n - If mux sends `MSG-REGISTER` again (re-register after reconnect), update the record\n\n#### 3e. Handle Client Connection (MSG-ATTACH from Client)\n\nWhen a client sends `MSG-ATTACH`:\n\n1. **Router-level auth** (if configured):\n - Same challenge-response as 3d. Send `MSG-ROUTER-AUTH`, wait for `MSG-AUTH-RESPONSE`.\n - This proves the client is allowed to use the router at all.\n\n2. **Build mux menu:**\n - Collect all `registered-mux` records where `alive?` is `#t`\n - Encode as `MSG-ROUTER-LIST` payload:\n ```\n [count:2B]\n For each mux:\n [name-len:2B][name:UTF-8][host-len:2B][host-label:UTF-8][session-count:2B][online:1B]\n ```\n\n3. **Send menu:** `mux-write-message ... MSG-ROUTER-LIST payload`\n\n4. **Wait for selection:** Read `MSG-SELECT-MUX` from client:\n - Payload: `[name-len:2B][name:UTF-8]`\n - Look up in `router-state-muxes`\n - If not found or dead: send `MSG-ERROR` with \"mux not available\", close\n - If found: proceed to channel opening (3f)\n\n#### 3f. Open Data Channel (The Routing)\n\nAfter client selects a mux:\n\n1. **Generate token:** `(rust-random-bytes 32)` — 32-byte random token\n\n2. **Create pending-channel record:**\n ```scheme\n (make-pending-channel token client-transport mux-name (current-epoch))\n ```\n Add to `router-state-pending-channels`.\n\n3. **Send MSG-OPEN-CHANNEL to mux** (via control channel):\n - Payload: the 32-byte token\n - `mux-write-message` on the registered-mux's transport\n\n4. **Mux receives MSG-OPEN-CHANNEL** (handled in Step 5):\n - Mux dials a NEW TLS connection to the router\n - Sends `MSG-CHANNEL-READY` with the same 32-byte token as payload\n\n5. **Router receives MSG-CHANNEL-READY** on a new connection:\n - Extract token from payload\n - Find matching `pending-channel` by token comparison (`rust-timing-safe-equal?` from `(std crypto native-rust)` — prevent timing attacks)\n - If found:\n - Remove from pending list\n - Send `MSG-ROUTE-OK` to the client (empty payload)\n - Spawn `relay-bridge-async` with client-transport and mux-data-transport\n - The relay copies bytes bidirectionally — client↔mux talk normal mux protocol from here\n - If not found (stale/invalid token): close the connection\n\n6. **Timeout:** In the event loop, expire any `pending-channel` older than 30 seconds. Send `MSG-ERROR` to the waiting client, close both.\n\n#### 3g. Entry Point — `mux-router-start`\n\n```scheme\n(define (mux-router-start port cert-bv key-bv password)\n ;; Same TLS setup as mux-server-start-tcp-impl (mux-server.sls:368-455):\n ;; 1. Hash password if provided\n ;; 2. Create TLS server context from cert-bv/key-bv PEM\n ;; 3. Create TCP listener on port\n ;; 4. Set up signal handlers (SIGHUP ignore, SIGPIPE ignore, SIGTERM flag)\n ;; 5. Enter router-event-loop\n ;; 6. Cleanup on exit\n ...)\n```\n\n**Add to build chain:** Add `(jsh mux-router)` to `build-jsh.ss`.\n\n**Verify:** `make jsh-musl` builds clean. Router doesn't need PTY/session code — it never spawns shells.\n\n---\n\n### Step 4: Add `,mux router` Command to main\n\n**File:** `src/jsh/main.sls` (or the `.ss` source that generates it — check `build-jerboa.ss`)\n\n**What to do:**\n\n1. Add `(jsh mux-router)` to the imports.\n\n2. In the `,mux` command dispatch (around line 439-582 in main.sls), add a new subcommand:\n\n```\n,mux router -p PORT [--password PW]\n```\n\nThis calls `mux-router-start`. The router always needs:\n- A port (required — it's internet-facing)\n- TLS cert/key from embed (same as `,mux server -p`)\n- Optional password for router-level access control\n\n3. Parse the arguments using the same flag-parsing pattern as the existing `,mux server` command (lines 503-541 in main.sls). The flags are:\n - `-p PORT` — required, TCP port\n - `--password PW` — optional, router-level password\n - No `--name` needed (the router is singular)\n\n4. The router should daemonize the same way `,mux server` does (via `ffi-fork-exec` — see how the server forks around line 1020-1031 in main.sls).\n\n**Verify:** `make jsh-musl && ./jsh-musl -c ',mux router -p 8443 --password test'` starts and listens.\n\n---\n\n### Step 5: Add `--router` Flag to Mux Server\n\n**File:** `src/jsh/mux-server.sls`\n\n**What to add:**\n\nA new exported function and supporting logic for registering with a remote router.\n\n#### 5a. New Export\n\n```scheme\nmux-server-register-router ;; (name router-host router-port password) → void\n```\n\nAdd to the `(export ...)` block.\n\n#### 5b. Registration Logic\n\nImplement `mux-server-register-router`:\n\n```scheme\n(define (mux-server-register-router name router-host router-port router-password)\n ;; 1. Connect to router via TLS\n ;; Use tls-connect-pinned with empty pin (accept self-signed)\n ;; Same as mux-client.sls:122\n (let ([tr (tls-connect-pinned router-host router-port (make-bytevector 0))])\n\n ;; 2. If router requires auth, handle challenge-response\n ;; Read first message — if MSG-ROUTER-AUTH, do auth handshake\n ;; (same flow as do-auth-handshake in mux-client.sls:295-327,\n ;; but using router-password)\n\n ;; 3. Send MSG-REGISTER\n ;; Encode: name-len(2) || name || host-label-len(2) || host-label || session-count(2)\n ;; host-label = (machine-type) or hostname from (getenv \"HOSTNAME\")\n (let ([payload (encode-register-payload name host-label session-count)])\n (mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-REGISTER payload))\n\n ;; 4. Read MSG-REGISTERED ack\n ;; 5. Return the transport (control channel) — caller stores it\n tr))\n```\n\n#### 5c. Integrate into Server Event Loop\n\nIn `server-event-loop` (`mux-server.sls:474`), add a step to monitor the router control channel:\n\n```\n;; In the event loop, after existing steps:\nhandle-router-control! ;; check for MSG-OPEN-CHANNEL, MSG-PING\n```\n\nImplement `handle-router-control!`:\n\n```scheme\n(define (handle-router-control! state)\n (let ([router-tr (server-state-router-transport state)]) ;; new field\n (when (and router-tr (transport-ready? router-tr))\n (let-values ([(type payload)\n (mux-read-message (transport-read-fn router-tr)\n (transport-id router-tr))])\n (cond\n [(not type)\n ;; Router disconnected — schedule reconnect\n (server-debug \"ROUTER: control channel lost, will reconnect\")\n (server-state-router-transport-set! state #f)\n (schedule-router-reconnect! state)]\n\n [(= type MSG-OPEN-CHANNEL)\n ;; Router wants us to open a data channel for a client\n (let ([token payload]) ;; payload IS the 32-byte token\n (server-debug \"ROUTER: opening data channel\")\n (fork-thread\n (lambda ()\n (open-data-channel-to-router! state token))))]\n\n [(= type MSG-PING)\n (mux-write-message (transport-write-fn router-tr)\n (transport-id router-tr) MSG-PONG (make-bytevector 0))]\n\n [else (void)])))))\n```\n\n#### 5d. Open Data Channel Back to Router\n\n```scheme\n(define (open-data-channel-to-router! state token)\n ;; 1. Dial new TLS connection to router\n (let ([tr (tls-connect-pinned\n (server-state-router-host state)\n (server-state-router-port state)\n (make-bytevector 0))])\n\n ;; 2. Send MSG-CHANNEL-READY with token\n (mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-CHANNEL-READY token)\n\n ;; 3. Now this transport IS a client connection.\n ;; Hand it to accept-mux-client! — from here on it's identical to\n ;; a direct client connecting over TCP.\n (accept-mux-client! state tr)))\n```\n\n**This is the key insight:** once the data channel is established, the mux server treats it exactly like any other client connection. The existing `accept-mux-client!` sends `MSG-AUTH-REQUIRED` or `MSG-STATUS`, and the client on the other end of the relay goes through the normal auth handshake. The router just copies bytes.\n\n#### 5e. Add Router Fields to server-state\n\nAdd these fields to the `server-state` record (`mux-server.sls:113`):\n\n```scheme\n[mutable router-transport] ;; transport or #f — control channel to router\n[mutable router-host] ;; string or #f\n[mutable router-port] ;; fixnum or #f\n[mutable router-password] ;; string or #f\n[mutable router-reconnect-at] ;; epoch seconds or #f — when to retry\n```\n\nInitialize all to `#f` in `make-server-state` calls. When `--router` is passed, populate them and call `mux-server-register-router` after the event loop starts.\n\n#### 5f. Auto-Reconnect\n\nIn the event loop, after `handle-router-control!`:\n\n```scheme\n(when (and (server-state-router-host state)\n (not (server-state-router-transport state)))\n (let ([now (ffi-clock-realtime-sec)]\n [at (server-state-router-reconnect-at state)])\n (when (and at (>= now at))\n (guard (e [#t\n ;; Reconnect failed — try again in 10s\n (server-state-router-reconnect-at-set! state (+ now 10))])\n (let ([tr (mux-server-register-router\n (server-state-name state)\n (server-state-router-host state)\n (server-state-router-port state)\n (server-state-router-password state))])\n (server-state-router-transport-set! state tr)\n (server-state-router-reconnect-at-set! state #f))))))\n```\n\n**Verify:** `make jsh-musl` builds clean.\n\n---\n\n### Step 6: Add `--router` Flag to `,mux server` Command\n\n**File:** Where `,mux server` arguments are parsed (main.sls ~503-541)\n\nAdd `--router HOST:PORT` flag parsing:\n\n```scheme\n;; In the flag loop, add:\n[(string=? arg \"--router\")\n (set! router-addr (next-arg))] ;; \"uap.tf:443\"\n```\n\nAfter the server starts (but before entering the event loop), if `router-addr` is set:\n\n```scheme\n(when router-addr\n (let-values ([(host port) (parse-host-port router-addr)])\n (server-state-router-host-set! state host)\n (server-state-router-port-set! state port)\n (server-state-router-password-set! state password) ;; reuse server password or add --router-password\n (guard (e [#t\n (fprintf (current-error-port) \"jsh: router registration failed, will retry~n\")\n (server-state-router-reconnect-at-set! state\n (+ (ffi-clock-realtime-sec) 5))])\n (let ([tr (mux-server-register-router name host port password)])\n (server-state-router-transport-set! state tr)\n (fprintf (current-error-port) \" router: registered with ~a:~a~n\" host port)))))\n```\n\n**Verify:** `make jsh-musl && ./jsh-musl -c ',mux server --name macbook --router localhost:8443 --password test'` registers with the router.\n\n---\n\n### Step 7: Modify Client to Handle Router Menu\n\n**File:** `src/jsh/mux-client.sls`\n\n**What changes:**\n\nWhen a client connects to a router (instead of a direct mux server), the first message back will be `MSG-ROUTER-LIST` instead of `MSG-AUTH-REQUIRED` or `MSG-STATUS`.\n\n#### 7a. Modify `attach-with-transport`\n\nIn `attach-with-transport` (`mux-client.sls:250-290`), add a new case in the initial message dispatch:\n\n```scheme\n[(= type MSG-ROUTER-LIST)\n ;; Connected to a router — show mux selection menu\n (handle-router-menu tr payload password)]\n```\n\nThis goes after the existing `MSG-AUTH-REQUIRED` and `MSG-STATUS` cases.\n\nBut wait — the client sends `MSG-ATTACH` first (the router needs to know it's a client, not a mux registering). So **before** reading the initial message, the client must send `MSG-ATTACH`:\n\n```scheme\n;; At the start of attach-with-transport, before reading:\n(mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-ATTACH (make-bytevector 0))\n```\n\n**However**, this changes behavior for direct connections too. The existing mux server doesn't expect `MSG-ATTACH` as the first client message — the server speaks first. Two options:\n\n**Option A (recommended):** Only send `MSG-ATTACH` when connecting to a known router. Add a `router?` parameter to `attach-with-transport`. When the user does `,mux attach uap.tf:443`, detect that it's a router by either:\n- A `--router` flag: `,mux attach --router uap.tf:443`\n- Or: always send `MSG-ATTACH` first, and modify `mux-server.sls` to handle it gracefully (ignore it if unexpected). This is the more resilient approach.\n\n**Option B (simpler):** Add a `--via` flag for router connections:\n\n```\n,mux attach --via uap.tf:443 # connect through router\n,mux attach myhost:443 # direct connect (existing)\n```\n\nGo with **Option A** — add a `--via` or `--router` flag so the intent is explicit. Create a new function:\n\n```scheme\n(define (mux-client-attach-via-router host port password)\n ;; 1. TLS connect to router\n (let ([tr (tls-connect-pinned host port (make-bytevector 0))])\n ;; 2. Send MSG-ATTACH (tells router \"I'm a client\")\n (mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-ATTACH (make-bytevector 0))\n ;; 3. Handle router auth if needed (MSG-ROUTER-AUTH)\n ;; 4. Receive MSG-ROUTER-LIST\n ;; 5. Display menu, get selection\n ;; 6. Send MSG-SELECT-MUX\n ;; 7. Receive MSG-ROUTE-OK\n ;; 8. From here, call attach-with-transport — normal mux flow\n ))\n```\n\n#### 7b. Implement the Mux Selection Menu\n\n```scheme\n(define (handle-router-menu tr router-list-payload password)\n ;; 1. Decode payload:\n ;; [count:2B] then for each: [name-len:2B][name][host-len:2B][host][sessions:2B][online:1B]\n (let ([muxes (decode-router-list router-list-payload)])\n\n ;; 2. Display menu on stderr (raw terminal):\n ;; jsh router — select a server:\n ;; 1) macbook (home) — 2 sessions\n ;; 2) workbox (office) — 1 session\n ;; >\n (fprintf (current-error-port) \"~njsh router — select a server:~n\")\n (let loop ([i 0] [muxes muxes])\n (when (pair? muxes)\n (let ([m (car muxes)])\n (fprintf (current-error-port) \" ~a) ~a (~a) — ~a session~a~n\"\n (+ i 1)\n (mux-entry-name m)\n (mux-entry-host m)\n (mux-entry-sessions m)\n (if (= (mux-entry-sessions m) 1) \"\" \"s\")))\n (loop (+ i 1) (cdr muxes))))\n (fprintf (current-error-port) \"> \")\n (flush-output-port (current-error-port))\n\n ;; 3. Read selection (number or name)\n ;; Use ffi-embed-read-passphrase or just (get-line (current-input-port))\n ;; since we're not in raw mode yet\n (let* ([input (get-line (current-input-port))]\n [selected (resolve-selection input muxes)])\n\n (unless selected\n (fprintf (current-error-port) \"jsh: invalid selection~n\")\n (transport-close tr)\n (error 'mux-client \"invalid router selection\"))\n\n ;; 4. Send MSG-SELECT-MUX\n (let ([name-bv (string->utf8 (mux-entry-name selected))])\n (mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-SELECT-MUX name-bv))\n\n ;; 5. Wait for MSG-ROUTE-OK\n (let-values ([(type payload)\n (mux-read-message (transport-read-fn tr) (transport-id tr))])\n (cond\n [(and type (= type MSG-ROUTE-OK))\n (fprintf (current-error-port)\n \"jsh: routed to ~a~n\" (mux-entry-name selected))\n ;; 6. Now the transport is bridged to the mux server.\n ;; Call attach-with-transport — the mux server's auth\n ;; handshake flows through transparently.\n (attach-with-transport tr (mux-entry-name selected) password)]\n [(and type (= type MSG-ERROR))\n (fprintf (current-error-port)\n \"jsh: router error: ~a~n\" (utf8->string payload))\n (transport-close tr)]\n [else\n (fprintf (current-error-port) \"jsh: unexpected router response~n\")\n (transport-close tr)])))))\n```\n\n#### 7c. Wire Up the Command\n\nIn main.sls, add handling for `,mux attach --via HOST:PORT`:\n\n```scheme\n[(string=? arg \"--via\")\n (set! via-router (next-arg))]\n```\n\nThen dispatch:\n\n```scheme\n(if via-router\n (let-values ([(host port) (parse-host-port via-router)])\n (mux-client-attach-via-router host port password))\n ;; ... existing direct-connect logic\n )\n```\n\n**Verify:** Full flow test:\n1. Start router: `./jsh-musl -c ',mux router -p 8443 --password test'`\n2. Start mux server: `./jsh-musl -c ',mux server --name macbook --router localhost:8443 --password test'`\n3. Attach via router: `./jsh-musl -c ',mux attach --via localhost:8443 --password test'`\n4. Should see menu with \"macbook\", select it, get a shell.\n\n---\n\n### Step 8: Payload Encoding/Decoding Helpers\n\nThese are used by Steps 3-7. Put them in `mux-router.ss` (router-side) and duplicate the decoder in `mux-client.sls` (client-side), or create a shared `mux-router-proto.ss` module.\n\n**Recommended:** Add to `mux-proto.sls` since it's the shared protocol module.\n\n```scheme\n;; Encode MSG-REGISTER payload\n(define (encode-register-payload name host-label session-count)\n ;; [name-len:2B BE][name:UTF-8][host-len:2B BE][host:UTF-8][session-count:2B BE]\n (let* ([name-bv (string->utf8 name)]\n [host-bv (string->utf8 host-label)]\n [nlen (bytevector-length name-bv)]\n [hlen (bytevector-length host-bv)]\n [buf (make-bytevector (+ 2 nlen 2 hlen 2))])\n (bytevector-u16-set! buf 0 nlen (endianness big))\n (bytevector-copy! name-bv 0 buf 2 nlen)\n (bytevector-u16-set! buf (+ 2 nlen) hlen (endianness big))\n (bytevector-copy! host-bv 0 buf (+ 4 nlen) hlen)\n (bytevector-u16-set! buf (+ 4 nlen hlen) session-count (endianness big))\n buf))\n\n;; Decode MSG-REGISTER payload → (values name host-label session-count)\n(define (decode-register-payload bv)\n (let* ([nlen (bytevector-u16-ref bv 0 (endianness big))]\n [name (utf8->string (subbytevector bv 2 (+ 2 nlen)))]\n [hlen (bytevector-u16-ref bv (+ 2 nlen) (endianness big))]\n [host (utf8->string (subbytevector bv (+ 4 nlen) (+ 4 nlen hlen)))]\n [sessions (bytevector-u16-ref bv (+ 4 nlen hlen) (endianness big))])\n (values name host sessions)))\n\n;; Encode MSG-ROUTER-LIST payload\n(define (encode-router-list muxes)\n ;; muxes: list of (name host-label session-count online?)\n ;; [count:2B] then for each: [name-len:2B][name][host-len:2B][host][sessions:2B][online:1B]\n ...)\n\n;; Decode MSG-ROUTER-LIST payload → list of alist entries\n(define (decode-router-list bv)\n ...)\n\n;; Helper: extract sub-bytevector\n(define (subbytevector bv start end)\n (let ([out (make-bytevector (- end start))])\n (bytevector-copy! bv start out 0 (- end start))\n out))\n```\n\n---\n\n### Step 9: Keepalive and Health\n\n#### Router Side\n\nIn `router-event-loop`, every 15 seconds:\n\n```scheme\n(for-each\n (lambda (mux)\n (when (registered-mux-alive? mux)\n (let ([ok (mux-write-message\n (transport-write-fn (registered-mux-transport mux))\n (transport-id (registered-mux-transport mux))\n MSG-PING (make-bytevector 0))])\n (unless ok\n (registered-mux-alive?-set! mux #f)))))\n (router-state-muxes state))\n```\n\nHandle `MSG-PONG` in the mux control channel reader — update `last-ping`. If `last-ping` is older than 45s, mark dead.\n\n#### Mux Server Side\n\nIn `handle-router-control!`, handle `MSG-PING`:\n\n```scheme\n[(= type MSG-PING)\n (mux-write-message (transport-write-fn router-tr)\n (transport-id router-tr) MSG-PONG (make-bytevector 0))]\n```\n\nAlready shown in Step 5c.\n\n---\n\n### Step 10: Testing Plan\n\n**Unit test — protocol encoding:**\n- Encode/decode `MSG-REGISTER`, `MSG-ROUTER-LIST` payloads round-trip\n\n**Integration test — local loopback:**\n1. Start router on port 18443\n2. Start mux-server with `--router localhost:18443`\n3. Verify registration (router logs \"macbook registered\")\n4. Client attaches via `--via localhost:18443`\n5. Client sees menu, selects macbook\n6. Client gets a working shell (type `echo hello`, see output)\n7. Client detaches (Ctrl-b d)\n8. Kill mux-server, verify router marks it offline\n9. Restart mux-server, verify it re-registers\n10. Client attaches again, verify it works\n\n**Stress test — reconnect:**\n1. Start router + mux-server\n2. Kill router (SIGTERM)\n3. Verify mux-server logs reconnect attempts\n4. Restart router\n5. Verify mux-server re-registers within 10s\n6. Client can attach\n\n**Security test:**\n1. Wrong router password → rejected\n2. Wrong mux password (through relay) → rejected\n3. Invalid token in `MSG-CHANNEL-READY` → connection closed\n4. Expired pending channel (wait 31s) → client gets error\n\n---\n\n### Build Order Summary\n\n```\nStep 1: mux-proto.sls ← add message constants (5 min)\nStep 2: mux-relay.ss ← new file, ~60 lines (30 min)\nStep 8: payload helpers ← add to mux-proto.sls (30 min)\nStep 3: mux-router.ss ← new file, ~400 lines (the big one) (4 hrs)\nStep 4: main.sls ← add ,mux router command (30 min)\nStep 5: mux-server.sls ← --router registration + data channel (2 hrs)\nStep 6: main.sls ← --router flag for ,mux server (15 min)\nStep 7: mux-client.sls ← router menu + --via flag (1.5 hrs)\nStep 9: keepalive ← ping/pong in router + server (30 min)\nStep 10: testing ← integration tests (1 hr)\n```\n\nBuild and smoke-test (`make jsh-musl && ./jsh-musl -c 'echo ok'`) after EVERY step. Do not batch steps.\n"} {"text":";; FILE: jerboa-shell/README.md\n# jerboa-shell\n\nA POSIX-compatible shell (`jsh`) built with [Jerboa](https://git.sr.ht/~lisp/jerboa) — a Chez Scheme standard library.\n\n**Status:** 1056/1179 (90%) Oils POSIX compat tests passing · 66/66 unit tests passing · 56/56 binary functional tests passing\n\n## Architecture\n\n`jsh` runs shell source code (originally from [jerboa-shell](https://github.com/ober/jerboa-shell)) translated to R6RS `.sls` libraries that run natively on Chez Scheme using:\n\n1. **Jerboa** — provides `(std ...)` and `(jerboa ...)` modules (sort, format, transducers, logging, threads, pattern matching, etc.)\n2. **Gherkin runtime** — MOP/class system and Jerboa compiler support for in-shell `eval`\n3. **C FFI shim** — POSIX system calls (fork, exec, signals, termios, etc.)\n4. **Self-contained binary** — boot files (petite, scheme, jsh) embedded as C byte arrays; fully portable single ELF\n\n### Enhancements over jerboa-shell\n\n| Feature | Implementation |\n|---------|---------------|\n| History search | Transducer pipeline `(std transducer)`: prefix filter + seen-set dedup + take |\n| Debug logging | Structured log via `(std log)`, activated by `JSH_DEBUG=1` |\n| Binary portability | memfd program loading (avoids Chez boot-file thread limitation) |\n| Embedded files | Virtual `//embed/` filesystem compiled into the binary with optional ChaCha20-Poly1305 encryption |\n| Coreutils builtins | 90+ GNU coreutils commands run in-process (busybox-style) via [jerboa-coreutils](https://git.sr.ht/~lisp/jerboa-coreutils) |\n| Pipeline threading | Builtin pipeline stages run as real POSIX threads — `ls \\| sort \\| wc` uses 3 CPU cores |\n| Background jobs | `,run CMD` runs detached with stdout+stderr captured to a log; `,run ls`/`log ID`/`kill ID` |\n| Mux resume + snapshot | per-pane output ring → reconnect with `,mux attach --from N` to replay missed output; Ctrl-b W/E save/restore session layout + scrollback |\n| Wormhole transfers | Optional `worm` feature exposes `,worm send`, `,worm receive`, relay, transit-relay, and SSH-key transfer commands via `jerboa-wormhole` |\n\n## Prerequisites\n\nOur Chez Scheme 10.4 (see the Chez note below). Normal builds use the\nself-contained `jerbuild` tool from Jerboa. The Makefile prefers local tools in\nthis order: `./jerbuild`, `.jerboa/bin/jerbuild`, `../jerboa/dist/jerbuild`,\nthen `jerbuild` on `PATH`. If none are available, it downloads the Jerboa release\nartifact for the host into `.jerboa/bin`. The supported artifact targets are\n`macos-arm64`, `linux-amd64`, `linux-arm64`, and `freebsd-amd64`; set\n`JERBOA_VERSION` to a Jerboa tag that has those artifacts attached.\n\nFor local builds:\n- Jerboa `jerbuild`/`jerboa` tools, found locally or fetched from\n [SourceHut release artifacts](https://git.sr.ht/~lisp/jerboa)\n- [jerboa-coreutils](https://git.sr.ht/~lisp/jerboa-coreutils) — coreutils builtins\n- [jerboa-ssh](https://github.com/ober/jerboa-ssh) — SSH agent support\n- [jerboa-native-rs](https://git.sr.ht/~lisp/jerboa-native-rs) — Rust native library (TLS via rustls, FUSE, crypto)\n- GCC or Clang\n\n## Building\n\n```bash\n# Native build for THIS host — the everyday build (no Podman).\n# macOS -> jsh-macos; Linux -> static-musl jsh-linux-<arch>; FreeBSD -> jsh-freebsd-<arch>\nmake binary\n\n# Build and install using local Jerboa tools, or fetch release tools if absent.\nmake install\n\n# Cross-build a fully static Linux ELF from any host (Chez xpatch + musl-cross,\n# no Podman/QEMU). scp the result to a Linux box to run.\nmake linux-amd64 # or: make linux-arm64\n\n# Native-only platform builds (run on that platform's host):\nmake freebsd-amd64 # on a FreeBSD amd64 host\nmake android # on a Termux/Android device\n\n# Dynamic jsh binary copied to ./jsh\nmake jsh\n\n# Compile modules only\nmake jsh-compile\n```\n\n> There is no `jsh-musl` target and no Podman/Docker build. `make binary` is the\n> canonical local build; the named `linux-*` targets cross-build statically.\n\n### Static musl Binary (Linux)\n\nCross-build a fully static binary from any host (Chez xpatch + musl-cross, no\ncontainer), or build natively on a Linux box with `make binary`:\n\n```bash\nmake linux-amd64 # cross from any host -> jsh-linux-amd64\n./jsh-linux-amd64 -c 'echo Works anywhere!' # run on a Linux box\n```\n\nBenefits:\n- **Zero dependencies** — works on any Linux (kernel 2.6.39+)\n- **Container-friendly** — runs in `FROM scratch` container images\n- **No Podman/QEMU** — pure Chez cross-compile\n\nSee [docs/musl-build.md](docs/musl-build.md) for details.\n\n### macOS Binary\n\n```bash\nmake macos\n./jsh-macos -c 'echo ok'\n```\n\nUses `.dylib` FFI loading. The `JSH_FFI_LIB` environment variable overrides the default library search path.\n\n### Android / Termux Binary\n\n```bash\nmake android\n./jsh-android -c 'echo ok'\n```\n\nBuilds a PIE ELF for `aarch64-linux-android` using the NDK toolchain. Links `libjerboa_native.a` from `~/jerboa/jerboa-native-rs/target/release/`. See [android.md](android.md) for details.\n\n### Embedded Files\n\nCompile files into the binary for a self-contained deployment with SSH keys, credentials, scripts, and shell config — no external files needed.\n\n```bash\n# Place files in embed/ and build\nmkdir -p embed/.ssh\ncp ~/.ssh/id_rsa embed/.ssh/\nmake binary\n\n# Access embedded files at runtime (jsh-macos / jsh-linux-<arch> / ...)\n./jsh-macos -c 'cat //embed/.ssh/id_rsa'\n./jsh-macos -c 'ssh -i //embed/.ssh/id_rsa user@host'\n```\n\nOptional encryption (ChaCha20-Poly1305, prompted passphrase, no echo):\n\n```bash\nJSH_EMBED_ENCRYPT=1 make binary\n# At runtime:\n,unlock # prompted for passphrase\nssh -i //embed/.ssh/id_rsa user@host\n```\n\nSecurity hardening (activated automatically after `,unlock`):\n- **Passphrase never enters Scheme heap** — prompt + PBKDF2 + zeroing all in C with `explicit_bzero()`\n- **mlock / MADV_DONTDUMP** — key never swapped to disk or included in core dumps\n- **MFD_CLOEXEC** — memfds invisible to child processes unless explicitly passed\n- **Exit cleanup** — decrypted cache and derived key zeroed on shell exit\n\nPassword-store helpers are available in pass-enabled builds after `,unlock`:\n`,pass-store` stores a prompted secret, `,pass` copies a stored secret, and\n`,pass-import-firefox` imports Firefox JSON rows (`url`, `user`, `password`) so\n`,login <site>` can autocomplete login labels and copy the password, username,\nor URL to the clipboard.\n\nSee [docs/embed.md](docs/embed.md) for full documentation.\n\n### Coreutils Builtins\n\n90+ GNU coreutils commands run in-process as shell builtins — no fork/exec overhead:\n\n```bash\n# These all run inside jsh, no external binaries needed\n./jsh -c 'seq 1 1000000 | sort -n | wc -l'\n./jsh -c 'echo hello | tr a-z A-Z | rev'\n./jsh -c 'factor 42'\n```\n\nIncluded commands: basename, cat, chmod, chown, cut, date, dirname, du, env, expr, factor, find, grep, head, id, ln, ls, md5sum, mkdir, mv, od, paste, readlink, realpath, rev, rm, rmdir, seq, sha256sum, sort, stat, tail, tee, touch, tr, true, false, uname, uniq, wc, whoami, xargs, yes, and many more.\n\nPipeline stages run as real POSIX threads (Chez Scheme uses pthreads, not green threads), so `ls | sort | wc` uses 3 CPU cores simultaneously — true parallel execution within a single process.\n\nThe musl static build automatically patches coreutils for static linking (no `load-shared-object`) and registers all required POSIX FFI symbols.\n\n### Sandbox Security\n\nThe `,sb` meta-command provides defense-in-depth isolation using three independent kernel mechanisms:\n\n1. **Landlock LSM** — restricts filesystem access to explicitly allowed paths (symlinks resolved via `realpath(3)` to prevent escapes)\n2. **Seccomp BPF** (Linux, irreversible) — a default-allow **blocklist** that denies\n the dangerous syscall set (ptrace, kernel-module/kexec, bpf, perf_event_open,\n mount, keyctl, …) with `EPERM`, plus socket-creation syscalls under `--no-net`\n and exec/fork for the pure-compute case. Allowing ordinary syscalls keeps\n arbitrary programs working (an earlier allowlist model SIGSYS-killed them).\n3. **SIGALRM timeout** — kills the sandboxed child after the specified deadline\n\nThe sandbox also supports restricted Scheme evaluation via `-e`:\n\n```bash\n,sb -e \"(+ 1 2 3)\" # pure computation in allowlist-only environment\n,sb -r /tmp -c \"cat /tmp/f\" # shell command with Landlock + seccomp\n```\n\nAdditional security features:\n- **FD leak detection** — guardian-based tracking warns about file descriptors and FIFOs not properly closed\n- **FTS query validation** — recording search rejects null bytes, excessive length, and SQL comment markers to prevent FTS5 DoS\n- **Path validation** — sandbox paths must be absolute, non-empty, and null-byte-free\n\n## Testing\n\n```bash\n# Unit tests (66 tests across all core modules)\nmake test\n\n# Binary functional tests (functional tests against the local binary)\nmake test-binary\n\n# Full Oils POSIX compat report vs gsh reference\nmake compat-test\n```\n\nSee [docs/test-status.md](docs/test-status.md) for per-suite results.\n\n## Configuration\n\n```makefile\nJSH_VERSION ?= 0.2.0 # Shell banner/prompt version\nJERBOA_VERSION ?= v0.2.0 # Jerboa artifact tag\nJERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin # Downloaded tool location\nJERBUILD ?= /path/to/jerbuild # Optional explicit tool\nCOREUTILS ?= vendor/jerboa-coreutils/lib # Coreutils library path\nJSH_EMBED ?= embed # Directory for embedded files\n```\n\n## Source Layout\n\n```\njsh.ss # Entry point (Chez script)\njsh-main.c # C main (embeds boot files + program via memfd)\nffi-shim.c # POSIX FFI (fork, exec, signals, termios, memfd, embed crypto, seccomp BPF)\nembed-crypto.c/h # Self-contained ChaCha20-Poly1305 + PBKDF2-HMAC-SHA256\nbuild-jsh.ss # Module compilation driver\nbuild-binary-jsh.ss # 7-step binary build (WPO → boot → C headers → link)\ngen-embed.ss # Build-time embed data generator (scans embed/, encrypts)\nembed/ # Files to compile into the binary (optional)\nsrc/\n compat/gambit.sls # Gambit→Chez compat shims\n jsh/embed.sls # Embedded virtual filesystem module (//embed/ paths)\n jsh/embed-data.sls# Generated: embedded file bytevectors + salt\n jsh/coreutils.sls # Coreutils builtin registration (90+ commands)\n jsh/coreutils-shim.sls # load-shared-object safety shim for dynamic builds\n jsh/ # 31+ translated shell modules (ast, lexer, parser, executor, ...)\ntest/\n test-jsh.ss # Unit test suite (66 tests)\n test-binary.sh # Functional test suite (56 tests, runs against both binaries)\ndocs/\n embed.md # Embedded files documentation\n test-status.md # Per-suite compat results\n optimization.md # Chez compiler optimization notes\n```\n"} {"text":";; FILE: jerboa-shell/startup.ss\n;;; startup.ss — RC file loading and startup sequences for jsh\n;;; Handles login/interactive/non-interactive startup file sourcing.\n\n(export #t)\n(import :std/sugar\n :std/format\n :jsh/util\n :jsh/environment\n :jsh/script\n :jsh/recorder\n :jsh/config)\n\n;;; --- Public interface ---\n\n;; Load startup files based on shell mode.\n;; login?: #t for login shells (first char of $0 is - or --login flag)\n;; interactive?: #t for interactive shells (stdin is tty)\n(def (load-startup-files! env login? interactive?)\n ;; Load ~/.jsh/config first. jsh-config-load! itself warns on and ignores a\n ;; malformed file; the outer with-catch is belt-and-suspenders so a startup\n ;; config problem can never abort shell startup.\n (with-catch (lambda (e) #f)\n (lambda () (jsh-config-load!)))\n (cond\n ;; Interactive login shell\n ((and login? interactive?)\n (source-if-exists! \"/etc/profile\" env)\n ;; First of ~/.jsh_profile, ~/.jsh_login, ~/.profile\n (let ((home (or (env-get env \"HOME\") (home-directory))))\n (or (source-if-exists! (string-append home \"/.jsh_profile\") env)\n (source-if-exists! (string-append home \"/.jsh_login\") env)\n (source-if-exists! (string-append home \"/.profile\") env))))\n ;; Interactive non-login shell\n (interactive?\n (let ((home (or (env-get env \"HOME\") (home-directory))))\n (source-if-exists! (string-append home \"/.jshrc\") env)))\n ;; Non-interactive (script)\n (else\n (let ((env-file (env-get env \"JSH_ENV\")))\n (when env-file\n (source-if-exists! env-file env)))))\n ;; Auto-start recording if JSH_RECORD=1\n (when (and interactive?\n (let ((v (env-get env \"JSH_RECORD\")))\n (and v (string=? v \"1\"))))\n (with-catch (lambda (e) #f)\n (lambda () (recorder-start!)))))\n\n;; Run logout sequence for login shells.\n(def (run-logout! env)\n (let ((home (or (env-get env \"HOME\") (home-directory))))\n (source-if-exists! (string-append home \"/.jsh_logout\") env)))\n\n;;; --- Helpers ---\n\n(def (source-if-exists! path env)\n ;; Source a file if it exists; return #t if sourced, #f if not found.\n (if (file-exists? path)\n (begin\n (source-file! path env)\n #t)\n #f))\n"} -{"text":";; FILE: jerboa-shell/build-jsh-macos.ss\n#!chezscheme\n;;; build-jsh-macos.ss — Build jsh binary on macOS\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:... < build-jsh-macos.ss\n;;;\n;;; This script:\n;;; 1. Patches coreutils/awk/sed/ssl for static builds (no dlopen)\n;;; 2. Compiles jsh modules (using stock scheme)\n;;; 3. Creates boot file + optimized program .so\n;;; 4. Generates C files with embedded boot data\n;;; 5. Compiles C with cc (clang) against static Chez's scheme.h\n;;; 6. Links fully static binary with libkernel.a\n;;;\n;;; The resulting jsh-macos binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") (format \"/Users/~a\" (getenv \"USER\"))))\n(define script-dir (or (getenv \"SCRIPT_DIR\") (current-directory)))\n(define output-name (or (getenv \"JSH_OUTPUT\") \"jsh-macos\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-macos.sh so we know the repo root.\n(define vendor-dir (format \"~a/vendor\" script-dir))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. Callers wrap with (or (getenv \"X\") (dep ...))\n;; to allow env var overrides from the shell script.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jsqlite/src\" home-dir)))\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (dep \"jerboa-coreutils\" \"lib\")))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n(define coreutils-shim\n (let ([upstream (dep-file \"jerboa-coreutils\" \"support/libcoreutils.c\")]\n [local \"patches/libcoreutils.c\"])\n (cond\n [(file-exists? upstream) upstream]\n [(file-exists? local) local]\n [else upstream])))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls).\n;; rustls is preferred over OpenSSL for security.\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; Feature-gated enable flags — gate on BOTH directory existence AND\n;; the feature being in *enabled-features*. This is how JSH_FEATURES\n;; actually controls whether feature dependencies land in the binary.\n(define enable-aws?\n (and has-aws? (memq 'aws *enabled-features*)))\n\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n;; Rust native library — resolve via vendor/ → ~/mine/ → ~/\n(define native-rs-dir\n (let* ([v (format \"~a/jerboa/jerboa-native-rs\" vendor-dir)]\n [m (format \"~a/mine/jerboa/jerboa-native-rs\" home-dir)]\n [h (format \"~a/jerboa/jerboa-native-rs\" home-dir)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n(define native-lib-path\n (format \"~a/target/release/libjerboa_native.a\" native-rs-dir))\n(define native-src-dir\n (format \"~a/src\" native-rs-dir))\n;; Sentinel file written after a successful native build without SQLite.\n;; If absent, the .a was built with default (tls-only) features — must rebuild.\n(define native-features-sentinel\n (format \"~a/target/release/.built-with-tls-crypto-no-sqlite\" native-rs-dir))\n;; Only attempt Rust rebuild if cargo is available (pre-built .a may have been\n;; downloaded by build-jsh-macos.sh — don't clobber it with a failed cargo call)\n(define has-cargo?\n (= 0 (system \"command -v cargo >/dev/null 2>&1\")))\n(when (and has-cargo?\n (file-exists? native-src-dir)\n (or (not (file-exists? native-lib-path))\n ;; Features sentinel absent → stale build (wrong feature set)\n (not (file-exists? native-features-sentinel))\n ;; Check if any .rs file is newer than the .a\n (let ([lib-mtime (file-modification-time native-lib-path)])\n (let check ([files (directory-list native-src-dir)])\n (and (pair? files)\n (let ([f (format \"~a/~a\" native-src-dir (car files))])\n (or (and (> (string-length (car files)) 3)\n (string=? \".rs\" (substring (car files)\n (- (string-length (car files)) 3)\n (string-length (car files))))\n (time>? (file-modification-time f) lib-mtime))\n (check (cdr files)))))))))\n (printf \"~n[0/7] Rebuilding Rust native library (source newer than .a)...~n\")\n (let ([rc (safe-system (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote native-rs-dir)))])\n (unless (= rc 0)\n (fprintf (current-error-port) \"FATAL: cargo build --release --no-default-features --features tls,crypto failed~n\")\n (exit 1)))\n ;; Write sentinel so next build knows the right features were used\n (let ([port (open-output-file native-features-sentinel 'truncate)])\n (display \"tls,crypto,no-sqlite\\n\" port)\n (close-output-port port)))\n(when (and (file-exists? native-lib-path)\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(define has-native-lib? (file-exists? native-lib-path))\n\n;; Rust coreutils static library\n(define rust-host-triple\n (case (machine-type)\n [(tarm64osx) \"aarch64-apple-darwin\"]\n [(ta6osx) \"x86_64-apple-darwin\"]\n [else #f]))\n\n(define rust-coreutils-lib-path\n (or (getenv \"RUST_COREUTILS_LIB\")\n (if rust-host-triple\n (format \"~a/rust-coreutils/target/~a/release/libjsh_coreutils.a\"\n script-dir rust-host-triple)\n (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" script-dir))))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — coreutils builtins will be stubs~n\"))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\"))\n\n;; Chez Scheme static installation\n;; macOS: machine type is tarm64osx (arm64) or ta6osx (x86_64)\n(define chez-machine\n (or (getenv \"CHEZ_MACHINE\")\n (machine-type)))\n\n(define chez-ta6fb\n (or (getenv \"CHEZ_TA6FB\")\n ;; Search Homebrew paths first, then /usr/local\n (let loop ([prefixes '(\"/opt/homebrew/Cellar/chezscheme\" \"/opt/homebrew/lib\" \"/usr/local/lib\")])\n (if (null? prefixes)\n (error 'build \"Cannot find Chez static directory (libkernel.a). Install: brew install chezscheme\")\n (let ([prefix (car prefixes)])\n (if (file-directory? prefix)\n (let check-dirs ([dirs (directory-list prefix)])\n (cond\n [(null? dirs) (loop (cdr prefixes))]\n [else\n (let* ([d (car dirs)]\n ;; For Cellar layout: /opt/homebrew/Cellar/chezscheme/<ver>/lib/csv<ver>/<machine>\n [cellar-path (format \"~a/~a/lib\" prefix d)]\n [direct-path (format \"~a/~a\" prefix d)])\n (cond\n ;; Cellar: check <prefix>/<ver>/lib/csv*/<machine>/libkernel.a\n [(and (file-directory? cellar-path)\n (let ([csv-dirs (filter (lambda (x) (string-prefix? \"csv\" x))\n (directory-list cellar-path))])\n (and (pair? csv-dirs)\n (let ([p (format \"~a/~a/~a\" cellar-path (car csv-dirs) chez-machine)])\n (and (file-exists? (format \"~a/libkernel.a\" p)) p)))))\n => (lambda (p) p)]\n ;; Direct: check <prefix>/csv*/<machine>/libkernel.a\n [(and (string-prefix? \"csv\" d)\n (file-directory? direct-path)\n (let ([p (format \"~a/~a\" direct-path chez-machine)])\n (and (file-exists? (format \"~a/libkernel.a\" p)) p)))\n => (lambda (p) p)]\n [else (check-dirs (cdr dirs))]))]))\n (loop (cdr prefixes))))))))\n\n(define scheme-h-dir chez-ta6fb)\n(define petite-boot-path (format \"~a/petite.boot\" chez-ta6fb))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-ta6fb))\n\n;; OpenSSL include directory — still needed for jerboa_ssh_crypto.c (SSH transport)\n;; libcrypto.a is NO LONGER linked; vault/crypto.sls uses ring via jerboa_native instead\n(define openssl-include-dir\n (let ([brew-inc \"/opt/homebrew/opt/openssl/include\"]\n [brew-inc-x86 \"/usr/local/opt/openssl/include\"])\n (cond\n [(file-directory? brew-inc) brew-inc]\n [(file-directory? brew-inc-x86) brew-inc-x86]\n [else \"/usr/include\"])))\n\n(define openssl-lib-dir\n (let ([brew-lib \"/opt/homebrew/opt/openssl/lib\"]\n [brew-lib-x86 \"/usr/local/opt/openssl/lib\"])\n (cond\n [(file-directory? brew-lib) brew-lib]\n [(file-directory? brew-lib-x86) brew-lib-x86]\n [else \"/usr/lib\"])))\n\n(printf \"Chez static: ~a~n\" chez-ta6fb)\n(printf \"Native lib: ~a~n\" (if has-native-lib? native-lib-path \"not found\"))\n(printf \"~n\")\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo so the broken vendor source is recompiled below.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n;; Compile the Jerboa runtime and stdlib entries before any staged dependency\n;; or jsh module. If these are compiled later, the boot image can embed a\n;; different compilation instance of (jerboa core) than the modules depend on.\n(define boot-jerboa-modules\n '(\"jerboa/runtime\"\n \"std/typed\" \"std/pregexp\" \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\"\n \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/misc/thread\"\n \"jerboa/core\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/alist\"\n \"std/stm\" \"std/foreign\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"\n ;; (std contract condition) — imported by (jerboa core); only has a .ss\n ;; (no .sls), so compile-imported-libraries doesn't auto-write its .so\n ;; and compile-whole-program can't find its .wpo. Force-precompile here.\n \"std/contract/condition\"))\n\n(define (precompile-boot-jerboa-modules! label)\n (printf \"~a~n\" label)\n ;; Parameter settings here must match the step [2/7] compile-program block\n ;; that builds jsh-generated.so. WPO files from a different optimize-level\n ;; or unsafe-* setting are flagged as \"does not define expected compilation\n ;; instance\" by compile-whole-program and fail the build.\n (parameterize ([compile-imported-libraries #t]\n [generate-wpo-files #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (cons (cons jerboa-dir jerboa-dir)\n (library-directories))])\n (for-each\n (lambda (m)\n ;; Source may be either .sls (R6RS) or .ss (Jerboa convention);\n ;; check both. Source absent => skip (module not vendored).\n ;; Compile failures are caught and logged so platform-specific\n ;; modules (e.g. (std os landlock) on macOS) don't abort the loop.\n (let* ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [ss (format \"~a/~a.ss\" jerboa-dir m)]\n [src (cond [(file-exists? sls) sls]\n [(file-exists? ss) ss]\n [else #f])]\n [so (format \"~a/~a.so\" jerboa-dir m)]\n [wpo (format \"~a/~a.wpo\" jerboa-dir m)])\n (when (and src\n (or (not (file-exists? so))\n (not (file-exists? wpo))))\n (printf \" Pre-compiling ~a~n\" src)\n (guard (exn [(condition? exn)\n (printf \" SKIP ~a: ~a~n\"\n m (condition-message-string exn))])\n (compile-library src)))))\n boot-jerboa-modules)))\n\n(define (condition-message-string c)\n ;; Best-effort one-line summary of a Chez condition for skip-log output.\n (cond [(and (condition? c) (message-condition? c))\n (condition-message c)]\n [else (format \"~s\" c)]))\n\n;; Skipped: WPO at step [2/7] now precompiles all transitive imports with\n;; compatible parameter settings. Pre-staging at lower optimize-level here\n;; produced .wpo files that compile-whole-program rejected as \"wrong\n;; compilation instance\".\n;; (precompile-boot-jerboa-modules!\n;; \"[0pre/7] Pre-compiling Jerboa boot dependencies...\")\n\n;; ========== Step 0: Patch coreutils for static builds ==========\n;; Coreutils modules call (load-shared-object #f) at library init time.\n;; In static builds, load-shared-object throws because dlopen is unavailable.\n;; Since FFI symbols are pre-registered via Sforeign_symbol, we patch these out.\n\n(printf \"[0/7] Patching coreutils for static build (no dlopen)...~n\")\n\n(define coreutils-stage (format \"~a/coreutils-stage\" (current-directory)))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(system (format \"mkdir -p '~a'\" coreutils-stage))\n\n(system (format \"cp -a '~a/jerboa-coreutils' '~a/'\"\n coreutils-dir coreutils-stage))\n;; macOS/FreeBSD sed uses -i '' instead of -i (no backup extension)\n(system (format \"find '~a/jerboa-coreutils' -name '*.sls' -exec sed -i '' 's/(load-shared-object #f)/(void)/g' {} +\"\n coreutils-stage))\n;; These modules import string-split explicitly from (std misc string). Newer\n;; (jerboa core) also re-exports string-split, so exclude it from core here.\n(for-each\n (lambda (name)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage name)])\n (when (file-exists? path)\n (system (format \"sed -i '' 's/(jerboa core)/(except (jerboa core) string-split)/' '~a'\"\n path)))))\n '(\"cut.sls\" \"grep.sls\" \"join.sls\"))\n(system (format \"find '~a/jerboa-coreutils' -name '*.so' -delete\"\n coreutils-stage))\n(system (format \"find '~a/jerboa-coreutils' -name '*.wpo' -delete\"\n coreutils-stage))\n\n(printf \" Recompiling patched coreutils...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"common.sls\" \"common/version.sls\" \"common/io.sls\" \"common/security.sls\"))\n (for-each\n (lambda (name)\n (let ([sls (format \"~a/jerboa-coreutils/~a.sls\" coreutils-stage name)])\n (when (file-exists? sls)\n (compile-library sls))))\n '(\"basename\" \"dirname\" \"link\" \"unlink\" \"yes\" \"printenv\"\n \"sleep\" \"whoami\" \"logname\" \"hostname\" \"nproc\" \"tty\" \"sync\" \"hostid\"\n \"cat\" \"head\" \"tail\" \"tac\" \"tee\" \"wc\" \"nl\" \"fold\" \"expand\" \"unexpand\" \"fmt\"\n \"cut\" \"paste\" \"join\" \"comm\" \"sort\" \"uniq\" \"tr\" \"numfmt\"\n \"mkdir\" \"rmdir\" \"mktemp\" \"touch\" \"readlink\" \"realpath\" \"ln\" \"cp\" \"mv\" \"rm\"\n \"install\" \"shred\"\n \"ls\" \"chmod\" \"chown\" \"chgrp\" \"stat\" \"du\" \"df\" \"pathchk\"\n \"date\" \"id\" \"groups\" \"who\" \"users\" \"pinky\" \"uptime\" \"uname\" \"arch\"\n \"seq\" \"expr\" \"basenc\" \"base64\" \"base32\" \"od\"\n \"cksum\" \"md5sum\" \"sha1sum\" \"sha224sum\" \"sha256sum\" \"sha384sum\" \"sha512sum\"\n \"b2sum\" \"sum\"\n \"env\" \"timeout\" \"nice\" \"nohup\" \"chroot\" \"stdbuf\"\n \"truncate\" \"mkfifo\" \"mknod\" \"split\" \"csplit\" \"dd\" \"dircolors\"\n \"tsort\" \"shuf\" \"factor\" \"pr\" \"ptx\" \"stty\"\n \"chcon\" \"runcon\"\n \"dir\" \"vdir\" \"rev\" \"top\")))\n\n;; grep + Rust-backed PCRE2\n(let ([grep-pcre2-patch (format \"~a/patches/grep-pcre2.sls\" (current-directory))])\n (when (file-exists? grep-pcre2-patch)\n (system (format \"mkdir -p '~a/jerboa-coreutils/grep'\" coreutils-stage))\n (system (format \"cp '~a' '~a/jerboa-coreutils/grep/pcre2.sls'\"\n grep-pcre2-patch coreutils-stage))))\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (let ([pcre2-sls (format \"~a/jerboa-coreutils/grep/pcre2.sls\" coreutils-stage)])\n (when (file-exists? pcre2-sls)\n (printf \" Compiling grep/pcre2...~n\")\n (compile-library pcre2-sls)))\n (let ([grep-sls (format \"~a/jerboa-coreutils/grep.sls\" coreutils-stage)])\n (when (file-exists? grep-sls)\n (printf \" Compiling grep...~n\")\n (compile-library grep-sls))))\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: patch pcre2 to use Rust regex\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if enable-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when enable-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (library-directories))])\n (when enable-aws?\n (printf \" Compiling jerboa-aws...~n\")\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0d: Stage jerboa-ssh for static build ==========\n(printf \"[0d/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build\n (system (format \"find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"sed -i '' 's/base64-encode/b64-encode/g' '~a'\" kh))\n (system (format \"sed -i '' 's/base64-decode/b64-decode/g' '~a'\" kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0e: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0e/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n;; Use a separate staging directory for macOS so we don't clobber the\n;; committed musl-targeted vault-stage/ (which has _loaded #f baked in).\n;; Each build cleans + repopulates its own dir from upstream jerboa-fuse.\n(define vault-stage (format \"~a/vault-stage-macos\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" vault-stage vault-stage))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n(when has-jerboa-fuse?\n ;; Copy the jerboa-fuse library tree (chez/fuse/ and chez/vault/)\n (system (format \"mkdir -p '~a/chez/fuse' '~a/chez/vault'\" vault-stage vault-stage))\n ;; FUSE layer\n (system (format \"cp '~a/chez/fuse.sls' '~a/chez/fuse.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/constants.sls' '~a/chez/fuse/constants.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/types.sls' '~a/chez/fuse/types.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/codec.sls' '~a/chez/fuse/codec.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/mount.sls' '~a/chez/fuse/mount.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/access.sls' '~a/chez/fuse/access.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/secmem.sls' '~a/chez/fuse/secmem.sls'\" jerboa-fuse-dir vault-stage))\n ;; Vault layer\n (system (format \"cp '~a/chez/vault.sls' '~a/chez/vault.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/format.sls' '~a/chez/vault/format.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/crypto.sls' '~a/chez/vault/crypto.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/blockstore.sls' '~a/chez/vault/blockstore.sls'\" jerboa-fuse-dir vault-stage))\n ;; Patch out ALL load-shared-object calls (FUSE mount helper + libcrypto + libc)\n ;; Use (if #f #f) instead of (void) since some modules only import (rnrs)\n ;; Simple single-level calls:\n (system (format \"find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object[^)]*)/(if #f #f)/g' {} +\" vault-stage))\n ;; fuse.sls and blockstore.sls have multi-line (load-shared-object (case ...)) blocks\n ;; that the simple sed can't handle. Use Scheme to patch them out.\n (let ([str-has? (lambda (haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))])\n (for-each\n (lambda (file-path)\n (when (file-exists? file-path)\n (let* ([content (let ([p (open-input-file file-path)])\n (let loop ([lines '()])\n (let ([l (get-line p)])\n (if (eof-object? l)\n (begin (close-input-port p) (reverse lines))\n (loop (cons l lines))))))]\n [patched\n (let loop ([lines content] [acc '()] [skip 0])\n (if (null? lines)\n (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (or (str-has? line \"(define _libc-loaded\")\n (str-has? line \"(define libc-loaded\")))\n (let ([name (if (str-has? line \"_libc-loaded\")\n \"_libc-loaded\" \"libc-loaded\")])\n (loop (cdr lines)\n (cons (format \" (define ~a #t)\" name) acc)\n 1))]\n [(and (> skip 0)\n (or (str-has? line \"#t))\")\n (str-has? line \"#f))\")))\n (loop (cdr lines) acc 0)]\n [(> skip 0)\n (loop (cdr lines) acc skip)]\n [else\n (loop (cdr lines) (cons line acc) 0)]))))])\n (let ([p (open-output-file file-path 'replace)])\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched)\n (close-output-port p)))))\n (list (format \"~a/chez/vault/blockstore.sls\" vault-stage)\n (format \"~a/chez/fuse.sls\" vault-stage)))) ;; close let\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up (format → crypto → secmem → mount → constants → types → codec → access → blockstore → fuse → vault)\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n ;; Format layer (no deps)\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n ;; FUSE foundation\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n ;; Secure memory + access control (depend on mount)\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n ;; Vault crypto (depends on format + libcrypto)\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n ;; Vault blockstore (depends on format + crypto + secmem)\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n ;; FUSE main (depends on all fuse sub-modules)\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n ;; Vault main (depends on everything)\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"~n[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(or (not (file-exists? so))\n (time>? (file-modification-time sls) (file-modification-time so)))\n (printf \" Compiling ~a...~n\" sls)\n (compile-library sls)]\n [else\n (printf \" (up to date) ~a~n\" sls)])))\n\n;; NOTE: WPO step uses a fresh subprocess (see step [2/7] below), so per-jsh\n;; module .so files compiled here are NOT inputs to compile-whole-program —\n;; the subprocess recompiles them. We compile here only as a fast sanity check\n;; and so that `jsh.ss` (non-WPO direct-load) keeps working in dev mode.\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n ;; Compat layer\n (compile-jsh-module \"../compat/gambit\")\n (for-each compile-jsh-module '(\"ffi\"))\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n (for-each compile-jsh-module '(\"conditions\" \"ast\" \"registry\"))\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-macos \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss + WPO via subprocess...~n\"\n )\n;; WHY a subprocess: build-jsh-macos.ss imports (jerboa build), which\n;; transitively loads (jerboa core) into THIS scheme process. Once a library\n;; is loaded, compile-program/compile-whole-program won't re-emit its .so/.wpo\n;; — but compile-whole-program needs (jerboa core).wpo on disk to fuse it in.\n;; The fix: shell out to a fresh scheme that imports only (chezscheme), so all\n;; transitive libs get compiled freshly with WPO. Pattern matches\n;; ~/mine/jerboa/support/build-boot.ss + build-jerbuild.sh.\n;;\n;; Also: nuke all .so/.wpo from step [1/7] so the subprocess recompiles every\n;; library with generate-wpo-files #t. Without this, compile-imported-libraries\n;; in the subprocess sees up-to-date .so files and skips them — leaving us\n;; without .wpo files for compile-whole-program to inline.\n(printf \" Clearing .so/.wpo from step [1/7] so subprocess recompiles with WPO...~n\")\n(for-each\n (lambda (dir)\n (when (and (string? dir) (file-directory? dir))\n (system (format \"find '~a' -type f \\\\( -name '*.so' -o -name '*.wpo' \\\\) -delete\"\n dir))))\n (list \"src\" jerboa-dir\n (if has-jerboa-ssh? ssh-stage #f)\n (if has-jerboa-fuse? vault-stage #f)\n awk-stage sed-stage\n (if enable-aws? aws-stage #f)\n coreutils-stage))\n(let* ([libdir-pair->str\n (lambda (e)\n (cond [(pair? e) (format \"~a::~a\" (car e) (cdr e))]\n [else e]))]\n [extra-libdirs\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage)))]\n [all-libdirs (append extra-libdirs (library-directories))]\n [libdirs-str\n (apply string-append\n (let loop ([lst (map libdir-pair->str all-libdirs)] [acc '()])\n (cond [(null? lst) (reverse acc)]\n [(null? acc) (loop (cdr lst) (list (car lst)))]\n [else (loop (cdr lst)\n (cons (car lst) (cons \":\" acc)))])))]\n [scheme-cmd (or (getenv \"SCHEME\")\n (format \"~a/.chez/bin/scheme\" jerboa-dir-base))]\n [build-boot-script (format \"~a/support/build-boot.ss\" jerboa-dir-base)])\n (unless (file-exists? build-boot-script)\n (fprintf (current-error-port)\n \"FATAL: build-boot.ss not found at ~a~n\" build-boot-script)\n (exit 1))\n (let* ([cmd (format \"~a -q --libdirs '~a' --script ~a jsh-generated.ss jsh-generated.wp.so\"\n scheme-cmd libdirs-str build-boot-script)]\n [rc (system cmd)])\n (unless (zero? rc)\n (fprintf (current-error-port)\n \"FATAL: WPO subprocess failed (rc=~a)~ncmd: ~a~n\" rc cmd)\n (exit 1))))\n\n;; Verify jsh-generated.wp.so was created by subprocess\n(unless (file-exists? \"jsh-generated.wp.so\")\n (fprintf (current-error-port) \"FATAL: jsh-generated.wp.so was not created~n\")\n (exit 1))\n\n;; ========== Step 3: (subsumed by step 2 subprocess) ==========\n(define program-so \"jsh-generated.wp.so\")\n\n;; Step 3.5 (precompile-boot-jerboa-modules!) + Step 4 (make-boot-file\n;; \"jsh.boot\") are subsumed by WPO above: every imported library is inlined\n;; into jsh-generated.wp.so by compile-whole-program.\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-macos-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define gcc \"cc\")\n\n(define (resolve-llvm-ar)\n (cond\n [(let ([p (getenv \"LLVM_AR\")])\n (and p (not (string=? p \"\")) p))]\n [(file-exists? \"/opt/homebrew/opt/llvm/bin/llvm-ar\")\n \"/opt/homebrew/opt/llvm/bin/llvm-ar\"]\n [else #f]))\n\n(define llvm-ar (resolve-llvm-ar))\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" home-dir \"=~\"))\n\n;; Helper: write file as C byte array directly to output port (avoids O(n^2) string-append)\n(define (write-c-array filepath varname out)\n (let* ([bv (call-with-port (open-file-input-port filepath) get-bytevector-all)]\n [len (bytevector-length bv)]\n [hex \"0123456789abcdef\"])\n (fprintf out \"static const unsigned char ~a[] = {~n\" varname)\n (do ([i 0 (+ i 1)])\n ((= i len))\n (when (and (> i 0) (= (mod i 16) 0)) (display \",\\n\" out))\n (when (and (> i 0) (not (= (mod i 16) 0))) (display \",\" out))\n (display \"0x\" out)\n (let ([b (bytevector-u8-ref bv i)])\n (display (string-ref hex (fxsrl b 4)) out)\n (display (string-ref hex (fxand b 15)) out)))\n (fprintf out \"~n};~nstatic const unsigned int ~a_len = ~a;~n\" varname len)))\n\n;; Generate static_boot.c\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n (write-c-array petite-boot-path \"petite_boot\" out) (newline out)\n (write-c-array scheme-boot-path \"scheme_boot\" out) (newline out)\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n;; FFI symbol whitelist — auto-generated from ffi-shim.c plus a small set of\n;; non-ffi_ helpers (Cage/Landlock wrappers, Rust-native shims).\n(define ffi-shim-symbols\n (append (read-symbol-list \"ffi-shim-symbols.list\")\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\")))\n\n(define native-symbols\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\"\n \"jerboa_x509_cert_fingerprint\"\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n\n(define native-int-symbols\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n\n;; High-level jsh_* coreutils commands (from Rust jerboa-coreutils).\n;; On macOS these are stubbed out — the Rust coreutils lib is not yet built.\n(define jsh-coreutils-commands\n '(\"jsh_arch\" \"jsh_b2sum\" \"jsh_base32\" \"jsh_base64\" \"jsh_basename\" \"jsh_basenc\"\n \"jsh_cat\" \"jsh_chgrp\" \"jsh_chmod\" \"jsh_chown\" \"jsh_chroot\" \"jsh_cksum\"\n \"jsh_comm\" \"jsh_cp\" \"jsh_csplit\" \"jsh_cu_realpath\" \"jsh_cut\" \"jsh_date\"\n \"jsh_dd\" \"jsh_df\" \"jsh_dir\" \"jsh_dircolors\" \"jsh_dirname\" \"jsh_du\"\n \"jsh_echo\" \"jsh_env\" \"jsh_expand\" \"jsh_expr\" \"jsh_factor\" \"jsh_fmt\"\n \"jsh_fold\" \"jsh_grep\" \"jsh_groups\" \"jsh_head\" \"jsh_hostid\" \"jsh_hostname\"\n \"jsh_id\" \"jsh_install\" \"jsh_join\" \"jsh_kill\" \"jsh_link\" \"jsh_ln\"\n \"jsh_logname\" \"jsh_ls\" \"jsh_md5sum\" \"jsh_mkdir\" \"jsh_mkfifo\" \"jsh_mknod\"\n \"jsh_mktemp\" \"jsh_mv\" \"jsh_nice\" \"jsh_nl\" \"jsh_nohup\" \"jsh_nproc\"\n \"jsh_numfmt\" \"jsh_od\" \"jsh_paste\" \"jsh_pathchk\" \"jsh_pinky\" \"jsh_pr\"\n \"jsh_printenv\" \"jsh_printf\" \"jsh_ptx\" \"jsh_pwd\" \"jsh_readlink\" \"jsh_rm\"\n \"jsh_rmdir\" \"jsh_seq\" \"jsh_sha1sum\" \"jsh_sha224sum\" \"jsh_sha256sum\"\n \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_shred\" \"jsh_shuf\" \"jsh_sleep\"\n \"jsh_sort\" \"jsh_split\" \"jsh_stat\" \"jsh_stty\" \"jsh_sum\" \"jsh_sync\"\n \"jsh_tac\" \"jsh_tail\" \"jsh_tee\" \"jsh_test\" \"jsh_timeout\" \"jsh_touch\"\n \"jsh_tr\" \"jsh_truncate\" \"jsh_tsort\" \"jsh_tty\" \"jsh_uname\" \"jsh_unexpand\"\n \"jsh_uniq\" \"jsh_unlink\" \"jsh_uptime\" \"jsh_users\" \"jsh_vdir\" \"jsh_wc\"\n \"jsh_who\" \"jsh_whoami\" \"jsh_yes\"))\n\n(define coreutils-symbols\n '(\"coreutils_chmod\" \"coreutils_lstat_mode\" \"coreutils_stat_isdir\"\n \"coreutils_chown\" \"coreutils_lchown\"\n \"coreutils_getpwnam_uid\" \"coreutils_getgrnam_gid\"\n \"coreutils_stat_call\" \"coreutils_stat_get\"\n \"coreutils_uid_to_name\" \"coreutils_gid_to_name\"\n \"coreutils_du_stat\" \"coreutils_statvfs\" \"coreutils_statvfs_get\"\n \"coreutils_test_access\" \"coreutils_test_stat\"\n \"coreutils_ls_lstat\" \"coreutils_ls_stat_get\" \"coreutils_ls_readlink\"\n \"coreutils_isatty\" \"coreutils_time_format\"\n \"coreutils_terminal_width\" \"coreutils_terminal_height\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_cp_lstat\" \"coreutils_cp_stat_get\" \"coreutils_cp_readlink\"\n \"coreutils_symlink\" \"coreutils_link\" \"coreutils_utime\"\n \"coreutils_mkdir\" \"coreutils_lstat_type\"\n \"coreutils_unlink\" \"coreutils_rmdir\" \"coreutils_access_w\"\n \"coreutils_rename\" \"coreutils_stat_get_mode\"\n \"coreutils_stat_atime\" \"coreutils_stat_mtime\"\n \"coreutils_file_size\" \"coreutils_fsync\"\n \"coreutils_chgrp_chown\" \"coreutils_chgrp_lchown\"\n \"coreutils_mkstemp\" \"coreutils_mkstemp_get_path\"\n \"coreutils_mkdtemp\" \"coreutils_readlink\" \"coreutils_realpath\"\n \"coreutils_stat_size\" \"coreutils_fsync_path\"))\n\n(define ssh-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n\n;; jerboa_ssh_crypto.c symbols (used by ssh/transport sub-library)\n(define ssh-crypto-symbols\n '(\"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\" \"jerboa_ssh_sha512\"\n \"jerboa_ssh_hmac_sha256\" \"jerboa_ssh_hmac_sha512\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_aes256_ctr_init\" \"jerboa_ssh_aes256_ctr_process\" \"jerboa_ssh_aes256_ctr_free\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_ed25519_sign\" \"jerboa_ssh_ed25519_derive_pubkey\"\n \"jerboa_ssh_tcp_connect\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_write\"\n \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_set_nodelay\"))\n\n;; OpenSSL symbols called directly as (foreign-procedure \"NAME\" ...) by vault/crypto.sls.\n;; The vault's load-shared-object patch leaves _loaded=#t (guard returns #t when no exception),\n;; so these foreign-procedure calls ARE evaluated. Since we link -lssl -lcrypto, we register\n;; the actual function pointers here so Chez can find them.\n(define openssl-ffi-symbols\n '(\"RAND_bytes\"\n \"EVP_sha256\"\n \"PKCS5_PBKDF2_HMAC\"\n \"EVP_CIPHER_CTX_new\"\n \"EVP_CIPHER_CTX_free\"\n \"EVP_aes_256_gcm\"\n \"EVP_EncryptInit_ex\"\n \"EVP_EncryptUpdate\"\n \"EVP_EncryptFinal_ex\"\n \"EVP_CIPHER_CTX_ctrl\"\n \"EVP_DecryptInit_ex\"\n \"EVP_DecryptUpdate\"\n \"EVP_DecryptFinal_ex\"))\n\n;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n(define vault-fuse-symbols\n '(;; Secure memory\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n ;; Process tree\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n ;; FUSE device + mount\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n\n;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n;; jerboa_aead_seal, jerboa_aead_open — all in libjerboa_native (ring). No libcrypto needed.\n;; POSIX symbols needed by vault code (pread/pwrite for file I/O, fsync, uid/gid)\n(define vault-crypto-symbols\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\"))\n\n;; Generate jsh_main_macos.c\n(define program-c (format \"~a/jsh_main_macos.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <mach-o/dyld.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include <dlfcn.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n\n ;; Embed program .so\n (write-c-array program-so \"jsh_program_data\" out)\n (newline out)\n\n ;; Declare static_boot_init\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n\n ;; Declare FFI symbols\n (display \"/* FFI symbols from ffi-shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n\n ;; Rust native symbols\n (when has-native-lib?\n (display \"\\n#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n native-symbols)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n native-int-symbols)\n (display \"#endif\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n\n ;; Coreutils FFI\n (display \"\\n/* FFI symbols from libcoreutils.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n coreutils-symbols)\n\n ;; High-level jsh_* coreutils commands (from Rust libjsh_coreutils.a)\n (display \"\\n/* jsh_* coreutils commands */\\n\" out)\n (if has-rust-coreutils?\n (begin\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n jsh-coreutils-commands))\n (begin\n (display \"/* Stubs — Rust coreutils not built */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"int ~a(int ac, const char **av) { return 127; }\\n\" name))\n jsh-coreutils-commands)\n (display \"void jsh_coreutils_init(int a, char **b) { }\\n\" out)))\n\n ;; jerboa-ssh\n (display \"\\n/* FFI symbols from jerboa_ssh_shim.c + jerboa_ssh_crypto.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n (append ssh-symbols ssh-crypto-symbols))\n\n ;; OpenSSL symbols — called directly as (foreign-procedure \"NAME\" ...) by vault/crypto.sls.\n ;; Linked via -lssl -lcrypto so the symbols are in the binary; we just need to register them.\n (display \"\\n/* OpenSSL symbols used by vault/crypto.sls */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n openssl-ffi-symbols)\n\n ;; jerboa-fuse vault (crypto symbols now from libjerboa_native via ring)\n (display \"/* FFI symbols for vault (from ffi-shim.c + libjerboa_native) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n vault-fuse-symbols)\n (newline out)\n\n ;; POSIX wrappers\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n\n ;; macOS errno compatibility — __errno_location doesn't exist on macOS\n (display \"/* macOS errno compatibility */\\n\" out)\n (display \"static int *macos_errno_location(void) { return &errno; }\\n\\n\" out)\n\n ;; Stubs for symbols not available on macOS\n ;; (regex extended, epoll, inotify, landlock, seccomp)\n (display \"/* Stubs for Linux-only / missing native symbols */\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n ;; Regex stubs only when libjerboa_native.a is absent — it provides real implementations\n (unless has-native-lib?\n (display \"void *jerboa_regex_compile_ex(const char *p, int f) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_find_at(void *r, const char *s, int o, int *ms, int *me) { return 0; }\\n\" out)\n (display \"char *jerboa_regex_captures(void *r, const char *s, int n) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_group_count(void *r) { return 0; }\\n\" out))\n (display \"int jerboa_epoll_create(void) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_ctl(int e, int o, int f, int ev) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_wait(int e, void *ev, int m, int t) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_close(int e) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_init(void) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_add_watch(int f, const char *p, int m) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_rm_watch(int f, int w) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_read(int f, void *b, int s) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_close(int f) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_create_ruleset(void) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_path_rule(int r, const char *p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_net_rule(int r, int p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_enforce(int r) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_available(void) { return 0; }\\n\" out)\n (display \"int jerboa_seccomp_lock(void) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_lock_strict(void) { return -1; }\\n\\n\" out)\n\n ;; htons/htonl are macros on macOS and cannot be used as function pointers directly.\n ;; Emit thin wrappers so Sforeign_symbol can register them.\n (display \"static unsigned short jsh_htons(unsigned short x) { return htons(x); }\\n\" out)\n (display \"static unsigned int jsh_htonl(unsigned int x) { return htonl(x); }\\n\\n\" out)\n\n ;; register_ffi_symbols\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Rust native\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append native-symbols native-int-symbols))\n (display \"#endif\\n\" out))\n ;; POSIX\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"readlink\" \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"\n ;; vault blockstore\n \"flock\" \"pread\" \"pwrite\" \"fsync\"\n ;; top builtin\n \"setpriority\"))\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\" \"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; macOS: __errno_location (Linux glibc) and __error (BSD) → our wrapper\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)macos_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)macos_errno_location);\\n\" out)\n ;; Register stub symbols for Linux-only / missing native functionality\n ;; Note: jerboa_regex_*_ex symbols are omitted here — when has-native-lib? they\n ;; are real symbols registered above via native-symbols; without it they are\n ;; declared as stubs in the definitions section above register_ffi_symbols.\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n ;; When native lib absent, also register the regex_ex stubs\n (unless has-native-lib?\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\")))\n ;; coreutils\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n coreutils-symbols)\n ;; jsh_* coreutils commands (stubs)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n jsh-coreutils-commands)\n (fprintf out \" Sforeign_symbol(\\\"jsh_coreutils_init\\\", (void*)jsh_coreutils_init);\\n\")\n ;; jerboa-ssh\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append ssh-symbols ssh-crypto-symbols))\n ;; OpenSSL — vault/crypto.sls calls these as foreign-procedure\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n openssl-ffi-symbols)\n ;; jerboa-fuse vault\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-fuse-symbols)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-crypto-symbols)\n ;; Sockets — htons/htonl are macros on macOS, use wrappers\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"))\n (display \" Sforeign_symbol(\\\"htons\\\", (void*)jsh_htons);\\n\" out)\n (display \" Sforeign_symbol(\\\"htonl\\\", (void*)jsh_htonl);\\n\" out)\n (display \"}\\n\\n\" out)\n\n ;; Custom main — macOS\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; macOS: _NSGetExecutablePath for exe path\n (display \" /* Resolve exe path via _NSGetExecutablePath (macOS) */\\n\" out)\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" uint32_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (_NSGetExecutablePath(exe_buf, &exe_len) == 0) {\\n\" out)\n (display \" char resolved[4096];\\n\" out)\n (display \" if (realpath(exe_buf, resolved))\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", resolved, 1);\\n\" out)\n (display \" else\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; macOS: no memfd_create — use tmpfile.\n (display \" /* macOS: extract program .so to tmpfile */\\n\" out)\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-program-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program_data, 1, jsh_program_data_len, fp) != jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"fwrite tmpfile\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C ==========\n\n(printf \"[6/7] Compiling C with cc (clang)...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-macos \"Command failed\" cmd)))\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_macos.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_macos.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; landlock-shim.c — Landlock is Linux-only; always use stub on macOS\n;; Note: ffi_landlock_abi_version and ffi_landlock_sandbox are already defined\n;; in ffi-shim.c (with macOS stubs), so we only emit the ffi_landlock_create/add/enforce\n;; and jerboa_landlock_* symbols here.\n(begin\n (printf \" Landlock is Linux-only, generating stub for macOS~n\")\n (system (format \"echo 'int ffi_landlock_create_ruleset(void) { return -1; } int ffi_landlock_add_path_rule(int a, const char *b, int c) { return -1; } int ffi_landlock_add_net_rule(int a, int b, int c) { return -1; } int ffi_landlock_enforce(int a) { return -1; } int jerboa_landlock_abi_version(void) { return -1; } int jerboa_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int jerboa_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, unsigned long long p) { return -1; }' | ~a -c -x c ~a -o '~a/landlock-shim.o' -\"\n gcc harden-cflags build-dir)))\n\n;; coreutils FFI shim\n(if (file-exists? coreutils-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir coreutils-shim))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))))\n\n;; embed-crypto.c (ChaCha20-Poly1305 AEAD for embedded file encryption)\n;; When libjerboa_native.a is present it already exports embed_pbkdf2_sha256,\n;; embed_encrypt, embed_decrypt, embed_random_bytes. Compiling embed-crypto.c\n;; would produce duplicate strong symbols that macOS ld rejects. Use an empty\n;; stub when the Rust native lib is available; compile the full C source otherwise.\n(let ([embed-crypto-src \"embed-crypto.c\"])\n (if has-native-lib?\n (begin\n (printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir)))\n (if (file-exists? embed-crypto-src)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/embed-crypto.o' '~a' -Wall\"\n gcc harden-cflags build-dir embed-crypto-src))\n (begin\n (printf \" Warning: embed-crypto.c not found~n\")\n (system (format \"echo 'int embed_pbkdf2_sha256(void){return -1;} int embed_encrypt(void){return -1;} int embed_decrypt(void){return -1;} int embed_random_bytes(void){return -1;}' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))))))\n\n;; jerboa-ssh shim\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Use standalone ed25519 backend (Rust libjerboa_native provides the symbols)\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c (SSH transport crypto + TCP — requires OpenSSL)\n (let ([crypto-src (dep-file \"jerboa-ssh\" \"jerboa_ssh_crypto.c\")])\n (if (file-exists? crypto-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jerboa-ssh-crypto.o' '~a' -Wall\"\n gcc harden-cflags openssl-include-dir build-dir crypto-src))\n (begin\n (printf \" Warning: jerboa_ssh_crypto.c not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))))))\n (begin\n (printf \" Warning: jerboa-ssh shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking ~a binary...~n\" output-name)\n\n;; When both Rust archives are present, merge them into one to deduplicate Rust\n;; runtime symbols (rust_eh_personality, std::panicking, etc.) that appear as\n;; strong (T/S) symbols in both libjerboa_native.a and libjsh_coreutils.a.\n;;\n;; Strategy:\n;; 1. Extract native objects directly into merge-dir (strong originals kept).\n;; 2. Extract coreutils objects into a subdirectory, then use llvm-objcopy to\n;; weaken any symbol that is strongly defined in BOTH archives — so the\n;; linker picks the native copy and ignores the weak coreutils duplicate.\n;; 3. Copy weakened coreutils objects with a \"cu-\" prefix (avoiding name\n;; collisions with native objects) and re-archive everything.\n;; Python script to make duplicate global symbols local (clear N_EXT) in Mach-O .o files.\n;; llvm-objcopy --weaken-symbol doesn't work on macOS Mach-O; direct byte patching does.\n(define rust-dedup-py\n (let ([py-path (format \"~a/rust_dedup.py\" build-dir)])\n (call-with-output-file py-path\n (lambda (out)\n (display\n (string-append\n \"import sys, struct\\n\"\n \"filename = sys.argv[1]\\n\"\n \"target_syms = set(sys.argv[2:])\\n\"\n \"with open(filename, 'r+b') as f: data = bytearray(f.read())\\n\"\n \"if len(data) < 32: sys.exit(0)\\n\"\n \"magic = struct.unpack_from('<I', data, 0)[0]\\n\"\n \"if magic != 0xFEEDFACF: sys.exit(0) # not 64-bit MachO\\n\"\n \"ncmds = struct.unpack_from('<I', data, 16)[0]\\n\"\n \"cmdoff, symoff, stroff, nsyms = 32, None, None, 0\\n\"\n \"for _ in range(ncmds):\\n\"\n \" cmd, sz = struct.unpack_from('<II', data, cmdoff)\\n\"\n \" if cmd == 2:\\n\"\n \" symoff, nsyms, stroff, _ = struct.unpack_from('<IIII', data, cmdoff+8)\\n\"\n \" break\\n\"\n \" cmdoff += sz\\n\"\n \"if symoff is None: sys.exit(0)\\n\"\n \"N_EXT = 0x01\\n\"\n \"for i in range(nsyms):\\n\"\n \" off = symoff + i * 16\\n\"\n \" n_strx = struct.unpack_from('<I', data, off)[0]\\n\"\n \" n_type = data[off + 4]\\n\"\n \" if n_type & N_EXT:\\n\"\n \" nm = data[stroff+n_strx : data.index(b'\\\\x00', stroff+n_strx)].decode('ascii','replace')\\n\"\n \" if nm in target_syms:\\n\"\n \" data[off + 4] = n_type & ~N_EXT\\n\"\n \"with open(filename, 'wb') as f: f.write(data)\\n\")\n out)))\n py-path))\n\n(define combined-rust-lib\n (and has-native-lib? has-rust-coreutils?\n (let* ([merge-dir (format \"~a/rust-merge\" build-dir)]\n [cu-dir (format \"~a/cu\" merge-dir)]\n [combined (format \"~a/librust_combined.a\" build-dir)])\n (unless llvm-ar\n (error 'build-jsh-macos\n \"LLVM ar is required to read Rust archives on macOS; install llvm or set LLVM_AR\"\n rust-coreutils-lib-path))\n (run-cmd (format \"rm -rf '~a' && mkdir -p '~a' '~a'\" merge-dir merge-dir cu-dir))\n ;; Extract native objects into merge-dir\n (run-cmd (format \"cd '~a' && '~a' x '~a'\" merge-dir llvm-ar native-lib-path))\n ;; Extract coreutils objects into cu-dir\n (run-cmd (format \"cd '~a' && '~a' x '~a'\" cu-dir llvm-ar rust-coreutils-lib-path))\n ;; Find symbols defined (global T/S) in BOTH archives; clear N_EXT in the\n ;; coreutils objects to make them local — linker picks native's definitions.\n ;; Write a shell script to avoid nested-quote hell with bash -c '...awk...'\n (let ([sh-path (format \"~a/dedup.sh\" build-dir)])\n (call-with-output-file sh-path\n (lambda (out)\n (display \"#!/bin/bash\\nset -e\\n\" out)\n (display (format \"NAT=$(nm '~a' 2>/dev/null | awk '/ [TS] /{print $NF}' | sort -u)\\n\"\n native-lib-path) out)\n (display (format \"CU=$(nm '~a' 2>/dev/null | awk '/ [TS] /{print $NF}' | sort -u)\\n\"\n rust-coreutils-lib-path) out)\n (display \"DUPES=$(comm -12 <(echo \\\"$NAT\\\") <(echo \\\"$CU\\\"))\\n\" out)\n (display \"[ -z \\\"$DUPES\\\" ] && exit 0\\n\" out)\n (display (format \"for f in '~a'/*.o; do python3 '~a' \\\"$f\\\" $DUPES; done\\n\"\n cu-dir rust-dedup-py) out)))\n (run-cmd (format \"bash '~a'\" sh-path)))\n ;; Copy patched coreutils objects with \"cu-\" prefix to avoid name conflicts\n (run-cmd (format \"for f in '~a'/*.o; do cp \\\"$f\\\" '~a/cu-'\\\"$(basename $f)\\\"; done\"\n cu-dir merge-dir))\n ;; Build combined archive from all objects\n (run-cmd (format \"'~a' rcs '~a' '~a'/*.o\" llvm-ar combined merge-dir))\n combined)))\n\n;; macOS does not support fully static binaries — link dynamically against system libs.\n(let* ([objs (format \"~a/jsh_main_macos.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/coreutils-ffi.o ~a/landlock-shim.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o\"\n build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir)]\n ;; Use combined archive when both Rust libs present, else fall back individually\n [native-flag (cond [combined-rust-lib (format \" ~a\" combined-rust-lib)]\n [has-native-lib? (format \" ~a\" native-lib-path)]\n [else \"\"])]\n [coreutils-flag (if combined-rust-lib \"\" ; already in combined\n (if has-rust-coreutils? (format \" ~a\" rust-coreutils-lib-path) \"\"))]\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n [cxx-libs (if has-native-lib? \" -lc++\" \"\")]\n [link-libs (format \"-L~a -L~a -L/opt/homebrew/lib -L/usr/local/lib -lssl -lcrypto -lkernel -llz4 -lz -lm -liconv -lncurses -lutil\"\n chez-ta6fb openssl-lib-dir)]\n [link-cmd (format \"~a -o ~a ~a~a~a~a ~a\"\n gcc output-name objs native-flag coreutils-flag\n cxx-libs link-libs)])\n (printf \" ~a~n\" link-cmd)\n (run-cmd link-cmd))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? output-name)\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port output-name))])\n (run-cmd (format \"strip ~a\" output-name))\n (let ([post-size (file-length (open-file-input-port output-name))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n ;; Compute SHA-256 integrity hash\n (printf \"[harden] Computing integrity hash...~n\")\n ;; macOS uses shasum -a 256; FreeBSD uses sha256 -q; Linux uses sha256sum\n (system (format \"shasum -a 256 ~a | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt 2>/dev/null || sha256sum ~a | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\"\n output-name output-name))\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port (string-append output-name \".sha256\")\n (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote ~a.sha256 (32 bytes)~n\" output-name)))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(when enable-aws? (system (format \"rm -rf '~a'\" aws-stage)))\n(system (format \"rm -rf '~a'\" awk-stage))\n(system (format \"rm -rf '~a'\" sed-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Binary created: ~a~n~n\" output-name)\n(system (format \"ls -lh ~a\" output-name))\n(printf \"~n\")\n(system (format \"file ~a\" output-name))\n(printf \"~n\")\n(system (format \"otool -L ~a 2>/dev/null || true\" output-name))\n(printf \"~nTest: ./~a -c 'echo Hello from jsh on macOS'~n\" output-name)\n"} +{"text":";; FILE: jerboa-shell/build-jsh-macos.ss\n#!chezscheme\n;;; build-jsh-macos.ss — Build jsh binary on macOS\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:... < build-jsh-macos.ss\n;;;\n;;; This script:\n;;; 1. Patches coreutils/awk/sed/ssl for static builds (no dlopen)\n;;; 2. Compiles jsh modules (using stock scheme)\n;;; 3. Creates boot file + optimized program .so\n;;; 4. Generates C files with embedded boot data\n;;; 5. Compiles C with cc (clang) against static Chez's scheme.h\n;;; 6. Links fully static binary with libkernel.a\n;;;\n;;; The resulting jsh-macos binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") (format \"/Users/~a\" (getenv \"USER\"))))\n(define script-dir (or (getenv \"SCRIPT_DIR\") (current-directory)))\n(define output-name (or (getenv \"JSH_OUTPUT\") \"jsh-macos\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-macos.sh so we know the repo root.\n(define vendor-dir (format \"~a/vendor\" script-dir))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. Callers wrap with (or (getenv \"X\") (dep ...))\n;; to allow env var overrides from the shell script.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jerboa-sqlite/src\" home-dir)))\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (dep \"jerboa-coreutils\" \"lib\")))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n(define coreutils-shim\n (let ([upstream (dep-file \"jerboa-coreutils\" \"support/libcoreutils.c\")]\n [local \"patches/libcoreutils.c\"])\n (cond\n [(file-exists? upstream) upstream]\n [(file-exists? local) local]\n [else upstream])))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls).\n;; rustls is preferred over OpenSSL for security.\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; Feature-gated enable flags — gate on BOTH directory existence AND\n;; the feature being in *enabled-features*. This is how JSH_FEATURES\n;; actually controls whether feature dependencies land in the binary.\n(define enable-aws?\n (and has-aws? (memq 'aws *enabled-features*)))\n\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n;; Rust native library — resolve via vendor/ → ~/mine/ → ~/\n(define native-rs-dir\n (let* ([v (format \"~a/jerboa/jerboa-native-rs\" vendor-dir)]\n [m (format \"~a/mine/jerboa/jerboa-native-rs\" home-dir)]\n [h (format \"~a/jerboa/jerboa-native-rs\" home-dir)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n(define native-lib-path\n (format \"~a/target/release/libjerboa_native.a\" native-rs-dir))\n(define native-src-dir\n (format \"~a/src\" native-rs-dir))\n;; Sentinel file written after a successful native build without SQLite.\n;; If absent, the .a was built with default (tls-only) features — must rebuild.\n(define native-features-sentinel\n (format \"~a/target/release/.built-with-tls-crypto-no-sqlite\" native-rs-dir))\n;; Only attempt Rust rebuild if cargo is available (pre-built .a may have been\n;; downloaded by build-jsh-macos.sh — don't clobber it with a failed cargo call)\n(define has-cargo?\n (= 0 (system \"command -v cargo >/dev/null 2>&1\")))\n(when (and has-cargo?\n (file-exists? native-src-dir)\n (or (not (file-exists? native-lib-path))\n ;; Features sentinel absent → stale build (wrong feature set)\n (not (file-exists? native-features-sentinel))\n ;; Check if any .rs file is newer than the .a\n (let ([lib-mtime (file-modification-time native-lib-path)])\n (let check ([files (directory-list native-src-dir)])\n (and (pair? files)\n (let ([f (format \"~a/~a\" native-src-dir (car files))])\n (or (and (> (string-length (car files)) 3)\n (string=? \".rs\" (substring (car files)\n (- (string-length (car files)) 3)\n (string-length (car files))))\n (time>? (file-modification-time f) lib-mtime))\n (check (cdr files)))))))))\n (printf \"~n[0/7] Rebuilding Rust native library (source newer than .a)...~n\")\n (let ([rc (safe-system (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote native-rs-dir)))])\n (unless (= rc 0)\n (fprintf (current-error-port) \"FATAL: cargo build --release --no-default-features --features tls,crypto failed~n\")\n (exit 1)))\n ;; Write sentinel so next build knows the right features were used\n (let ([port (open-output-file native-features-sentinel 'truncate)])\n (display \"tls,crypto,no-sqlite\\n\" port)\n (close-output-port port)))\n(when (and (file-exists? native-lib-path)\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(define has-native-lib? (file-exists? native-lib-path))\n\n;; Rust coreutils static library\n(define rust-host-triple\n (case (machine-type)\n [(tarm64osx) \"aarch64-apple-darwin\"]\n [(ta6osx) \"x86_64-apple-darwin\"]\n [else #f]))\n\n(define rust-coreutils-lib-path\n (or (getenv \"RUST_COREUTILS_LIB\")\n (if rust-host-triple\n (format \"~a/rust-coreutils/target/~a/release/libjsh_coreutils.a\"\n script-dir rust-host-triple)\n (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" script-dir))))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — coreutils builtins will be stubs~n\"))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\"))\n\n;; Chez Scheme static installation\n;; macOS: machine type is tarm64osx (arm64) or ta6osx (x86_64)\n(define chez-machine\n (or (getenv \"CHEZ_MACHINE\")\n (machine-type)))\n\n(define chez-ta6fb\n (or (getenv \"CHEZ_TA6FB\")\n ;; Search Homebrew paths first, then /usr/local\n (let loop ([prefixes '(\"/opt/homebrew/Cellar/chezscheme\" \"/opt/homebrew/lib\" \"/usr/local/lib\")])\n (if (null? prefixes)\n (error 'build \"Cannot find Chez static directory (libkernel.a). Install: brew install chezscheme\")\n (let ([prefix (car prefixes)])\n (if (file-directory? prefix)\n (let check-dirs ([dirs (directory-list prefix)])\n (cond\n [(null? dirs) (loop (cdr prefixes))]\n [else\n (let* ([d (car dirs)]\n ;; For Cellar layout: /opt/homebrew/Cellar/chezscheme/<ver>/lib/csv<ver>/<machine>\n [cellar-path (format \"~a/~a/lib\" prefix d)]\n [direct-path (format \"~a/~a\" prefix d)])\n (cond\n ;; Cellar: check <prefix>/<ver>/lib/csv*/<machine>/libkernel.a\n [(and (file-directory? cellar-path)\n (let ([csv-dirs (filter (lambda (x) (string-prefix? \"csv\" x))\n (directory-list cellar-path))])\n (and (pair? csv-dirs)\n (let ([p (format \"~a/~a/~a\" cellar-path (car csv-dirs) chez-machine)])\n (and (file-exists? (format \"~a/libkernel.a\" p)) p)))))\n => (lambda (p) p)]\n ;; Direct: check <prefix>/csv*/<machine>/libkernel.a\n [(and (string-prefix? \"csv\" d)\n (file-directory? direct-path)\n (let ([p (format \"~a/~a\" direct-path chez-machine)])\n (and (file-exists? (format \"~a/libkernel.a\" p)) p)))\n => (lambda (p) p)]\n [else (check-dirs (cdr dirs))]))]))\n (loop (cdr prefixes))))))))\n\n(define scheme-h-dir chez-ta6fb)\n(define petite-boot-path (format \"~a/petite.boot\" chez-ta6fb))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-ta6fb))\n\n;; OpenSSL include directory — still needed for jerboa_ssh_crypto.c (SSH transport)\n;; libcrypto.a is NO LONGER linked; vault/crypto.sls uses ring via jerboa_native instead\n(define openssl-include-dir\n (let ([brew-inc \"/opt/homebrew/opt/openssl/include\"]\n [brew-inc-x86 \"/usr/local/opt/openssl/include\"])\n (cond\n [(file-directory? brew-inc) brew-inc]\n [(file-directory? brew-inc-x86) brew-inc-x86]\n [else \"/usr/include\"])))\n\n(define openssl-lib-dir\n (let ([brew-lib \"/opt/homebrew/opt/openssl/lib\"]\n [brew-lib-x86 \"/usr/local/opt/openssl/lib\"])\n (cond\n [(file-directory? brew-lib) brew-lib]\n [(file-directory? brew-lib-x86) brew-lib-x86]\n [else \"/usr/lib\"])))\n\n(printf \"Chez static: ~a~n\" chez-ta6fb)\n(printf \"Native lib: ~a~n\" (if has-native-lib? native-lib-path \"not found\"))\n(printf \"~n\")\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo so the broken vendor source is recompiled below.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n;; Compile the Jerboa runtime and stdlib entries before any staged dependency\n;; or jsh module. If these are compiled later, the boot image can embed a\n;; different compilation instance of (jerboa core) than the modules depend on.\n(define boot-jerboa-modules\n '(\"jerboa/runtime\"\n \"std/typed\" \"std/pregexp\" \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\"\n \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/misc/thread\"\n \"jerboa/core\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/alist\"\n \"std/stm\" \"std/foreign\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"\n ;; (std contract condition) — imported by (jerboa core); only has a .ss\n ;; (no .sls), so compile-imported-libraries doesn't auto-write its .so\n ;; and compile-whole-program can't find its .wpo. Force-precompile here.\n \"std/contract/condition\"))\n\n(define (precompile-boot-jerboa-modules! label)\n (printf \"~a~n\" label)\n ;; Parameter settings here must match the step [2/7] compile-program block\n ;; that builds jsh-generated.so. WPO files from a different optimize-level\n ;; or unsafe-* setting are flagged as \"does not define expected compilation\n ;; instance\" by compile-whole-program and fail the build.\n (parameterize ([compile-imported-libraries #t]\n [generate-wpo-files #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (cons (cons jerboa-dir jerboa-dir)\n (library-directories))])\n (for-each\n (lambda (m)\n ;; Source may be either .sls (R6RS) or .ss (Jerboa convention);\n ;; check both. Source absent => skip (module not vendored).\n ;; Compile failures are caught and logged so platform-specific\n ;; modules (e.g. (std os landlock) on macOS) don't abort the loop.\n (let* ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [ss (format \"~a/~a.ss\" jerboa-dir m)]\n [src (cond [(file-exists? sls) sls]\n [(file-exists? ss) ss]\n [else #f])]\n [so (format \"~a/~a.so\" jerboa-dir m)]\n [wpo (format \"~a/~a.wpo\" jerboa-dir m)])\n (when (and src\n (or (not (file-exists? so))\n (not (file-exists? wpo))))\n (printf \" Pre-compiling ~a~n\" src)\n (guard (exn [(condition? exn)\n (printf \" SKIP ~a: ~a~n\"\n m (condition-message-string exn))])\n (compile-library src)))))\n boot-jerboa-modules)))\n\n(define (condition-message-string c)\n ;; Best-effort one-line summary of a Chez condition for skip-log output.\n (cond [(and (condition? c) (message-condition? c))\n (condition-message c)]\n [else (format \"~s\" c)]))\n\n;; Skipped: WPO at step [2/7] now precompiles all transitive imports with\n;; compatible parameter settings. Pre-staging at lower optimize-level here\n;; produced .wpo files that compile-whole-program rejected as \"wrong\n;; compilation instance\".\n;; (precompile-boot-jerboa-modules!\n;; \"[0pre/7] Pre-compiling Jerboa boot dependencies...\")\n\n;; ========== Step 0: Patch coreutils for static builds ==========\n;; Coreutils modules call (load-shared-object #f) at library init time.\n;; In static builds, load-shared-object throws because dlopen is unavailable.\n;; Since FFI symbols are pre-registered via Sforeign_symbol, we patch these out.\n\n(printf \"[0/7] Patching coreutils for static build (no dlopen)...~n\")\n\n(define coreutils-stage (format \"~a/coreutils-stage\" (current-directory)))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(system (format \"mkdir -p '~a'\" coreutils-stage))\n\n(system (format \"cp -a '~a/jerboa-coreutils' '~a/'\"\n coreutils-dir coreutils-stage))\n;; macOS/FreeBSD sed uses -i '' instead of -i (no backup extension)\n(system (format \"find '~a/jerboa-coreutils' -name '*.sls' -exec sed -i '' 's/(load-shared-object #f)/(void)/g' {} +\"\n coreutils-stage))\n;; These modules import string-split explicitly from (std misc string). Newer\n;; (jerboa core) also re-exports string-split, so exclude it from core here.\n(for-each\n (lambda (name)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage name)])\n (when (file-exists? path)\n (system (format \"sed -i '' 's/(jerboa core)/(except (jerboa core) string-split)/' '~a'\"\n path)))))\n '(\"cut.sls\" \"grep.sls\" \"join.sls\"))\n(system (format \"find '~a/jerboa-coreutils' -name '*.so' -delete\"\n coreutils-stage))\n(system (format \"find '~a/jerboa-coreutils' -name '*.wpo' -delete\"\n coreutils-stage))\n\n(printf \" Recompiling patched coreutils...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"common.sls\" \"common/version.sls\" \"common/io.sls\" \"common/security.sls\"))\n (for-each\n (lambda (name)\n (let ([sls (format \"~a/jerboa-coreutils/~a.sls\" coreutils-stage name)])\n (when (file-exists? sls)\n (compile-library sls))))\n '(\"basename\" \"dirname\" \"link\" \"unlink\" \"yes\" \"printenv\"\n \"sleep\" \"whoami\" \"logname\" \"hostname\" \"nproc\" \"tty\" \"sync\" \"hostid\"\n \"cat\" \"head\" \"tail\" \"tac\" \"tee\" \"wc\" \"nl\" \"fold\" \"expand\" \"unexpand\" \"fmt\"\n \"cut\" \"paste\" \"join\" \"comm\" \"sort\" \"uniq\" \"tr\" \"numfmt\"\n \"mkdir\" \"rmdir\" \"mktemp\" \"touch\" \"readlink\" \"realpath\" \"ln\" \"cp\" \"mv\" \"rm\"\n \"install\" \"shred\"\n \"ls\" \"chmod\" \"chown\" \"chgrp\" \"stat\" \"du\" \"df\" \"pathchk\"\n \"date\" \"id\" \"groups\" \"who\" \"users\" \"pinky\" \"uptime\" \"uname\" \"arch\"\n \"seq\" \"expr\" \"basenc\" \"base64\" \"base32\" \"od\"\n \"cksum\" \"md5sum\" \"sha1sum\" \"sha224sum\" \"sha256sum\" \"sha384sum\" \"sha512sum\"\n \"b2sum\" \"sum\"\n \"env\" \"timeout\" \"nice\" \"nohup\" \"chroot\" \"stdbuf\"\n \"truncate\" \"mkfifo\" \"mknod\" \"split\" \"csplit\" \"dd\" \"dircolors\"\n \"tsort\" \"shuf\" \"factor\" \"pr\" \"ptx\" \"stty\"\n \"chcon\" \"runcon\"\n \"dir\" \"vdir\" \"rev\" \"top\")))\n\n;; grep + Rust-backed PCRE2\n(let ([grep-pcre2-patch (format \"~a/patches/grep-pcre2.sls\" (current-directory))])\n (when (file-exists? grep-pcre2-patch)\n (system (format \"mkdir -p '~a/jerboa-coreutils/grep'\" coreutils-stage))\n (system (format \"cp '~a' '~a/jerboa-coreutils/grep/pcre2.sls'\"\n grep-pcre2-patch coreutils-stage))))\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (let ([pcre2-sls (format \"~a/jerboa-coreutils/grep/pcre2.sls\" coreutils-stage)])\n (when (file-exists? pcre2-sls)\n (printf \" Compiling grep/pcre2...~n\")\n (compile-library pcre2-sls)))\n (let ([grep-sls (format \"~a/jerboa-coreutils/grep.sls\" coreutils-stage)])\n (when (file-exists? grep-sls)\n (printf \" Compiling grep...~n\")\n (compile-library grep-sls))))\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: patch pcre2 to use Rust regex\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if enable-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when enable-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (library-directories))])\n (when enable-aws?\n (printf \" Compiling jerboa-aws...~n\")\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0d: Stage jerboa-ssh for static build ==========\n(printf \"[0d/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build\n (system (format \"find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"sed -i '' 's/base64-encode/b64-encode/g' '~a'\" kh))\n (system (format \"sed -i '' 's/base64-decode/b64-decode/g' '~a'\" kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0e: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0e/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n;; Use a separate staging directory for macOS so we don't clobber the\n;; committed musl-targeted vault-stage/ (which has _loaded #f baked in).\n;; Each build cleans + repopulates its own dir from upstream jerboa-fuse.\n(define vault-stage (format \"~a/vault-stage-macos\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" vault-stage vault-stage))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n(when has-jerboa-fuse?\n ;; Copy the jerboa-fuse library tree (chez/fuse/ and chez/vault/)\n (system (format \"mkdir -p '~a/chez/fuse' '~a/chez/vault'\" vault-stage vault-stage))\n ;; FUSE layer\n (system (format \"cp '~a/chez/fuse.sls' '~a/chez/fuse.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/constants.sls' '~a/chez/fuse/constants.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/types.sls' '~a/chez/fuse/types.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/codec.sls' '~a/chez/fuse/codec.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/mount.sls' '~a/chez/fuse/mount.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/access.sls' '~a/chez/fuse/access.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/secmem.sls' '~a/chez/fuse/secmem.sls'\" jerboa-fuse-dir vault-stage))\n ;; Vault layer\n (system (format \"cp '~a/chez/vault.sls' '~a/chez/vault.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/format.sls' '~a/chez/vault/format.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/crypto.sls' '~a/chez/vault/crypto.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/blockstore.sls' '~a/chez/vault/blockstore.sls'\" jerboa-fuse-dir vault-stage))\n ;; Patch out ALL load-shared-object calls (FUSE mount helper + libcrypto + libc)\n ;; Use (if #f #f) instead of (void) since some modules only import (rnrs)\n ;; Simple single-level calls:\n (system (format \"find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object[^)]*)/(if #f #f)/g' {} +\" vault-stage))\n ;; fuse.sls and blockstore.sls have multi-line (load-shared-object (case ...)) blocks\n ;; that the simple sed can't handle. Use Scheme to patch them out.\n (let ([str-has? (lambda (haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))])\n (for-each\n (lambda (file-path)\n (when (file-exists? file-path)\n (let* ([content (let ([p (open-input-file file-path)])\n (let loop ([lines '()])\n (let ([l (get-line p)])\n (if (eof-object? l)\n (begin (close-input-port p) (reverse lines))\n (loop (cons l lines))))))]\n [patched\n (let loop ([lines content] [acc '()] [skip 0])\n (if (null? lines)\n (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (or (str-has? line \"(define _libc-loaded\")\n (str-has? line \"(define libc-loaded\")))\n (let ([name (if (str-has? line \"_libc-loaded\")\n \"_libc-loaded\" \"libc-loaded\")])\n (loop (cdr lines)\n (cons (format \" (define ~a #t)\" name) acc)\n 1))]\n [(and (> skip 0)\n (or (str-has? line \"#t))\")\n (str-has? line \"#f))\")))\n (loop (cdr lines) acc 0)]\n [(> skip 0)\n (loop (cdr lines) acc skip)]\n [else\n (loop (cdr lines) (cons line acc) 0)]))))])\n (let ([p (open-output-file file-path 'replace)])\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched)\n (close-output-port p)))))\n (list (format \"~a/chez/vault/blockstore.sls\" vault-stage)\n (format \"~a/chez/fuse.sls\" vault-stage)))) ;; close let\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up (format → crypto → secmem → mount → constants → types → codec → access → blockstore → fuse → vault)\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n ;; Format layer (no deps)\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n ;; FUSE foundation\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n ;; Secure memory + access control (depend on mount)\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n ;; Vault crypto (depends on format + libcrypto)\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n ;; Vault blockstore (depends on format + crypto + secmem)\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n ;; FUSE main (depends on all fuse sub-modules)\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n ;; Vault main (depends on everything)\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"~n[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(or (not (file-exists? so))\n (time>? (file-modification-time sls) (file-modification-time so)))\n (printf \" Compiling ~a...~n\" sls)\n (compile-library sls)]\n [else\n (printf \" (up to date) ~a~n\" sls)])))\n\n;; NOTE: WPO step uses a fresh subprocess (see step [2/7] below), so per-jsh\n;; module .so files compiled here are NOT inputs to compile-whole-program —\n;; the subprocess recompiles them. We compile here only as a fast sanity check\n;; and so that `jsh.ss` (non-WPO direct-load) keeps working in dev mode.\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n ;; Compat layer\n (compile-jsh-module \"../compat/gambit\")\n (for-each compile-jsh-module '(\"ffi\"))\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n (for-each compile-jsh-module '(\"conditions\" \"ast\" \"registry\"))\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-macos \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss + WPO via subprocess...~n\"\n )\n;; WHY a subprocess: build-jsh-macos.ss imports (jerboa build), which\n;; transitively loads (jerboa core) into THIS scheme process. Once a library\n;; is loaded, compile-program/compile-whole-program won't re-emit its .so/.wpo\n;; — but compile-whole-program needs (jerboa core).wpo on disk to fuse it in.\n;; The fix: shell out to a fresh scheme that imports only (chezscheme), so all\n;; transitive libs get compiled freshly with WPO. Pattern matches\n;; ~/mine/jerboa/support/build-boot.ss + build-jerbuild.sh.\n;;\n;; Also: nuke all .so/.wpo from step [1/7] so the subprocess recompiles every\n;; library with generate-wpo-files #t. Without this, compile-imported-libraries\n;; in the subprocess sees up-to-date .so files and skips them — leaving us\n;; without .wpo files for compile-whole-program to inline.\n(printf \" Clearing .so/.wpo from step [1/7] so subprocess recompiles with WPO...~n\")\n(for-each\n (lambda (dir)\n (when (and (string? dir) (file-directory? dir))\n (system (format \"find '~a' -type f \\\\( -name '*.so' -o -name '*.wpo' \\\\) -delete\"\n dir))))\n (list \"src\" jerboa-dir\n (if has-jerboa-ssh? ssh-stage #f)\n (if has-jerboa-fuse? vault-stage #f)\n awk-stage sed-stage\n (if enable-aws? aws-stage #f)\n coreutils-stage))\n(let* ([libdir-pair->str\n (lambda (e)\n (cond [(pair? e) (format \"~a::~a\" (car e) (cdr e))]\n [else e]))]\n [extra-libdirs\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage)))]\n [all-libdirs (append extra-libdirs (library-directories))]\n [libdirs-str\n (apply string-append\n (let loop ([lst (map libdir-pair->str all-libdirs)] [acc '()])\n (cond [(null? lst) (reverse acc)]\n [(null? acc) (loop (cdr lst) (list (car lst)))]\n [else (loop (cdr lst)\n (cons (car lst) (cons \":\" acc)))])))]\n [scheme-cmd (or (getenv \"SCHEME\")\n (format \"~a/.chez/bin/scheme\" jerboa-dir-base))]\n [build-boot-script (format \"~a/support/build-boot.ss\" jerboa-dir-base)])\n (unless (file-exists? build-boot-script)\n (fprintf (current-error-port)\n \"FATAL: build-boot.ss not found at ~a~n\" build-boot-script)\n (exit 1))\n (let* ([cmd (format \"~a -q --libdirs '~a' --script ~a jsh-generated.ss jsh-generated.wp.so\"\n scheme-cmd libdirs-str build-boot-script)]\n [rc (system cmd)])\n (unless (zero? rc)\n (fprintf (current-error-port)\n \"FATAL: WPO subprocess failed (rc=~a)~ncmd: ~a~n\" rc cmd)\n (exit 1))))\n\n;; Verify jsh-generated.wp.so was created by subprocess\n(unless (file-exists? \"jsh-generated.wp.so\")\n (fprintf (current-error-port) \"FATAL: jsh-generated.wp.so was not created~n\")\n (exit 1))\n\n;; ========== Step 3: (subsumed by step 2 subprocess) ==========\n(define program-so \"jsh-generated.wp.so\")\n\n;; Step 3.5 (precompile-boot-jerboa-modules!) + Step 4 (make-boot-file\n;; \"jsh.boot\") are subsumed by WPO above: every imported library is inlined\n;; into jsh-generated.wp.so by compile-whole-program.\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-macos-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define gcc \"cc\")\n\n(define (resolve-llvm-ar)\n (cond\n [(let ([p (getenv \"LLVM_AR\")])\n (and p (not (string=? p \"\")) p))]\n [(file-exists? \"/opt/homebrew/opt/llvm/bin/llvm-ar\")\n \"/opt/homebrew/opt/llvm/bin/llvm-ar\"]\n [else #f]))\n\n(define llvm-ar (resolve-llvm-ar))\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" home-dir \"=~\"))\n\n;; Helper: write file as C byte array directly to output port (avoids O(n^2) string-append)\n(define (write-c-array filepath varname out)\n (let* ([bv (call-with-port (open-file-input-port filepath) get-bytevector-all)]\n [len (bytevector-length bv)]\n [hex \"0123456789abcdef\"])\n (fprintf out \"static const unsigned char ~a[] = {~n\" varname)\n (do ([i 0 (+ i 1)])\n ((= i len))\n (when (and (> i 0) (= (mod i 16) 0)) (display \",\\n\" out))\n (when (and (> i 0) (not (= (mod i 16) 0))) (display \",\" out))\n (display \"0x\" out)\n (let ([b (bytevector-u8-ref bv i)])\n (display (string-ref hex (fxsrl b 4)) out)\n (display (string-ref hex (fxand b 15)) out)))\n (fprintf out \"~n};~nstatic const unsigned int ~a_len = ~a;~n\" varname len)))\n\n;; Generate static_boot.c\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n (write-c-array petite-boot-path \"petite_boot\" out) (newline out)\n (write-c-array scheme-boot-path \"scheme_boot\" out) (newline out)\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n;; FFI symbol whitelist — auto-generated from ffi-shim.c plus a small set of\n;; non-ffi_ helpers (Cage/Landlock wrappers, Rust-native shims).\n(define ffi-shim-symbols\n (append (read-symbol-list \"ffi-shim-symbols.list\")\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\")))\n\n(define native-symbols\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\"\n \"jerboa_x509_cert_fingerprint\"\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n\n(define native-int-symbols\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n\n;; High-level jsh_* coreutils commands (from Rust jerboa-coreutils).\n;; On macOS these are stubbed out — the Rust coreutils lib is not yet built.\n(define jsh-coreutils-commands\n '(\"jsh_arch\" \"jsh_b2sum\" \"jsh_base32\" \"jsh_base64\" \"jsh_basename\" \"jsh_basenc\"\n \"jsh_cat\" \"jsh_chgrp\" \"jsh_chmod\" \"jsh_chown\" \"jsh_chroot\" \"jsh_cksum\"\n \"jsh_comm\" \"jsh_cp\" \"jsh_csplit\" \"jsh_cu_realpath\" \"jsh_cut\" \"jsh_date\"\n \"jsh_dd\" \"jsh_df\" \"jsh_dir\" \"jsh_dircolors\" \"jsh_dirname\" \"jsh_du\"\n \"jsh_echo\" \"jsh_env\" \"jsh_expand\" \"jsh_expr\" \"jsh_factor\" \"jsh_fmt\"\n \"jsh_fold\" \"jsh_grep\" \"jsh_groups\" \"jsh_head\" \"jsh_hostid\" \"jsh_hostname\"\n \"jsh_id\" \"jsh_install\" \"jsh_join\" \"jsh_kill\" \"jsh_link\" \"jsh_ln\"\n \"jsh_logname\" \"jsh_ls\" \"jsh_md5sum\" \"jsh_mkdir\" \"jsh_mkfifo\" \"jsh_mknod\"\n \"jsh_mktemp\" \"jsh_mv\" \"jsh_nice\" \"jsh_nl\" \"jsh_nohup\" \"jsh_nproc\"\n \"jsh_numfmt\" \"jsh_od\" \"jsh_paste\" \"jsh_pathchk\" \"jsh_pinky\" \"jsh_pr\"\n \"jsh_printenv\" \"jsh_printf\" \"jsh_ptx\" \"jsh_pwd\" \"jsh_readlink\" \"jsh_rm\"\n \"jsh_rmdir\" \"jsh_seq\" \"jsh_sha1sum\" \"jsh_sha224sum\" \"jsh_sha256sum\"\n \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_shred\" \"jsh_shuf\" \"jsh_sleep\"\n \"jsh_sort\" \"jsh_split\" \"jsh_stat\" \"jsh_stty\" \"jsh_sum\" \"jsh_sync\"\n \"jsh_tac\" \"jsh_tail\" \"jsh_tee\" \"jsh_test\" \"jsh_timeout\" \"jsh_touch\"\n \"jsh_tr\" \"jsh_truncate\" \"jsh_tsort\" \"jsh_tty\" \"jsh_uname\" \"jsh_unexpand\"\n \"jsh_uniq\" \"jsh_unlink\" \"jsh_uptime\" \"jsh_users\" \"jsh_vdir\" \"jsh_wc\"\n \"jsh_who\" \"jsh_whoami\" \"jsh_yes\"))\n\n(define coreutils-symbols\n '(\"coreutils_chmod\" \"coreutils_lstat_mode\" \"coreutils_stat_isdir\"\n \"coreutils_chown\" \"coreutils_lchown\"\n \"coreutils_getpwnam_uid\" \"coreutils_getgrnam_gid\"\n \"coreutils_stat_call\" \"coreutils_stat_get\"\n \"coreutils_uid_to_name\" \"coreutils_gid_to_name\"\n \"coreutils_du_stat\" \"coreutils_statvfs\" \"coreutils_statvfs_get\"\n \"coreutils_test_access\" \"coreutils_test_stat\"\n \"coreutils_ls_lstat\" \"coreutils_ls_stat_get\" \"coreutils_ls_readlink\"\n \"coreutils_isatty\" \"coreutils_time_format\"\n \"coreutils_terminal_width\" \"coreutils_terminal_height\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_cp_lstat\" \"coreutils_cp_stat_get\" \"coreutils_cp_readlink\"\n \"coreutils_symlink\" \"coreutils_link\" \"coreutils_utime\"\n \"coreutils_mkdir\" \"coreutils_lstat_type\"\n \"coreutils_unlink\" \"coreutils_rmdir\" \"coreutils_access_w\"\n \"coreutils_rename\" \"coreutils_stat_get_mode\"\n \"coreutils_stat_atime\" \"coreutils_stat_mtime\"\n \"coreutils_file_size\" \"coreutils_fsync\"\n \"coreutils_chgrp_chown\" \"coreutils_chgrp_lchown\"\n \"coreutils_mkstemp\" \"coreutils_mkstemp_get_path\"\n \"coreutils_mkdtemp\" \"coreutils_readlink\" \"coreutils_realpath\"\n \"coreutils_stat_size\" \"coreutils_fsync_path\"))\n\n(define ssh-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n\n;; jerboa_ssh_crypto.c symbols (used by ssh/transport sub-library)\n(define ssh-crypto-symbols\n '(\"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\" \"jerboa_ssh_sha512\"\n \"jerboa_ssh_hmac_sha256\" \"jerboa_ssh_hmac_sha512\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_aes256_ctr_init\" \"jerboa_ssh_aes256_ctr_process\" \"jerboa_ssh_aes256_ctr_free\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_ed25519_sign\" \"jerboa_ssh_ed25519_derive_pubkey\"\n \"jerboa_ssh_tcp_connect\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_write\"\n \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_set_nodelay\"))\n\n;; OpenSSL symbols called directly as (foreign-procedure \"NAME\" ...) by vault/crypto.sls.\n;; The vault's load-shared-object patch leaves _loaded=#t (guard returns #t when no exception),\n;; so these foreign-procedure calls ARE evaluated. Since we link -lssl -lcrypto, we register\n;; the actual function pointers here so Chez can find them.\n(define openssl-ffi-symbols\n '(\"RAND_bytes\"\n \"EVP_sha256\"\n \"PKCS5_PBKDF2_HMAC\"\n \"EVP_CIPHER_CTX_new\"\n \"EVP_CIPHER_CTX_free\"\n \"EVP_aes_256_gcm\"\n \"EVP_EncryptInit_ex\"\n \"EVP_EncryptUpdate\"\n \"EVP_EncryptFinal_ex\"\n \"EVP_CIPHER_CTX_ctrl\"\n \"EVP_DecryptInit_ex\"\n \"EVP_DecryptUpdate\"\n \"EVP_DecryptFinal_ex\"))\n\n;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n(define vault-fuse-symbols\n '(;; Secure memory\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n ;; Process tree\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n ;; FUSE device + mount\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n\n;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n;; jerboa_aead_seal, jerboa_aead_open — all in libjerboa_native (ring). No libcrypto needed.\n;; POSIX symbols needed by vault code (pread/pwrite for file I/O, fsync, uid/gid)\n(define vault-crypto-symbols\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\"))\n\n;; Generate jsh_main_macos.c\n(define program-c (format \"~a/jsh_main_macos.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <mach-o/dyld.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include <dlfcn.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n\n ;; Embed program .so\n (write-c-array program-so \"jsh_program_data\" out)\n (newline out)\n\n ;; Declare static_boot_init\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n\n ;; Declare FFI symbols\n (display \"/* FFI symbols from ffi-shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n\n ;; Rust native symbols\n (when has-native-lib?\n (display \"\\n#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n native-symbols)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n native-int-symbols)\n (display \"#endif\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n\n ;; Coreutils FFI\n (display \"\\n/* FFI symbols from libcoreutils.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n coreutils-symbols)\n\n ;; High-level jsh_* coreutils commands (from Rust libjsh_coreutils.a)\n (display \"\\n/* jsh_* coreutils commands */\\n\" out)\n (if has-rust-coreutils?\n (begin\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n jsh-coreutils-commands))\n (begin\n (display \"/* Stubs — Rust coreutils not built */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"int ~a(int ac, const char **av) { return 127; }\\n\" name))\n jsh-coreutils-commands)\n (display \"void jsh_coreutils_init(int a, char **b) { }\\n\" out)))\n\n ;; jerboa-ssh\n (display \"\\n/* FFI symbols from jerboa_ssh_shim.c + jerboa_ssh_crypto.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n (append ssh-symbols ssh-crypto-symbols))\n\n ;; OpenSSL symbols — called directly as (foreign-procedure \"NAME\" ...) by vault/crypto.sls.\n ;; Linked via -lssl -lcrypto so the symbols are in the binary; we just need to register them.\n (display \"\\n/* OpenSSL symbols used by vault/crypto.sls */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n openssl-ffi-symbols)\n\n ;; jerboa-fuse vault (crypto symbols now from libjerboa_native via ring)\n (display \"/* FFI symbols for vault (from ffi-shim.c + libjerboa_native) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n vault-fuse-symbols)\n (newline out)\n\n ;; POSIX wrappers\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n\n ;; macOS errno compatibility — __errno_location doesn't exist on macOS\n (display \"/* macOS errno compatibility */\\n\" out)\n (display \"static int *macos_errno_location(void) { return &errno; }\\n\\n\" out)\n\n ;; Stubs for symbols not available on macOS\n ;; (regex extended, epoll, inotify, landlock, seccomp)\n (display \"/* Stubs for Linux-only / missing native symbols */\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n ;; Regex stubs only when libjerboa_native.a is absent — it provides real implementations\n (unless has-native-lib?\n (display \"void *jerboa_regex_compile_ex(const char *p, int f) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_find_at(void *r, const char *s, int o, int *ms, int *me) { return 0; }\\n\" out)\n (display \"char *jerboa_regex_captures(void *r, const char *s, int n) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_group_count(void *r) { return 0; }\\n\" out))\n (display \"int jerboa_epoll_create(void) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_ctl(int e, int o, int f, int ev) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_wait(int e, void *ev, int m, int t) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_close(int e) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_init(void) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_add_watch(int f, const char *p, int m) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_rm_watch(int f, int w) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_read(int f, void *b, int s) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_close(int f) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_create_ruleset(void) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_path_rule(int r, const char *p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_net_rule(int r, int p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_enforce(int r) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_available(void) { return 0; }\\n\" out)\n (display \"int jerboa_seccomp_lock(void) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_lock_strict(void) { return -1; }\\n\\n\" out)\n\n ;; htons/htonl are macros on macOS and cannot be used as function pointers directly.\n ;; Emit thin wrappers so Sforeign_symbol can register them.\n (display \"static unsigned short jsh_htons(unsigned short x) { return htons(x); }\\n\" out)\n (display \"static unsigned int jsh_htonl(unsigned int x) { return htonl(x); }\\n\\n\" out)\n\n ;; register_ffi_symbols\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Rust native\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append native-symbols native-int-symbols))\n (display \"#endif\\n\" out))\n ;; POSIX\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"readlink\" \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"\n ;; vault blockstore\n \"flock\" \"pread\" \"pwrite\" \"fsync\"\n ;; top builtin\n \"setpriority\"))\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\" \"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; macOS: __errno_location (Linux glibc) and __error (BSD) → our wrapper\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)macos_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)macos_errno_location);\\n\" out)\n ;; Register stub symbols for Linux-only / missing native functionality\n ;; Note: jerboa_regex_*_ex symbols are omitted here — when has-native-lib? they\n ;; are real symbols registered above via native-symbols; without it they are\n ;; declared as stubs in the definitions section above register_ffi_symbols.\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n ;; When native lib absent, also register the regex_ex stubs\n (unless has-native-lib?\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\")))\n ;; coreutils\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n coreutils-symbols)\n ;; jsh_* coreutils commands (stubs)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n jsh-coreutils-commands)\n (fprintf out \" Sforeign_symbol(\\\"jsh_coreutils_init\\\", (void*)jsh_coreutils_init);\\n\")\n ;; jerboa-ssh\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append ssh-symbols ssh-crypto-symbols))\n ;; OpenSSL — vault/crypto.sls calls these as foreign-procedure\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n openssl-ffi-symbols)\n ;; jerboa-fuse vault\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-fuse-symbols)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-crypto-symbols)\n ;; Sockets — htons/htonl are macros on macOS, use wrappers\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"))\n (display \" Sforeign_symbol(\\\"htons\\\", (void*)jsh_htons);\\n\" out)\n (display \" Sforeign_symbol(\\\"htonl\\\", (void*)jsh_htonl);\\n\" out)\n (display \"}\\n\\n\" out)\n\n ;; Custom main — macOS\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; macOS: _NSGetExecutablePath for exe path\n (display \" /* Resolve exe path via _NSGetExecutablePath (macOS) */\\n\" out)\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" uint32_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (_NSGetExecutablePath(exe_buf, &exe_len) == 0) {\\n\" out)\n (display \" char resolved[4096];\\n\" out)\n (display \" if (realpath(exe_buf, resolved))\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", resolved, 1);\\n\" out)\n (display \" else\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; macOS: no memfd_create — use tmpfile.\n (display \" /* macOS: extract program .so to tmpfile */\\n\" out)\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-program-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program_data, 1, jsh_program_data_len, fp) != jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"fwrite tmpfile\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C ==========\n\n(printf \"[6/7] Compiling C with cc (clang)...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-macos \"Command failed\" cmd)))\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_macos.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_macos.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; landlock-shim.c — Landlock is Linux-only; always use stub on macOS\n;; Note: ffi_landlock_abi_version and ffi_landlock_sandbox are already defined\n;; in ffi-shim.c (with macOS stubs), so we only emit the ffi_landlock_create/add/enforce\n;; and jerboa_landlock_* symbols here.\n(begin\n (printf \" Landlock is Linux-only, generating stub for macOS~n\")\n (system (format \"echo 'int ffi_landlock_create_ruleset(void) { return -1; } int ffi_landlock_add_path_rule(int a, const char *b, int c) { return -1; } int ffi_landlock_add_net_rule(int a, int b, int c) { return -1; } int ffi_landlock_enforce(int a) { return -1; } int jerboa_landlock_abi_version(void) { return -1; } int jerboa_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int jerboa_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, unsigned long long p) { return -1; }' | ~a -c -x c ~a -o '~a/landlock-shim.o' -\"\n gcc harden-cflags build-dir)))\n\n;; coreutils FFI shim\n(if (file-exists? coreutils-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir coreutils-shim))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))))\n\n;; embed-crypto.c (ChaCha20-Poly1305 AEAD for embedded file encryption)\n;; When libjerboa_native.a is present it already exports embed_pbkdf2_sha256,\n;; embed_encrypt, embed_decrypt, embed_random_bytes. Compiling embed-crypto.c\n;; would produce duplicate strong symbols that macOS ld rejects. Use an empty\n;; stub when the Rust native lib is available; compile the full C source otherwise.\n(let ([embed-crypto-src \"embed-crypto.c\"])\n (if has-native-lib?\n (begin\n (printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir)))\n (if (file-exists? embed-crypto-src)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/embed-crypto.o' '~a' -Wall\"\n gcc harden-cflags build-dir embed-crypto-src))\n (begin\n (printf \" Warning: embed-crypto.c not found~n\")\n (system (format \"echo 'int embed_pbkdf2_sha256(void){return -1;} int embed_encrypt(void){return -1;} int embed_decrypt(void){return -1;} int embed_random_bytes(void){return -1;}' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))))))\n\n;; jerboa-ssh shim\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Use standalone ed25519 backend (Rust libjerboa_native provides the symbols)\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c (SSH transport crypto + TCP — requires OpenSSL)\n (let ([crypto-src (dep-file \"jerboa-ssh\" \"jerboa_ssh_crypto.c\")])\n (if (file-exists? crypto-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jerboa-ssh-crypto.o' '~a' -Wall\"\n gcc harden-cflags openssl-include-dir build-dir crypto-src))\n (begin\n (printf \" Warning: jerboa_ssh_crypto.c not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))))))\n (begin\n (printf \" Warning: jerboa-ssh shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking ~a binary...~n\" output-name)\n\n;; When both Rust archives are present, merge them into one to deduplicate Rust\n;; runtime symbols (rust_eh_personality, std::panicking, etc.) that appear as\n;; strong (T/S) symbols in both libjerboa_native.a and libjsh_coreutils.a.\n;;\n;; Strategy:\n;; 1. Extract native objects directly into merge-dir (strong originals kept).\n;; 2. Extract coreutils objects into a subdirectory, then use llvm-objcopy to\n;; weaken any symbol that is strongly defined in BOTH archives — so the\n;; linker picks the native copy and ignores the weak coreutils duplicate.\n;; 3. Copy weakened coreutils objects with a \"cu-\" prefix (avoiding name\n;; collisions with native objects) and re-archive everything.\n;; Python script to make duplicate global symbols local (clear N_EXT) in Mach-O .o files.\n;; llvm-objcopy --weaken-symbol doesn't work on macOS Mach-O; direct byte patching does.\n(define rust-dedup-py\n (let ([py-path (format \"~a/rust_dedup.py\" build-dir)])\n (call-with-output-file py-path\n (lambda (out)\n (display\n (string-append\n \"import sys, struct\\n\"\n \"filename = sys.argv[1]\\n\"\n \"target_syms = set(sys.argv[2:])\\n\"\n \"with open(filename, 'r+b') as f: data = bytearray(f.read())\\n\"\n \"if len(data) < 32: sys.exit(0)\\n\"\n \"magic = struct.unpack_from('<I', data, 0)[0]\\n\"\n \"if magic != 0xFEEDFACF: sys.exit(0) # not 64-bit MachO\\n\"\n \"ncmds = struct.unpack_from('<I', data, 16)[0]\\n\"\n \"cmdoff, symoff, stroff, nsyms = 32, None, None, 0\\n\"\n \"for _ in range(ncmds):\\n\"\n \" cmd, sz = struct.unpack_from('<II', data, cmdoff)\\n\"\n \" if cmd == 2:\\n\"\n \" symoff, nsyms, stroff, _ = struct.unpack_from('<IIII', data, cmdoff+8)\\n\"\n \" break\\n\"\n \" cmdoff += sz\\n\"\n \"if symoff is None: sys.exit(0)\\n\"\n \"N_EXT = 0x01\\n\"\n \"for i in range(nsyms):\\n\"\n \" off = symoff + i * 16\\n\"\n \" n_strx = struct.unpack_from('<I', data, off)[0]\\n\"\n \" n_type = data[off + 4]\\n\"\n \" if n_type & N_EXT:\\n\"\n \" nm = data[stroff+n_strx : data.index(b'\\\\x00', stroff+n_strx)].decode('ascii','replace')\\n\"\n \" if nm in target_syms:\\n\"\n \" data[off + 4] = n_type & ~N_EXT\\n\"\n \"with open(filename, 'wb') as f: f.write(data)\\n\")\n out)))\n py-path))\n\n(define combined-rust-lib\n (and has-native-lib? has-rust-coreutils?\n (let* ([merge-dir (format \"~a/rust-merge\" build-dir)]\n [cu-dir (format \"~a/cu\" merge-dir)]\n [combined (format \"~a/librust_combined.a\" build-dir)])\n (unless llvm-ar\n (error 'build-jsh-macos\n \"LLVM ar is required to read Rust archives on macOS; install llvm or set LLVM_AR\"\n rust-coreutils-lib-path))\n (run-cmd (format \"rm -rf '~a' && mkdir -p '~a' '~a'\" merge-dir merge-dir cu-dir))\n ;; Extract native objects into merge-dir\n (run-cmd (format \"cd '~a' && '~a' x '~a'\" merge-dir llvm-ar native-lib-path))\n ;; Extract coreutils objects into cu-dir\n (run-cmd (format \"cd '~a' && '~a' x '~a'\" cu-dir llvm-ar rust-coreutils-lib-path))\n ;; Find symbols defined (global T/S) in BOTH archives; clear N_EXT in the\n ;; coreutils objects to make them local — linker picks native's definitions.\n ;; Write a shell script to avoid nested-quote hell with bash -c '...awk...'\n (let ([sh-path (format \"~a/dedup.sh\" build-dir)])\n (call-with-output-file sh-path\n (lambda (out)\n (display \"#!/bin/bash\\nset -e\\n\" out)\n (display (format \"NAT=$(nm '~a' 2>/dev/null | awk '/ [TS] /{print $NF}' | sort -u)\\n\"\n native-lib-path) out)\n (display (format \"CU=$(nm '~a' 2>/dev/null | awk '/ [TS] /{print $NF}' | sort -u)\\n\"\n rust-coreutils-lib-path) out)\n (display \"DUPES=$(comm -12 <(echo \\\"$NAT\\\") <(echo \\\"$CU\\\"))\\n\" out)\n (display \"[ -z \\\"$DUPES\\\" ] && exit 0\\n\" out)\n (display (format \"for f in '~a'/*.o; do python3 '~a' \\\"$f\\\" $DUPES; done\\n\"\n cu-dir rust-dedup-py) out)))\n (run-cmd (format \"bash '~a'\" sh-path)))\n ;; Copy patched coreutils objects with \"cu-\" prefix to avoid name conflicts\n (run-cmd (format \"for f in '~a'/*.o; do cp \\\"$f\\\" '~a/cu-'\\\"$(basename $f)\\\"; done\"\n cu-dir merge-dir))\n ;; Build combined archive from all objects\n (run-cmd (format \"'~a' rcs '~a' '~a'/*.o\" llvm-ar combined merge-dir))\n combined)))\n\n;; macOS does not support fully static binaries — link dynamically against system libs.\n(let* ([objs (format \"~a/jsh_main_macos.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/coreutils-ffi.o ~a/landlock-shim.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o\"\n build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir)]\n ;; Use combined archive when both Rust libs present, else fall back individually\n [native-flag (cond [combined-rust-lib (format \" ~a\" combined-rust-lib)]\n [has-native-lib? (format \" ~a\" native-lib-path)]\n [else \"\"])]\n [coreutils-flag (if combined-rust-lib \"\" ; already in combined\n (if has-rust-coreutils? (format \" ~a\" rust-coreutils-lib-path) \"\"))]\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n [cxx-libs (if has-native-lib? \" -lc++\" \"\")]\n [link-libs (format \"-L~a -L~a -L/opt/homebrew/lib -L/usr/local/lib -lssl -lcrypto -lkernel -llz4 -lz -lm -liconv -lncurses -lutil\"\n chez-ta6fb openssl-lib-dir)]\n [link-cmd (format \"~a -o ~a ~a~a~a~a ~a\"\n gcc output-name objs native-flag coreutils-flag\n cxx-libs link-libs)])\n (printf \" ~a~n\" link-cmd)\n (run-cmd link-cmd))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? output-name)\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port output-name))])\n (run-cmd (format \"strip ~a\" output-name))\n (let ([post-size (file-length (open-file-input-port output-name))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n ;; Compute SHA-256 integrity hash\n (printf \"[harden] Computing integrity hash...~n\")\n ;; macOS uses shasum -a 256; FreeBSD uses sha256 -q; Linux uses sha256sum\n (system (format \"shasum -a 256 ~a | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt 2>/dev/null || sha256sum ~a | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\"\n output-name output-name))\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port (string-append output-name \".sha256\")\n (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote ~a.sha256 (32 bytes)~n\" output-name)))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(when enable-aws? (system (format \"rm -rf '~a'\" aws-stage)))\n(system (format \"rm -rf '~a'\" awk-stage))\n(system (format \"rm -rf '~a'\" sed-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Binary created: ~a~n~n\" output-name)\n(system (format \"ls -lh ~a\" output-name))\n(printf \"~n\")\n(system (format \"file ~a\" output-name))\n(printf \"~n\")\n(system (format \"otool -L ~a 2>/dev/null || true\" output-name))\n(printf \"~nTest: ./~a -c 'echo Hello from jsh on macOS'~n\" output-name)\n"} {"text":";; FILE: jerboa-shell/signals.ss\n;;; signals.ss — Signal handling and traps for gsh\n\n(export #t)\n(import :std/sugar\n :std/format\n :std/sort\n (only (compat gambit) let/cc)\n :std/os/signal\n :jsh/ffi\n :jsh/util)\n\n;;; --- Trap table ---\n;; Maps signal names to actions:\n;; string -> command to execute\n;; 'ignore -> ignore the signal\n;; 'default -> restore default behavior\n\n(defstruct trap-entry (signal action) transparent: #t)\n\n;; Global trap table (managed by the shell environment)\n(def *trap-table* (make-hash-table))\n\n;; Well-known signal name -> number mapping\n(def *signal-names*\n (hash\n (\"HUP\" SIGHUP)\n (\"INT\" SIGINT)\n (\"QUIT\" SIGQUIT)\n (\"ILL\" SIGILL)\n (\"TRAP\" SIGTRAP)\n (\"ABRT\" SIGABRT)\n (\"FPE\" SIGFPE)\n (\"KILL\" SIGKILL)\n (\"SEGV\" SIGSEGV)\n (\"PIPE\" SIGPIPE)\n (\"ALRM\" SIGALRM)\n (\"TERM\" SIGTERM)\n (\"USR1\" SIGUSR1)\n (\"USR2\" SIGUSR2)\n (\"CHLD\" SIGCHLD)\n (\"CONT\" SIGCONT)\n (\"STOP\" SIGSTOP)\n (\"TSTP\" SIGTSTP)\n (\"TTIN\" SIGTTIN)\n (\"TTOU\" SIGTTOU)\n (\"WINCH\" SIGWINCH)\n (\"URG\" SIGURG)\n (\"IO\" SIGIO)\n (\"XCPU\" SIGXCPU)\n (\"XFSZ\" SIGXFSZ)\n (\"VTALRM\" SIGVTALRM)\n (\"PROF\" SIGPROF)\n (\"SYS\" SIGSYS)))\n\n;; Pseudo-signals (not real OS signals)\n(def *pseudo-signals* '(\"EXIT\" \"DEBUG\" \"RETURN\" \"ERR\"))\n\n;; Reverse mapping: signal number -> short name\n(def *signal-number-to-name* (make-hash-table))\n(hash-for-each (lambda (name num) (hash-put! *signal-number-to-name* num name)) *signal-names*)\n\n;; Normalize a signal argument to canonical short name (e.g. \"INT\", \"EXIT\")\n;; Handles: SIGINT -> INT, INT -> INT, 2 -> INT, 0 -> EXIT, etc.\n(def (normalize-signal-arg arg)\n (let ((uarg (string-upcase arg)))\n ;; Strip SIG prefix\n (let ((stripped (if (and (> (string-length uarg) 3)\n (string=? (substring uarg 0 3) \"SIG\"))\n (substring uarg 3 (string-length uarg))\n uarg)))\n ;; Check if it's a number\n (let ((num (string->number stripped)))\n (cond\n ;; Signal number: 0 = EXIT, others look up\n ((and num (= num 0)) \"EXIT\")\n ((and num (hash-get *signal-number-to-name* num))\n => (lambda (name) name))\n ;; Valid signal number but no name in table — return as-is\n ((and num (integer? num) (> num 0) (<= num 64))\n (number->string num))\n ;; Known signal name\n ((hash-get *signal-names* stripped) stripped)\n ;; Pseudo-signal\n ((member stripped *pseudo-signals*) stripped)\n ;; Unknown\n (else #f))))))\n\n;; Get canonical display name for trap -p output\n;; Pseudo signals: EXIT, DEBUG, RETURN, ERR (no SIG prefix)\n;; Real signals: SIGHUP, SIGINT, SIGTERM, etc.\n(def (signal-display-name short-name)\n (if (member short-name *pseudo-signals*)\n short-name\n (string-append \"SIG\" short-name)))\n\n;; Convert signal name to number (or #f for pseudo/unknown)\n(def (signal-name->number name)\n (let ((uname (string-upcase name)))\n ;; Strip SIG prefix if present\n (let ((stripped (if (and (> (string-length uname) 3)\n (string=? (substring uname 0 3) \"SIG\"))\n (substring uname 3 (string-length uname))\n uname)))\n (hash-get *signal-names* stripped))))\n\n;; Human-readable signal descriptions (matching strsignal/bash output)\n(def *signal-descriptions*\n (hash\n (\"HUP\" \"Hangup\") (\"INT\" \"Interrupt\") (\"QUIT\" \"Quit\")\n (\"ILL\" \"Illegal instruction\") (\"TRAP\" \"Trace/breakpoint trap\")\n (\"ABRT\" \"Aborted\") (\"FPE\" \"Floating point exception\")\n (\"KILL\" \"Killed\") (\"SEGV\" \"Segmentation fault\")\n (\"PIPE\" \"Broken pipe\") (\"ALRM\" \"Alarm clock\") (\"TERM\" \"Terminated\")\n (\"USR1\" \"User defined signal 1\") (\"USR2\" \"User defined signal 2\")\n (\"CHLD\" \"Child exited\") (\"CONT\" \"Continued\") (\"STOP\" \"Stopped (signal)\")\n (\"TSTP\" \"Stopped\") (\"TTIN\" \"Stopped (tty input)\")\n (\"TTOU\" \"Stopped (tty output)\")))\n\n;; Get human-readable description for a signal number\n(def (signal-description signum)\n (let ((name (signal-number->name signum)))\n (and name (hash-get *signal-descriptions* name))))\n\n;;; --- Trap operations ---\n\n;; Set a trap for a signal\n;; signal-name should be a normalized short name (e.g. \"INT\", \"EXIT\")\n;; action: string (command), \"\" or 'ignore (ignore), 'default or #f (reset)\n(def (trap-set! signal-name action)\n (let ((uname (or (normalize-signal-arg signal-name)\n (string-upcase signal-name))))\n (cond\n ;; Reset to default\n ((or (eq? action 'default) (not action) (string=? (if (string? action) action \"\") \"-\"))\n (hash-remove! *trap-table* uname)\n (hash-remove! *flag-trapped-signals* uname)\n (let ((signum (signal-name->number uname)))\n (when (and signum (not (hash-get *initially-ignored-signals* signum)))\n (ffi-signal-set-default signum)\n (with-catch (lambda (e) #!void) ;; ignore error if no handler installed\n (lambda () (remove-signal-handler! signum))))))\n ;; Ignore signal\n ((or (eq? action 'ignore) (and (string? action) (string=? action \"\")))\n (hash-put! *trap-table* uname 'ignore)\n (hash-remove! *flag-trapped-signals* uname)\n (let ((signum (signal-name->number uname)))\n (when (and signum (not (hash-get *initially-ignored-signals* signum)))\n (ffi-signal-set-ignore signum))))\n ;; Set command handler\n ((string? action)\n (hash-put! *trap-table* uname action)\n ;; For real signals, install a C-level signal flag handler.\n ;; This is synchronous (flag set immediately on signal delivery),\n ;; unlike Jerboa's async signalfd-based add-signal-handler! which\n ;; has timing issues with signal delivery.\n ;; POSIX: signals that were SIG_IGN at startup cannot be trapped\n (let ((signum (signal-name->number uname)))\n (when (and signum (not (hash-get *initially-ignored-signals* signum)))\n ;; Remove any existing Jerboa handler first\n (with-catch (lambda (e) #!void)\n (lambda () (remove-signal-handler! signum)))\n ;; Install C-level flag handler (also unblocks the signal)\n (ffi-signal-flag-install signum)\n ;; Track which signals use flag-based handling\n (hash-put! *flag-trapped-signals* uname signum))))\n (else\n (error #f (format \"trap: invalid action: ~a\" action))))))\n\n;; Get the trap action for a signal\n(def (trap-get signal-name)\n (let ((uname (or (normalize-signal-arg signal-name)\n (string-upcase signal-name))))\n (hash-get *trap-table* uname)))\n\n;; List all traps as alist of (signal-name . action), sorted.\n;; Bash sorts EXIT/ERR/DEBUG/RETURN first, then by signal number.\n(def (trap-list)\n (sort (hash->list *trap-table*)\n (lambda (a b)\n (let ((na (signal-name->number (car a)))\n (nb (signal-name->number (car b))))\n (cond\n ;; Pseudo-signals (EXIT, ERR, etc.) have no number — sort first\n ((and (not na) nb) #t)\n ((and na (not nb)) #f)\n ((and (not na) (not nb)) (string<? (car a) (car b)))\n (else (< na nb)))))))\n\n;; Check if any signal command traps are registered (not 'ignore, not EXIT/ERR/DEBUG)\n(def (has-signal-traps?)\n (let/cc return\n (hash-for-each\n (lambda (name action)\n (when (and (string? action) (not (string=? action \"\"))\n ;; Only real signals, not pseudo-signals\n (signal-name->number name))\n (return #t)))\n *trap-table*)\n #f))\n\n;;; --- Pending signal queue ---\n\n(def *pending-signals* [])\n\n;; Signals using C-level flag handlers (maps signal-name -> signum)\n(def *flag-trapped-signals* (make-hash-table))\n\n;; Check and clear pending signals, return list of signal names.\n;; Checks both the Jerboa signalfd-based queue and C-level signal flags.\n(def (pending-signals!)\n ;; First, check C-level signal flags (synchronous, no timing issues)\n (hash-for-each\n (lambda (name signum)\n (when (= 1 (ffi-signal-flag-check signum))\n (set! *pending-signals* (cons name *pending-signals*))))\n *flag-trapped-signals*)\n ;; Return combined pending list\n (let ((pending *pending-signals*))\n (set! *pending-signals* [])\n (reverse pending)))\n\n;; Remove a specific signal from the pending queue.\n;; Used when a foreground child was killed by a signal — the shell should\n;; NOT run the trap for that signal (bash behavior).\n(def (clear-pending-signal! sig-name)\n (set! *pending-signals*\n (filter (lambda (s) (not (string=? s sig-name))) *pending-signals*)))\n\n;; Map a signal number to its short name (e.g. 2 -> \"INT\")\n(def (signal-number->name num)\n (hash-get *signal-number-to-name* num))\n\n;;; --- Initially-ignored signals (POSIX) ---\n;; Signals that were SIG_IGN when the shell started.\n;; Non-interactive shells must not override these (POSIX requirement).\n;; Populated by setup-noninteractive-signal-handlers!\n(def *initially-ignored-signals* (make-hash-table))\n\n;;; --- Default signal setup for interactive shell ---\n\n(def (setup-default-signal-handlers!)\n ;; SIGINT: interrupt current command\n (add-signal-handler! SIGINT\n (lambda ()\n (set! *pending-signals* (cons \"INT\" *pending-signals*))))\n ;; SIGQUIT: ignore in interactive mode\n (ffi-signal-set-ignore SIGQUIT)\n ;; SIGTERM: flag for exit\n (add-signal-handler! SIGTERM\n (lambda ()\n (set! *pending-signals* (cons \"TERM\" *pending-signals*))))\n ;; SIGTSTP: ignore for the shell itself (children get it)\n (ffi-signal-set-ignore SIGTSTP)\n (ffi-signal-set-ignore SIGTTIN)\n (ffi-signal-set-ignore SIGTTOU)\n ;; SIGPIPE: ignore (let write fail with error)\n (ffi-signal-set-ignore SIGPIPE)\n ;; SIGXFSZ: install flag handler so write fails instead of killing process,\n ;; and the signal is recorded for script termination (exit 153 = 128+25)\n (ffi-signal-flag-install SIGXFSZ)\n (hash-put! *flag-trapped-signals* \"XFSZ\" SIGXFSZ)\n ;; SIGWINCH: record for terminal resize\n (add-signal-handler! SIGWINCH\n (lambda ()\n (set! *pending-signals* (cons \"WINCH\" *pending-signals*))))\n ;; SIGCHLD: record for job status updates\n (add-signal-handler! SIGCHLD\n (lambda ()\n (set! *pending-signals* (cons \"CHLD\" *pending-signals*)))))\n\n;;; --- Signal setup for non-interactive shell (scripts, -c) ---\n\n(def (setup-noninteractive-signal-handlers!)\n ;; Record which signals were SIG_IGN at startup (before Gambit).\n ;; POSIX: non-interactive shells must not override inherited SIG_IGN.\n (for-each\n (lambda (signum)\n (when (= (ffi-signal-was-ignored signum) 1)\n (hash-put! *initially-ignored-signals* signum #t)\n ;; Restore SIG_IGN that Gambit's startup overrode\n (ffi-signal-set-ignore signum)))\n (list SIGINT SIGQUIT SIGTERM SIGHUP))\n ;; SIGINT: record for processing between commands\n ;; Without this, Gambit's default handler terminates the process\n ;; and EXIT traps never fire.\n (unless (hash-get *initially-ignored-signals* SIGINT)\n (add-signal-handler! SIGINT\n (lambda ()\n (set! *pending-signals* (cons \"INT\" *pending-signals*)))))\n ;; SIGTERM: record for processing\n (unless (hash-get *initially-ignored-signals* SIGTERM)\n (add-signal-handler! SIGTERM\n (lambda ()\n (set! *pending-signals* (cons \"TERM\" *pending-signals*)))))\n ;; SIGPIPE: ignore (always, regardless of initial state)\n (add-signal-handler! SIGPIPE (lambda () #!void))\n ;; SIGXFSZ: install flag handler for proper handling (exit 153)\n (ffi-signal-flag-install SIGXFSZ)\n (hash-put! *flag-trapped-signals* \"XFSZ\" SIGXFSZ))\n\n;;; --- Signal context for command execution ---\n\n;; Run a thunk with appropriate signal handling for foreground command execution\n(def (with-signal-context thunk)\n ;; Clear pending signals before running\n (set! *pending-signals* [])\n (thunk))\n\n;;; --- Utility ---\n\n;; List all known signal names\n(def (signal-name-list)\n (sort! (hash-keys *signal-names*) string<?))\n"} {"text":";; FILE: jerboa-shell/build-all.ss\n#!chezscheme\n;; Build driver: imports all modules to trigger Chez compilation.\n;; Generates .so + .wpo files for the platform-specific binary builds\n;; (build-jsh-{macos,musl,freebsd,android}.ss).\n(parameterize ([compile-imported-libraries #t]\n [generate-wpo-files #t]\n [optimize-level 3]\n [generate-inspector-information #f])\n (eval '(import\n (jsh ast) (jsh registry) (jsh macros) (jsh util)\n (jsh environment) (jsh lexer) (jsh arithmetic) (jsh glob)\n (jsh fuzzy) (jsh history) (jsh parser) (jsh functions)\n (jsh signals) (jsh expander) (jsh redirect) (jsh control)\n (jsh jobs) (jsh builtins) (jsh pipeline) (jsh executor)\n (jsh completion) (jsh prompt) (jsh lineedit) (jsh fzf)\n (jsh script) (jsh startup) (jsh main) (jsh stage)\n (jsh worm))\n (interaction-environment)))\n"} {"text":";; FILE: jerboa-shell/arithmetic.ss\n;;; arithmetic.ss — Shell arithmetic evaluation for gsh\n;;; Implements $(( )) arithmetic with full operator precedence\n\n(export arith-eval arith-tokenize arith-token? arith-token-type arith-token-value\n arith-state? arith-state-tokens arith-state-pos arith-state-env-get arith-state-env-set\n arith-state-suppress-effects arith-state-nounset?)\n(import :std/sugar\n :std/format)\n\n;;; --- Public interface ---\n\n;; Evaluate a shell arithmetic expression string\n;; env-get-fn: (lambda (name) value-string-or-#f)\n;; env-set-fn: (lambda (name value) void)\n;; nounset?: when #t, referencing undefined variables is an error\n;; Returns: integer result\n(def (arith-eval expr env-get-fn env-set-fn (nounset? #f))\n (let* ((tokens (arith-tokenize expr))\n (state (make-arith-state tokens 0 env-get-fn env-set-fn #f nounset?)))\n (if (null? tokens)\n 0\n (let ((result (parse-comma-expr state)))\n ;; Validate all tokens were consumed\n (when (< (arith-state-pos state) (length tokens))\n (error #f (format \"arithmetic: syntax error: unexpected token '~a'\"\n (arith-token-value (list-ref tokens (arith-state-pos state))))))\n result))))\n\n;;; --- Tokenizer ---\n\n;; Added suppress-effects for short-circuit evaluation, nounset? for set -u\n(defstruct arith-state (tokens pos env-get env-set suppress-effects nounset?) transparent: #t)\n\n;; Arith token types: 'number 'name 'op\n(defstruct arith-token (type value) transparent: #t)\n\n(def (arith-tokenize expr)\n (let loop ((i 0) (tokens []))\n (cond\n ((>= i (string-length expr))\n (reverse tokens))\n ;; Skip whitespace\n ((char-whitespace? (string-ref expr i))\n (loop (+ i 1) tokens))\n ;; Numbers: decimal, hex (0x), octal (0), binary (0b), base-N (N#val)\n ((char-numeric? (string-ref expr i))\n (let-values (((num end) (read-number expr i)))\n (loop end (cons (make-arith-token 'number num) tokens))))\n ;; Names (variable references) — may also start base-N constant\n ((or (char-alphabetic? (string-ref expr i))\n (char=? (string-ref expr i) #\\_))\n (let-values (((name end) (read-name expr i)))\n (loop end (cons (make-arith-token 'name name) tokens))))\n ;; # is not a valid arithmetic operator — reject it early to prevent\n ;; side-effects from being committed before the syntax error is detected\n ((char=? (string-ref expr i) #\\#)\n (error #f (format \"arithmetic: syntax error: unexpected token '#'\")))\n ;; Multi-char operators\n (else\n (let-values (((op end) (read-operator expr i)))\n (loop end (cons (make-arith-token 'op op) tokens)))))))\n\n(def (read-number expr i)\n (let ((len (string-length expr)))\n (cond\n ;; Hex: 0x or 0X\n ((and (< (+ i 1) len)\n (char=? (string-ref expr i) #\\0)\n (or (char=? (string-ref expr (+ i 1)) #\\x)\n (char=? (string-ref expr (+ i 1)) #\\X)))\n (let loop ((j (+ i 2)))\n (if (and (< j len) (hex-digit? (string-ref expr j)))\n (loop (+ j 1))\n ;; Check for trailing alphanumeric (invalid hex like 0x1X)\n (begin\n (when (and (< j len)\n (let ((ch (string-ref expr j)))\n (or (char-alphabetic? ch) (char=? ch #\\_))))\n (error #f (format \"arithmetic: invalid hex constant: ~a\"\n (substring expr i (let lp ((k j))\n (if (and (< k len)\n (let ((c (string-ref expr k)))\n (or (arith-alnum? c) (char=? c #\\_))))\n (lp (+ k 1)) k))))))\n (let ((num (string->number (substring expr (+ i 2) j) 16)))\n (if num (values num j)\n (error #f (format \"arithmetic: invalid hex constant: ~a\"\n (substring expr i j)))))))))\n ;; Binary: 0b or 0B\n ((and (< (+ i 1) len)\n (char=? (string-ref expr i) #\\0)\n (or (char=? (string-ref expr (+ i 1)) #\\b)\n (char=? (string-ref expr (+ i 1)) #\\B)))\n (let loop ((j (+ i 2)))\n (if (and (< j len) (or (char=? (string-ref expr j) #\\0)\n (char=? (string-ref expr j) #\\1)))\n (loop (+ j 1))\n (values (string->number (substring expr (+ i 2) j) 2) j))))\n ;; Octal: starts with 0 followed by digit\n ((and (char=? (string-ref expr i) #\\0)\n (< (+ i 1) len)\n (char-numeric? (string-ref expr (+ i 1))))\n ;; Read all consecutive digits first, then validate\n (let loop ((j (+ i 1)))\n (if (and (< j len) (char-numeric? (string-ref expr j)))\n (loop (+ j 1))\n ;; Check that all digits are valid octal (0-7)\n (let check ((k (+ i 1)))\n (if (< k j)\n (if (octal-digit? (string-ref expr k))\n (check (+ k 1))\n (error #f (format \"arithmetic: invalid octal constant: ~a\"\n (substring expr i j))))\n ;; Check for trailing # (invalid base-N with leading 0: 02#xxx)\n (if (and (< j len) (char=? (string-ref expr j) #\\#))\n (let ((end (let lp ((k (+ j 1)))\n (if (and (< k len) (arith-alnum? (string-ref expr k)))\n (lp (+ k 1)) k))))\n (error #f (format \"arithmetic: invalid number: ~a\"\n (substring expr i end))))\n (let ((num (string->number (substring expr (+ i 1) j) 8)))\n (if num (values num j)\n (error #f (format \"arithmetic: invalid octal constant: ~a\"\n (substring expr i j)))))))))))\n ;; Decimal — possibly followed by #val for base-N\n (else\n (let loop ((j i))\n (if (and (< j len) (char-numeric? (string-ref expr j)))\n (loop (+ j 1))\n (cond\n ;; Float literal: reject\n ((and (< j len) (char=? (string-ref expr j) #\\.))\n (error #f \"arithmetic: invalid number (float not supported)\"))\n ;; Check for base#value pattern\n ((and (< j len) (char=? (string-ref expr j) #\\#))\n (let ((base (string->number (substring expr i j))))\n (if (and base (>= base 2) (<= base 64))\n ;; Read the value part (alphanumeric + _ + @)\n (let vloop ((k (+ j 1)) (val 0))\n (if (and (< k len) (base-n-digit? (string-ref expr k) base))\n (vloop (+ k 1) (+ (* val base) (base-n-digit-value (string-ref expr k))))\n ;; Check for trailing invalid digit (e.g. 2#A)\n (if (and (< k len)\n (let ((ch (string-ref expr k)))\n (or (arith-alnum? ch) (char=? ch #\\_) (char=? ch #\\@))))\n (error #f (format \"arithmetic: invalid base ~a constant: ~a\"\n base (substring expr i (let lp ((kk k))\n (if (and (< kk len)\n (let ((c (string-ref expr kk)))\n (or (arith-alnum? c) (char=? c #\\_) (char=? c #\\@))))\n (lp (+ kk 1)) kk)))))\n (if (= k (+ j 1))\n ;; No digits at all after base# — error\n (error #f (format \"arithmetic: invalid constant: ~a\"\n (substring expr i (+ j 1))))\n (values val k)))))\n (error #f (format \"arithmetic: invalid base: ~a\"\n (substring expr i j))))))\n ;; Check for trailing alphabetic chars (e.g. 42x) — invalid constant\n ((and (< j len)\n (let ((ch (string-ref expr j)))\n (or (char-alphabetic? ch) (char=? ch #\\_))))\n ;; Read the full invalid token\n (let lp ((k j))\n (if (and (< k len)\n (let ((ch (string-ref expr k)))\n (or (arith-alnum? ch) (char=? ch #\\_))))\n (lp (+ k 1))\n (error #f (format \"arithmetic: invalid constant: ~a\"\n (substring expr i k))))))\n (else\n (values (string->number (substring expr i j)) j)))))))))\n\n(def (read-name expr i)\n (let ((len (string-length expr)))\n (let loop ((j i))\n (if (and (< j len)\n (let ((ch (string-ref expr j)))\n (or (char-alphabetic? ch) (char-numeric? ch) (char=? ch #\\_))))\n (loop (+ j 1))\n (values (substring expr i j) j)))))\n\n(def (read-operator expr i)\n (let ((len (string-length expr))\n (ch (string-ref expr i)))\n (cond\n ;; Three-char operators\n ((and (< (+ i 2) len)\n (string=? (substring expr i (+ i 3)) \"<<=\"))\n (values \"<<=\" (+ i 3)))\n ((and (< (+ i 2) len)\n (string=? (substring expr i (+ i 3)) \">>=\"))\n (values \">>=\" (+ i 3)))\n ;; Two-char operators\n ((< (+ i 1) len)\n (let ((two (substring expr i (+ i 2))))\n (cond\n ((member two '(\"==\" \"!=\" \"<=\" \">=\" \"&&\" \"||\" \"<<\" \">>\"\n \"+=\" \"-=\" \"*=\" \"/=\" \"%=\" \"&=\" \"^=\" \"|=\"\n \"++\" \"--\" \"**\"))\n (values two (+ i 2)))\n (else\n (values (string ch) (+ i 1))))))\n (else\n (values (string ch) (+ i 1))))))\n\n(def (arith-alnum? ch)\n (or (char-alphabetic? ch) (char-numeric? ch)))\n\n(def (hex-digit? ch)\n (or (char-numeric? ch)\n (and (char>=? ch #\\a) (char<=? ch #\\f))\n (and (char>=? ch #\\A) (char<=? ch #\\F))))\n\n(def (octal-digit? ch)\n (and (char>=? ch #\\0) (char<=? ch #\\7)))\n\n;; Check if a character is a valid digit for base N\n;; Bases 2-36: 0-9 a-z (case insensitive)\n;; Bases 37-62: 0-9 a-z A-Z\n;; Bases 63-64: 0-9 a-z A-Z _ @\n(def (base-n-digit? ch base)\n (let ((v (base-n-digit-value-raw ch)))\n (and v (< v base))))\n\n(def (base-n-digit-value ch)\n (or (base-n-digit-value-raw ch) 0))\n\n(def (base-n-digit-value-raw ch)\n (cond\n ((and (char>=? ch #\\0) (char<=? ch #\\9))\n (- (char->integer ch) (char->integer #\\0)))\n ((and (char>=? ch #\\a) (char<=? ch #\\z))\n (+ 10 (- (char->integer ch) (char->integer #\\a))))\n ((and (char>=? ch #\\A) (char<=? ch #\\Z))\n (+ 36 (- (char->integer ch) (char->integer #\\A))))\n ((char=? ch #\\@) 62)\n ((char=? ch #\\_) 63)\n (else #f)))\n\n;;; --- Recursive descent parser (operator precedence) ---\n\n(def (arith-peek state)\n (if (>= (arith-state-pos state) (length (arith-state-tokens state)))\n #f\n (list-ref (arith-state-tokens state) (arith-state-pos state))))\n\n(def (arith-advance! state)\n (set! (arith-state-pos state) (+ 1 (arith-state-pos state))))\n\n(def (arith-expect-op! state expected)\n (let ((tok (arith-peek state)))\n (if (and tok (eq? (arith-token-type tok) 'op)\n (string=? (arith-token-value tok) expected))\n (begin (arith-advance! state) #t)\n (error #f (format \"arithmetic: expected ~a\" expected)))))\n\n(def (arith-match-op? state op)\n (let ((tok (arith-peek state)))\n (and tok (eq? (arith-token-type tok) 'op)\n (string=? (arith-token-value tok) op))))\n\n(def (arith-consume-op! state op)\n (if (arith-match-op? state op)\n (begin (arith-advance! state) #t)\n #f))\n\n;; Get variable value as integer, with recursive name resolution\n;; If the value of a variable is another variable name, resolve it\n;; For dynamic arithmetic resolution: e=1+2; echo $((e+3)) → 6\n(def (arith-get-var state name)\n (let resolve ((name name) (depth 0))\n (if (> depth 10) 0 ;; prevent infinite loops\n (let ((val ((arith-state-env-get state) name)))\n (cond\n ((not val)\n ;; Check nounset — if nounset? is a procedure, call it (raises exception)\n ;; If it's #t, raise generic error. If #f, silently return 0.\n (let ((nu (arith-state-nounset? state)))\n (cond\n ((procedure? nu) (nu name))\n (nu (error #f (format \"arithmetic: ~a: unbound variable\" name)))))\n 0)\n (else\n (let ((trimmed (arith-string-trim val)))\n (if (string=? trimmed \"\") 0\n (let ((num (string->number trimmed)))\n (if num num\n ;; Try to resolve as an arithmetic expression\n ;; First check if it's a plain variable name\n (if (and (> (string-length trimmed) 0)\n (or (char-alphabetic? (string-ref trimmed 0))\n (char=? (string-ref trimmed 0) #\\_))\n (let check ((j 0))\n (or (>= j (string-length trimmed))\n (let ((ch (string-ref trimmed j)))\n (and (or (char-alphabetic? ch) (char-numeric? ch)\n (char=? ch #\\_))\n (check (+ j 1)))))))\n ;; Plain variable name — resolve through variable chain\n (resolve trimmed (+ depth 1))\n ;; Contains operators — evaluate as arithmetic expression\n (arith-eval trimmed\n (arith-state-env-get state)\n (arith-state-env-set state)))))))))))))\n\n(def (arith-string-trim s)\n (let* ((len (string-length s))\n (start (let loop ((i 0))\n (if (and (< i len) (char-whitespace? (string-ref s i)))\n (loop (+ i 1)) i)))\n (end (let loop ((i len))\n (if (and (> i start) (char-whitespace? (string-ref s (- i 1))))\n (loop (- i 1)) i))))\n (substring s start end)))\n\n;; Set variable value (respects suppress-effects for short-circuit)\n(def (arith-set-var! state name value)\n (unless (arith-state-suppress-effects state)\n ((arith-state-env-set state) name (number->string value)))\n value)\n\n;; Resolve name through dynamic variable references (for assignment targets)\n(def (arith-resolve-name state name)\n (let resolve ((name name) (depth 0))\n (if (> depth 10) name\n (let ((val ((arith-state-env-get state) name)))\n (if (not val) name\n (let ((num (string->number val)))\n (if num name ;; value is numeric, stop\n ;; Value looks like a variable name, follow it\n (if (and (> (string-length val) 0)\n (or (char-alphabetic? (string-ref val 0))\n (char=? (string-ref val 0) #\\_))\n (let check ((j 0))\n (or (>= j (string-length val))\n (let ((ch (string-ref val j)))\n (and (or (char-alphabetic? ch) (char-numeric? ch)\n (char=? ch #\\_))\n (check (+ j 1)))))))\n (resolve val (+ depth 1))\n name))))))))\n\n;;; --- Precedence levels (lowest to highest) ---\n\n;; Level 1: Comma (sequence)\n(def (parse-comma-expr state)\n (let loop ((result (parse-assignment-expr state)))\n (if (arith-consume-op! state \",\")\n (loop (parse-assignment-expr state))\n result)))\n\n;; Level 2: Assignment\n(def (parse-assignment-expr state)\n (let ((tok (arith-peek state)))\n (if (and tok (eq? (arith-token-type tok) 'name))\n ;; Peek ahead for assignment operator (possibly past array index)\n (let* ((name (arith-token-value tok))\n (saved-pos (arith-state-pos state)))\n (arith-advance! state)\n ;; Check for array indexing: name[expr]\n (let-values (((final-name is-array?) (parse-array-index state name)))\n (let ((op-tok (arith-peek state)))\n (if (and op-tok (eq? (arith-token-type op-tok) 'op)\n (member (arith-token-value op-tok)\n '(\"=\" \"+=\" \"-=\" \"*=\" \"/=\" \"%=\" \"<<=\" \">>=\" \"&=\" \"^=\" \"|=\")))\n (let ((op (arith-token-value op-tok))\n ;; For dynamic names, resolve to final target\n (target (if is-array? final-name (arith-resolve-name state final-name))))\n (arith-advance! state)\n (let ((rhs (parse-assignment-expr state)))\n (cond\n ((string=? op \"=\") (arith-set-var! state target rhs))\n ((string=? op \"+=\") (arith-set-var! state target (+ (arith-get-var state target) rhs)))\n ((string=? op \"-=\") (arith-set-var! state target (- (arith-get-var state target) rhs)))\n ((string=? op \"*=\") (arith-set-var! state target (* (arith-get-var state target) rhs)))\n ((string=? op \"/=\") (arith-set-var! state target (quotient (arith-get-var state target) rhs)))\n ((string=? op \"%=\") (arith-set-var! state target (remainder (arith-get-var state target) rhs)))\n ((string=? op \"<<=\") (arith-set-var! state target (arithmetic-shift (arith-get-var state target) rhs)))\n ((string=? op \">>=\") (arith-set-var! state target (arithmetic-shift (arith-get-var state target) (- rhs))))\n ((string=? op \"&=\") (arith-set-var! state target (bitwise-and (arith-get-var state target) rhs)))\n ((string=? op \"^=\") (arith-set-var! state target (bitwise-xor (arith-get-var state target) rhs)))\n ((string=? op \"|=\") (arith-set-var! state target (bitwise-ior (arith-get-var state target) rhs)))\n (else (error #f \"unknown assignment op\" op)))))\n ;; Not an assignment, backtrack\n (begin\n (set! (arith-state-pos state) saved-pos)\n (parse-ternary-expr state))))))\n (parse-ternary-expr state))))\n\n;; Parse optional array index after a name: name[expr]\n;; Returns (values effective-name is-array?)\n(def (parse-array-index state name)\n (if (arith-consume-op! state \"[\")\n (let ((idx (parse-comma-expr state)))\n (arith-expect-op! state \"]\")\n (values (string-append name \"[\" (number->string idx) \"]\") #t))\n (values name #f)))\n\n;; Level 3: Ternary ?:\n(def (parse-ternary-expr state)\n (let ((cond-val (parse-logical-or state)))\n (if (arith-consume-op! state \"?\")\n (if (not (= cond-val 0))\n ;; Condition true: evaluate then-branch, suppress else-branch\n (let ((then-val (parse-comma-expr state)))\n (arith-expect-op! state \":\")\n (let ((saved (arith-state-suppress-effects state)))\n (set! (arith-state-suppress-effects state) #t)\n (parse-ternary-expr state)\n (set! (arith-state-suppress-effects state) saved))\n then-val)\n ;; Condition false: suppress then-branch, evaluate else-branch\n (begin\n (let ((saved (arith-state-suppress-effects state)))\n (set! (arith-state-suppress-effects state) #t)\n (parse-comma-expr state)\n (set! (arith-state-suppress-effects state) saved))\n (arith-expect-op! state \":\")\n (parse-ternary-expr state)))\n cond-val)))\n\n;; Level 4: Logical OR || (short-circuit)\n(def (parse-logical-or state)\n (let loop ((result (parse-logical-and state)))\n (if (arith-consume-op! state \"||\")\n (if (not (= result 0))\n ;; Short-circuit: parse but suppress side effects on RHS\n (let ((saved (arith-state-suppress-effects state)))\n (set! (arith-state-suppress-effects state) #t)\n (parse-logical-and state)\n (set! (arith-state-suppress-effects state) saved)\n (loop 1))\n (let ((rhs (parse-logical-and state)))\n (loop (if (not (= rhs 0)) 1 0))))\n result)))\n\n;; Level 5: Logical AND && (short-circuit)\n(def (parse-logical-and state)\n (let loop ((result (parse-bitwise-or state)))\n (if (arith-consume-op! state \"&&\")\n (if (= result 0)\n ;; Short-circuit: parse but suppress side effects on RHS\n (let ((saved (arith-state-suppress-effects state)))\n (set! (arith-state-suppress-effects state) #t)\n (parse-bitwise-or state)\n (set! (arith-state-suppress-effects state) saved)\n (loop 0))\n (let ((rhs (parse-bitwise-or state)))\n (loop (if (not (= rhs 0)) 1 0))))\n result)))\n\n;; Level 6: Bitwise OR |\n(def (parse-bitwise-or state)\n (let loop ((result (parse-bitwise-xor state)))\n (if (arith-consume-op! state \"|\")\n (loop (bitwise-ior result (parse-bitwise-xor state)))\n result)))\n\n;; Level 7: Bitwise XOR ^\n(def (parse-bitwise-xor state)\n (let loop ((result (parse-bitwise-and state)))\n (if (arith-consume-op! state \"^\")\n (loop (bitwise-xor result (parse-bitwise-and state)))\n result)))\n\n;; Level 8: Bitwise AND &\n(def (parse-bitwise-and state)\n (let loop ((result (parse-equality state)))\n (if (arith-consume-op! state \"&\")\n (loop (bitwise-and result (parse-equality state)))\n result)))\n\n;; Level 9: Equality == !=\n(def (parse-equality state)\n (let loop ((result (parse-comparison state)))\n (cond\n ((arith-consume-op! state \"==\")\n (loop (if (= result (parse-comparison state)) 1 0)))\n ((arith-consume-op! state \"!=\")\n (loop (if (not (= result (parse-comparison state))) 1 0)))\n (else result))))\n\n;; Level 10: Comparison < <= > >=\n(def (parse-comparison state)\n (let loop ((result (parse-shift state)))\n (cond\n ((arith-consume-op! state \"<=\")\n (loop (if (<= result (parse-shift state)) 1 0)))\n ((arith-consume-op! state \">=\")\n (loop (if (>= result (parse-shift state)) 1 0)))\n ((arith-consume-op! state \"<\")\n (loop (if (< result (parse-shift state)) 1 0)))\n ((arith-consume-op! state \">\")\n (loop (if (> result (parse-shift state)) 1 0)))\n (else result))))\n\n;; Level 11: Bit shift << >>\n;; Bash allows negative shifts (implementation-defined behavior)\n;; We emulate 64-bit C behavior: mask shift amount to 0-63\n(def (parse-shift state)\n (let loop ((result (parse-additive state)))\n (cond\n ((arith-consume-op! state \"<<\")\n (let* ((amt (parse-additive state))\n ;; Emulate 64-bit C: mask to 6 bits (0-63)\n (effective-amt (bitwise-and amt 63)))\n (loop (arith-truncate-64 (arithmetic-shift result effective-amt)))))\n ((arith-consume-op! state \">>\")\n (let* ((amt (parse-additive state))\n (effective-amt (bitwise-and amt 63)))\n (loop (arith-truncate-64 (arithmetic-shift (arith-to-signed-64 result) (- effective-amt))))))\n (else result))))\n\n;; Truncate to 64-bit signed integer range (emulate C int64_t)\n(def (arith-truncate-64 n)\n (let ((masked (bitwise-and n #xFFFFFFFFFFFFFFFF)))\n (if (> masked #x7FFFFFFFFFFFFFFF)\n (- masked #x10000000000000000)\n masked)))\n\n;; Convert to 64-bit signed representation\n(def (arith-to-signed-64 n)\n (let ((masked (bitwise-and n #xFFFFFFFFFFFFFFFF)))\n (if (> masked #x7FFFFFFFFFFFFFFF)\n (- masked #x10000000000000000)\n masked)))\n\n;; Level 12: Addition + -\n(def (parse-additive state)\n (let loop ((result (parse-multiplicative state)))\n (cond\n ((arith-consume-op! state \"+\")\n (loop (+ result (parse-multiplicative state))))\n ((arith-consume-op! state \"-\")\n (loop (- result (parse-multiplicative state))))\n (else result))))\n\n;; Level 13: Multiplication * / %\n;; Use quotient and remainder for C-style semantics\n(def (parse-multiplicative state)\n (let loop ((result (parse-exponent state)))\n (cond\n ((arith-consume-op! state \"*\")\n (loop (* result (parse-exponent state))))\n ((arith-consume-op! state \"/\")\n (let ((divisor (parse-exponent state)))\n (when (= divisor 0) (error #f \"arithmetic: division by zero\"))\n (loop (quotient result divisor))))\n ((arith-consume-op! state \"%\")\n (let ((divisor (parse-exponent state)))\n (when (= divisor 0) (error #f \"arithmetic: division by zero\"))\n (loop (remainder result divisor))))\n (else result))))\n\n;; Level 14: Exponentiation ** (right-associative)\n(def (parse-exponent state)\n (let ((base (parse-unary state)))\n (if (arith-consume-op! state \"**\")\n (let ((exp (parse-exponent state))) ;; right-associative\n (when (< exp 0) (error #f \"arithmetic: exponent less than 0\"))\n (expt base exp))\n base)))\n\n;; Level 15: Unary ! ~ + - (prefix)\n(def (parse-unary state)\n (cond\n ((arith-consume-op! state \"!\")\n (if (= (parse-unary state) 0) 1 0))\n ((arith-consume-op! state \"~\")\n (bitwise-not (parse-unary state)))\n ((arith-consume-op! state \"-\")\n (- (parse-unary state)))\n ((arith-consume-op! state \"+\")\n (parse-unary state))\n ;; Pre-increment/decrement\n ((arith-consume-op! state \"++\")\n (let ((tok (arith-peek state)))\n (if (and tok (eq? (arith-token-type tok) 'name))\n (let* ((name (arith-token-value tok)))\n (arith-advance! state)\n (let-values (((target is-array?) (parse-array-index state name)))\n (let ((resolved (if is-array? target (arith-resolve-name state target))))\n (arith-set-var! state resolved (+ (arith-get-var state resolved) 1)))))\n (error #f \"arithmetic: ++ requires variable\"))))\n ((arith-consume-op! state \"--\")\n (let ((tok (arith-peek state)))\n (if (and tok (eq? (arith-token-type tok) 'name))\n (let* ((name (arith-token-value tok)))\n (arith-advance! state)\n (let-values (((target is-array?) (parse-array-index state name)))\n (let ((resolved (if is-array? target (arith-resolve-name state target))))\n (arith-set-var! state resolved (- (arith-get-var state resolved) 1)))))\n (error #f \"arithmetic: -- requires variable\"))))\n (else (parse-postfix state))))\n\n;; Level 16: Postfix ++ --\n(def (parse-postfix state)\n (let ((val (parse-primary state)))\n ;; Check for postfix ++ or -- (only valid after a name)\n val))\n\n;; Primary: number, variable (with optional array index), or (expr)\n(def (parse-primary state)\n (let ((tok (arith-peek state)))\n (cond\n ((not tok) (error #f \"arithmetic: unexpected end of expression\"))\n ((eq? (arith-token-type tok) 'number)\n (arith-advance! state)\n (arith-token-value tok))\n ((eq? (arith-token-type tok) 'name)\n (arith-advance! state)\n (let ((name (arith-token-value tok)))\n ;; Check for array indexing: name[expr]\n (let-values (((effective-name is-array?) (parse-array-index state name)))\n ;; Check for postfix ++ --\n (cond\n ((arith-consume-op! state \"++\")\n (let* ((target (if is-array? effective-name (arith-resolve-name state effective-name)))\n (val (arith-get-var state target)))\n (arith-set-var! state target (+ val 1))\n val)) ;; return old value\n ((arith-consume-op! state \"--\")\n (let* ((target (if is-array? effective-name (arith-resolve-name state effective-name)))\n (val (arith-get-var state target)))\n (arith-set-var! state target (- val 1))\n val)) ;; return old value\n (else (arith-get-var state effective-name))))))\n ((and (eq? (arith-token-type tok) 'op)\n (string=? (arith-token-value tok) \"(\"))\n (arith-advance! state)\n (let ((result (parse-comma-expr state)))\n (arith-expect-op! state \")\")\n result))\n (else\n (error #f (format \"arithmetic: unexpected token ~a\" (arith-token-value tok)))))))\n"} {"text":";; FILE: jerboa-shell/bash-compatibility.md\n# Shell Compatibility Report\n\nGenerated: 2026-05-13\n\n## Summary\n\n| Shell | Pass | Total | Rate |\n|-------|------|-------|------|\n| bash | 901 | 1179 | 76% |\n| jsh-macos | 1149 | 1179 | 97% |\n\n## Results by Tier\n\n### Tier 0 — Core\n\n| Suite | Description | bash | jsh-macos |\n|-------|-------------|-----|-----|\n| smoke | Basic shell operations | 14/18 | **18/18** |\n| pipeline | Pipe operator and pipelines | 17/26 | 24/26 |\n| redirect | I/O redirection (>, <, >>, etc.) | 32/41 | **41/41** |\n| redirect-multi | Multiple and complex redirections | 11/13 | **13/13** |\n| builtin-eval-source | eval and source/. builtins | 20/23 | **23/23** |\n| command-sub | Command substitution $() and `` | 28/30 | **30/30** |\n| comments | Shell comments | **2/2** | **2/2** |\n| exit-status | Exit status and $? | 9/11 | **11/11** |\n\n### Tier 1 — Expansion & Variables\n\n| Suite | Description | bash | jsh-macos |\n|-------|-------------|-----|-----|\n| here-doc | Here-documents (<<, <<-, <<< ) | 35/36 | **36/36** |\n| quote | Quoting (single, double, $'...') | 28/35 | 34/35 |\n| word-eval | Word evaluation and expansion | **8/8** | **8/8** |\n| word-split | IFS word splitting | 47/55 | **55/55** |\n| var-sub | Variable substitution ($var, ${var}) | 4/6 | **6/6** |\n| var-sub-quote | Variable substitution in quoting contexts | 39/41 | **41/41** |\n| var-num | Numeric/special variables ($#, $?, $$, etc.) | **7/7** | **7/7** |\n| var-op-test | Variable operators (${var:-default}, etc.) | 26/37 | 35/37 |\n| var-op-strip | Variable pattern stripping (${var#pat}, etc.) | 27/29 | 28/29 |\n| var-op-len | Variable length ${#var} | 3/9 | 7/9 |\n| assign | Variable assignment | 33/48 | **48/48** |\n| tilde | Tilde expansion (~, ~user) | 8/14 | 12/14 |\n\n### Tier 2 — Builtins & Advanced\n\n| Suite | Description | bash | jsh-macos |\n|-------|-------------|-----|-----|\n| arith | Arithmetic expansion $(( )) and (( )) | 61/74 | **74/74** |\n| glob | Filename globbing (*, ?, [...]) | 35/39 | 38/39 |\n| brace-expansion | Brace expansion ({a,b}, {1..5}) | 36/55 | 52/55 |\n| case_ | case statement | 11/13 | **13/13** |\n| if_ | if/elif/else statement | **5/5** | **5/5** |\n| loop | while, until, for loops | 23/29 | **29/29** |\n| for-expr | C-style for ((i=0; ...)) | **9/9** | **9/9** |\n| subshell | Subshell execution (...) | **2/2** | **2/2** |\n| sh-func | Shell functions | 10/12 | **12/12** |\n| builtin-echo | echo builtin | 15/27 | **27/27** |\n| builtin-printf | printf builtin | 38/63 | 58/63 |\n| builtin-read | read builtin | 52/64 | **64/64** |\n| builtin-cd | cd builtin | 23/30 | 28/30 |\n| builtin-set | set and shopt builtins | **24/24** | **24/24** |\n| builtin-type | type/command/which builtins | 2/6 | 5/6 |\n| builtin-trap | trap builtin | 30/33 | **33/33** |\n| builtin-bracket | [[ ]] and [ ] test operators | 48/52 | **52/52** |\n| builtin-misc | Misc builtins (true, false, colon, etc.) | 2/7 | 5/7 |\n| builtin-process | Process builtins (kill, wait, ulimit, etc.) | 16/26 | 25/26 |\n| background | Background jobs (&, wait, jobs) | 19/27 | 25/27 |\n| command-parsing | Command parsing edge cases | 4/5 | **5/5** |\n| var-op-bash | Bash-specific variable operations | 6/27 | **27/27** |\n| var-op-slice | Variable slicing ${var:offset:length} | 13/22 | **22/22** |\n| assign-extended | declare/typeset/local/export | 19/39 | 36/39 |\n\n## Failing Tests — jsh-macos\n\nTests where jsh-macos fails but bash passes.\n\n### Tier 2 — Builtins & Advanced\n\n| Suite | # | Test | Reason |\n|-------|---|------|--------|\n| brace-expansion | 53 | Side effect in expansion | stdout mismatch |\n| background | 8 | wait for N parallel jobs and check failure | stdout mismatch |\n| background | 13 | Wait for job and PIPESTATUS | stdout mismatch |\n\n## Bonus: Tests where jsh-macos passes but bash fails\n\n| Suite | # | Test |\n|-------|---|------|\n| smoke | 4 | pipeline |\n| smoke | 5 | pipeline with builtin |\n| smoke | 12 | Here doc with redirect |\n| smoke | 15 | failed command |\n| pipeline | 4 | Redirect in Pipeline |\n| pipeline | 11 | |& |\n| pipeline | 19 | Evaluation of argv[0] in pipeline occurs in child |\n| pipeline | 20 | bash/dash/mksh run the last command is run in its own process |\n| pipeline | 21 | shopt -s lastpipe (always on in OSH) |\n| pipeline | 22 | shopt -s lastpipe (always on in OSH) |\n| pipeline | 26 | shopt -s lastpipe and shopt -s no_last_fork interaction |\n| redirect | 9 | Descriptor redirect with filename |\n| redirect | 10 | Redirect echo to stderr, and then redirect all of stdout somewhere. |\n| redirect | 11 | Named file descriptor |\n| redirect | 28 | 1>&2- (Bash bug: fail to restore closed fd) |\n| redirect | 31 | &>> appends stdout and stderr |\n| redirect | 33 | can't mention big file descriptor |\n| redirect | 37 | exec {fd}>&- (OSH regression: fails to close fd) |\n| redirect | 38 | noclobber can still write to non-regular files like /dev/null |\n| redirect | 40 | Parsing of x={myvar} and related cases |\n| redirect-multi | 3 | ysh behavior when glob doesn't match |\n| redirect-multi | 11 | Non-file redirects don't respect glob args (we differe from bash) |\n| builtin-eval-source | 5 | eval YSH block with 'break continue return error' |\n| builtin-eval-source | 14 | Source with syntax error |\n| builtin-eval-source | 15 | Eval with syntax error |\n| command-sub | 2 | case in subshell |\n| command-sub | 29 | Syntax errors with double quotes within backticks |\n| exit-status | 3 | subshell OverflowError https://github.com/oilshell/oil/issues/996 |\n| exit-status | 4 | func subshell OverflowError https://github.com/oilshell/oil/issues/996 |\n| here-doc | 7 | Here doc with bad comsub delimiter |\n| quote | 20 | $? split over multiple lines |\n| quote | 21 | Unterminated single quote |\n| quote | 22 | Unterminated double quote |\n| quote | 29 | $'' octal escapes with fewer than 3 chars |\n| quote | 34 | $'' supports \\cA escape for Ctrl-A - mask with 0x1f |\n| quote | 35 | \\c' is an escape, unlike bash |\n| word-split | 20 | empty literals are not elided |\n| word-split | 40 | IFS='' with ${!prefix@} and ${!prefix*} (bug #627) |\n| word-split | 41 | IFS='' with ${!a[@]} and ${!a[*]} (bug #627) |\n| word-split | 42 | Bug #628 split on : with : in literal word |\n| word-split | 49 | IFS=x and '' and $@ - same bug as spec/toysh-posix case #12 |\n| word-split | 50 | IFS=x and '' and $@ (#2) |\n| word-split | 51 | IFS=x and '' and $@ (#3) |\n| word-split | 52 | \"\"$A\"\" - empty string on both sides - derived from spec/toysh-posix #15 |\n| var-sub | 1 | Bad var sub |\n| var-sub | 2 | Braced block inside ${} |\n| var-sub-quote | 33 | \"${undef-'c d'}\" and \"${foo%'c d'}\" are parsed differently |\n| var-sub-quote | 39 | Right Brace as argument (similar to #702) |\n| var-op-test | 5 | Quoted with array as default value |\n| var-op-test | 13 | \"${array[@]} with set -u (bash is outlier) |\n| var-op-test | 16 | Nix idiom ${!hooksSlice+\"${!hooksSlice}\"} - was workaround for obsolete bash 4.3 bug |\n| var-op-test | 22 | $* (\"\" \"\") and - and + (IFS=) |\n| var-op-test | 23 | \"$*\" (\"\" \"\") and - and + (IFS=) |\n| var-op-test | 25 | Error when empty |\n| var-op-test | 26 | Error when unset |\n| var-op-test | 34 | op-test for ${a[@]} and ${a[*]} |\n| var-op-test | 36 | op-test for ${!array} with array=\"a[@]\" or array=\"a[*]\" |\n| var-op-strip | 11 | Strip unicode prefix |\n| var-op-len | 2 | Unicode string length (UTF-8) |\n| var-op-len | 7 | Length of undefined variable with nounset |\n| var-op-len | 8 | Length operator can't be followed by test operator |\n| var-op-len | 9 | ${#s} respects LC_ALL - length in bytes or code points |\n| assign | 3 | Env binding can use preceding bindings, but not subsequent ones |\n| assign | 15 | Env binding in readonly/declare is NOT exported! (pitfall) |\n| assign | 17 | dynamic local variables (and splitting) |\n| assign | 19 | 'local x' does not set variable |\n| assign | 20 | 'local -a x' does not set variable |\n| assign | 25 | Reveal existence of \"temp frame\" (All shells disagree here!!!) |\n| assign | 27 | Using ${x-default} after unsetting local shadowing a global |\n| assign | 28 | Using ${x-default} after unsetting a temp binding shadowing a global |\n| assign | 31 | assignment using dynamic keyword (splits in most shells, not in zsh/osh) |\n| assign | 32 | assignment using dynamic var names doesn't split |\n| assign | 35 | readonly $x where x='b c' |\n| assign | 41 | redirect after bare assignment |\n| assign | 44 | declare -A dict does not remove existing arrays (OSH regression) |\n| assign | 45 | \"readonly -a arr\" and \"readonly -A dict\" should not not remove existing arrays |\n| assign | 46 | \"declare -a arr\" and \"readonly -a a\" creates an empty array (OSH) |\n| tilde | 4 | No tilde expansion in word that looks like assignment but isn't |\n| tilde | 5 | tilde expansion of word after redirect |\n| tilde | 12 | x=${undef-~:~} |\n| tilde | 13 | strict tilde |\n| arith | 8 | Constant with quotes like '1' |\n| arith | 12 | Invalid string to int with strict_arith |\n| arith | 21 | Increment undefined variables with nounset |\n| arith | 29 | No floating point |\n| arith | 41 | nounset with arithmetic |\n| arith | 44 | Invalid LValue |\n| arith | 45 | Invalid LValue that looks like array |\n| arith | 46 | Invalid LValue: two sets of brackets |\n| arith | 51 | Comment not allowed in the middle of multiline arithmetic |\n| arith | 66 | Invalid constant |\n| arith | 69 | Negative numbers with bit shift |\n| arith | 71 | undef[0] with nounset |\n| arith | 74 | s[0] with string '12 34' |\n| glob | 24 | set -o noglob |\n| glob | 31 | Glob unicode char |\n| glob | 38 | pattern starting with . does not return . and .. |\n| brace-expansion | 12 | double expansion with simple var -- bash bug |\n| brace-expansion | 14 | double expansion with literal and simple var |\n| brace-expansion | 18 | { in expansion |\n| brace-expansion | 32 | Number range expansion |\n| brace-expansion | 33 | Ascending number range expansion with negative step is invalid |\n| brace-expansion | 34 | regression: -1 step disallowed |\n| brace-expansion | 35 | regression: 0 step disallowed |\n| brace-expansion | 36 | Descending number range expansion with positive step is invalid |\n| brace-expansion | 37 | Descending number range expansion with negative step |\n| brace-expansion | 38 | Singleton ranges |\n| brace-expansion | 39 | Singleton char ranges with steps |\n| brace-expansion | 41 | Char range expansion with step |\n| brace-expansion | 42 | Char ranges with steps of the wrong sign |\n| brace-expansion | 44 | Descending char range expansion |\n| brace-expansion | 45 | Fixed width number range expansion |\n| brace-expansion | 46 | Inconsistent fixed width number range expansion |\n| brace-expansion | 47 | Inconsistent fixed width number range expansion |\n| case_ | 2 | Case statement with ;;& |\n| case_ | 3 | Case statement with ;& |\n| loop | 3 | for loop with invalid identifier |\n| loop | 15 | continue in subshell |\n| loop | 16 | continue in subshell aborts with errexit |\n| loop | 17 | bad arg to break |\n| loop | 18 | too many args to continue |\n| loop | 24 | top-level break/continue/return (without strict_control_flow) |\n| sh-func | 9 | return \"\" (a lot of disagreement) |\n| sh-func | 12 | Scope of global variable when sourced in function (Shell Functions aren't Closures) |\n| builtin-echo | 10 | echo -e with C escapes |\n| builtin-echo | 16 | echo -e with 4 digit unicode escape |\n| builtin-echo | 17 | echo -e with 8 digit unicode escape |\n| builtin-echo | 18 | \\0377 is the highest octal byte |\n| builtin-echo | 19 | \\0400 is one more than the highest octal byte |\n| builtin-echo | 20 | \\0777 is out of range |\n| builtin-echo | 21 | incomplete hex escape |\n| builtin-echo | 22 | \\x |\n| builtin-echo | 23 | incomplete octal escape |\n| builtin-echo | 24 | incomplete unicode escape |\n| builtin-echo | 25 | \\u6 |\n| builtin-echo | 26 | \\0 \\1 \\8 |\n| builtin-printf | 4 | printf -v a[1] |\n| builtin-printf | 7 | dynamic declare instead of %q |\n| builtin-printf | 17 | %06s is no-op |\n| builtin-printf | 26 | Unicode char with ' |\n| builtin-printf | 27 | Invalid UTF-8 |\n| builtin-printf | 39 | printf %c unicode - prints the first BYTE of a string - it does not respect UTF-8 |\n| builtin-printf | 41 | printf %q |\n| builtin-printf | 42 | printf %6q (width) |\n| builtin-printf | 43 | printf negative numbers |\n| builtin-printf | 46 | Runtime error for invalid integer |\n| builtin-printf | 47 | %(strftime format)T |\n| builtin-printf | 48 | %(strftime format)T doesn't respect TZ if not exported |\n| builtin-printf | 49 | %(strftime format)T TZ in environ but not in shell's memory |\n| builtin-printf | 50 | %10.5(strftime format)T |\n| builtin-printf | 53 | printf positive integer overflow |\n| builtin-printf | 54 | printf negative integer overflow |\n| builtin-printf | 56 | printf %b unicode escapes |\n| builtin-printf | 59 | printf %b with truncated octal escapes |\n| builtin-printf | 62 | leading spaces are accepted in value given to %d %X, but not trailing spaces |\n| builtin-printf | 63 | Arbitrary base 64#a is rejected (unlike in shell arithmetic) |\n| builtin-read | 12 | read -n with invalid arg |\n| builtin-read | 15 | read -n vs. -N |\n| builtin-read | 16 | read -N ignores delimiters |\n| builtin-read | 34 | read -t 0 tests if input is available |\n| builtin-read | 36 | read -t -0.5 is invalid |\n| builtin-read | 38 | read -u syntax error |\n| builtin-read | 41 | read -u 3 -d b -N 6 |\n| builtin-read | 42 | read -N doesn't respect delimiter, while read -n does |\n| builtin-read | 44 | read usage |\n| builtin-read | 49 | mapfile from directory (bash doesn't handle errors) |\n| builtin-read | 50 | read -n 0 |\n| builtin-read | 64 | read bash bug |\n| builtin-cd | 3 | cd with 2 or more args - with strict_arg_parse |\n| builtin-cd | 26 | What happens when inherited $PWD and current dir disagree? |\n| builtin-cd | 27 | Survey of getcwd() syscall |\n| builtin-cd | 28 | chdir is a synonym for cd - busybox ash |\n| builtin-cd | 30 | pwd errors out on args with strict_arg_parse |\n| builtin-type | 3 | type of relative path |\n| builtin-type | 5 | special builtins are called out |\n| builtin-type | 6 | more special builtins |\n| builtin-trap | 1 | traps are not active inside subshells $() () trap | cat |\n| builtin-trap | 17 | exit 1 when trap code string is invalid |\n| builtin-trap | 33 | trap with command.NoOp - check internal invariant |\n| builtin-bracket | 31 | [ -t invalid ] |\n| builtin-bracket | 39 | -v to test variable (bash) |\n| builtin-bracket | 43 | Overflow error |\n| builtin-bracket | 51 | Looks like octal, but digit is too big |\n| builtin-misc | 1 | history builtin usage |\n| builtin-misc | 4 | time pipeline |\n| builtin-misc | 7 | Invalid shift argument |\n| builtin-process | 8 | Exit builtin with invalid arg |\n| builtin-process | 9 | Exit builtin with too many args |\n| builtin-process | 10 | time with brace group argument |\n| builtin-process | 12 | ulimit too many args |\n| builtin-process | 14 | ulimit negative arg |\n| builtin-process | 15 | ulimit -a doesn't take arg |\n| builtin-process | 16 | ulimit doesn't accept multiple flags - reduce confusion between shells |\n| builtin-process | 20 | ulimit that is 64 bits |\n| builtin-process | 22 | ulimit -f 1 prevents files larger 512 bytes |\n| background | 2 | wait -n with arguments - arguments are respected |\n| background | 3 | wait -n with nothing to wait for |\n| background | 6 | wait with invalid arg |\n| background | 18 | wait -n |\n| background | 22 | jobs prints one line per job |\n| background | 23 | jobs -p prints one line per job |\n| background | 25 | YSH wait --all |\n| background | 26 | YSH wait --verbose |\n| command-parsing | 1 | Prefix env on assignment |\n| var-op-bash | 1 | Lower Case with , and ,, |\n| var-op-bash | 2 | Upper Case with ^ and ^^ |\n| var-op-bash | 3 | Case folding - Unicode characters |\n| var-op-bash | 5 | Case folding that depends on locale (not enabled, requires Turkish locale) |\n| var-op-bash | 6 | Lower Case with constant string (VERY WEIRD) |\n| var-op-bash | 7 | Lower Case glob |\n| var-op-bash | 8 | ${x@u} U L - upper / lower case (bash 5.1 feature) |\n| var-op-bash | 9 | ${x@Q} |\n| var-op-bash | 10 | ${array@Q} and ${array[@]@Q} |\n| var-op-bash | 13 | ${var@a} for attributes |\n| var-op-bash | 14 | ${var@a} error conditions |\n| var-op-bash | 15 | undef and @P @Q @a |\n| var-op-bash | 16 | argv array and @P @Q @a |\n| var-op-bash | 17 | assoc array and @P @Q @a |\n| var-op-bash | 19 | ${#var@X} is a parse error |\n| var-op-bash | 21 | undef vs. empty string in var ops |\n| var-op-bash | 23 | ${a[0]@a} and ${a@a} |\n| var-op-bash | 24 | ${!r@a} with r='a[0]' (attribute for indirect expansion of an array element) |\n| var-op-bash | 25 | Array expansion with nullary var op @Q |\n| var-op-bash | 26 | Array expansion with nullary var op @P |\n| var-op-bash | 27 | Array expansion with nullary var op @a |\n| var-op-slice | 7 | Negative second arg is position, not length! |\n| var-op-slice | 8 | Negative start index respects unicode |\n| var-op-slice | 10 | Slice undefined |\n| var-op-slice | 12 | Slice string with invalid UTF-8 results in empty string and warning |\n| var-op-slice | 13 | Slice string with invalid UTF-8 with strict_word_eval |\n| var-op-slice | 16 | Simple ${@:offset} |\n| var-op-slice | 17 | ${@:offset} and ${*:offset} |\n| var-op-slice | 18 | ${@:offset:length} and ${*:offset:length} |\n| var-op-slice | 19 | ${@:0:1} |\n| assign-extended | 5 | declare -F with shopt -s extdebug prints more info |\n| assign-extended | 8 | declare |\n| assign-extended | 9 | declare -p |\n| assign-extended | 11 | declare -p var |\n| assign-extended | 12 | declare -p arr |\n| assign-extended | 14 | declare -pnrx |\n| assign-extended | 15 | declare -paA |\n| assign-extended | 16 | declare -pnrx var |\n| assign-extended | 17 | declare -pg |\n| assign-extended | 18 | declare -pg var |\n| assign-extended | 20 | declare -p and value.Undef |\n| assign-extended | 25 | typeset -r makes a string readonly |\n| assign-extended | 26 | typeset -ar makes it readonly |\n| assign-extended | 29 | Env bindings shouldn't contain array assignments |\n| assign-extended | 31 | declare -g (bash-specific; bash-completion uses it) |\n| assign-extended | 33 | dynamic array parsing is not allowed |\n| assign-extended | 36 | typeset +r removes read-only attribute (TODO: documented in bash to do nothing) |\n"} -{"text":";; FILE: jerboa-shell/build-jsh-android.ss\n#!chezscheme\n;;; build-jsh-android.ss — Build jsh binary on Android/Termux (aarch64, Bionic)\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:<stubs> < build-jsh-android.ss\n;;;\n;;; This script:\n;;; 1. Compiles jsh program with WPO\n;;; 2. Creates libs-only boot file\n;;; 3. Generates C files with embedded boot data\n;;; 4. Compiles C with cc (clang)\n;;; 5. Links binary with libkernel.a (static Chez) + shared Bionic libc\n;;;\n;;; The resulting jsh-android binary is a self-contained ELF for aarch64 Android.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table))\n\n;; Suppress format warnings during compilation (Chez warns about ~<space>\n;; directives in format strings used by jsh code)\n(define (with-warnings-suppressed thunk)\n (with-exception-handler\n (lambda (c) (if (warning? c) (void) (raise-continuable c)))\n thunk))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/data/data/com.termux/files/home\"))\n\n;; All deps are vendored inside the repo\n(define vendor-dir\n (or (getenv \"VENDOR\")\n (format \"~a/vendor\" (current-directory))))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (format \"~a/jerboa/lib\" vendor-dir)))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (format \"~a/jerboa\" vendor-dir)))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (format \"~a/jerboa-ssh/src\" vendor-dir)))\n\n(define jerboa-ssh-shim\n (format \"~a/jerboa-ssh/jerboa_ssh_shim.c\" vendor-dir))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jsqlite/src\" home-dir)))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls)\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (format \"~a/jerboa-crypto/src\" vendor-dir)))\n\n(define jerboa-crypto-shim\n (format \"~a/jerboa-crypto/jerboa_crypto_shim.c\" vendor-dir))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (format \"~a/jerboa-coreutils/lib\" vendor-dir)))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (format \"~a/jerboa-awk/lib\" vendor-dir)))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (format \"~a/jerboa-sed/lib\" vendor-dir)))\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (format \"~a/jerboa-aws/lib\" vendor-dir)))\n\n(define has-aws? (file-exists? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; Staged vendor directories (compiled .sls→.so by build-jsh-android.sh step 1b)\n(define stage-dir\n (or (getenv \"STAGE\")\n (format \"~a/android-stage\" (current-directory))))\n\n(define stage-jerboa-crypto (format \"~a/jerboa-crypto\" stage-dir))\n(define stage-jerboa-ssh (format \"~a/jerboa-ssh\" stage-dir))\n(define stage-jerboa-aws (format \"~a/jerboa-aws\" stage-dir))\n(define stage-jerboa-fuse (format \"~a/jerboa-fuse\" stage-dir))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/vault.sls\" stage-jerboa-fuse)))\n\n;; Chez Scheme static installation\n(define chez-tarm64le\n (or (getenv \"CHEZ_TARM64LE\")\n (let ([prefix \"/data/data/com.termux/files/usr/lib\"])\n (let ([dirs (directory-list prefix)])\n (let ([csv-dir (find (lambda (d)\n (and (> (string-length d) 3)\n (string=? \"csv\" (substring d 0 3))))\n dirs)])\n (if csv-dir\n (format \"~a/~a/tarm64le\" prefix csv-dir)\n (error 'build \"Cannot find Chez tarm64le directory\")))))))\n\n(define scheme-h-dir chez-tarm64le)\n(define petite-boot-path (format \"~a/petite.boot\" chez-tarm64le))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-tarm64le))\n\n(printf \"Chez static: ~a~n\" chez-tarm64le)\n(printf \"Jerboa: ~a~n\" jerboa-dir)\n(printf \"~n\")\n\n;; Rust native library (libjerboa_native.a — crypto, TLS, integrity, etc.)\n;; Built by build-jsh-android.sh or manually: cd ~/jerboa/jerboa-native-rs && cargo build --release\n(define native-lib-path\n (let ([env-path (getenv \"JERBOA_NATIVE_LIB\")]\n [vendor-path (format \"~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a\" vendor-dir)]\n [home-path (format \"~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a\" home-dir)])\n (cond\n [(and env-path (file-exists? env-path)) env-path]\n [(file-exists? vendor-path) vendor-path]\n [(file-exists? home-path) home-path]\n [else (error 'build-jsh-android\n \"libjerboa_native.a not found. Build it: cd ~/jerboa/jerboa-native-rs && cargo build --release\")])))\n(printf \"Native lib: ~a~n\" native-lib-path)\n\n;; Rust coreutils (libjsh_coreutils.a — ls, cat, grep, etc.)\n;; Built by build-jsh-android.sh or manually: cd rust-coreutils && cargo build --release\n(define rust-coreutils-lib-path\n (let ([env-path (getenv \"JSH_COREUTILS_LIB\")]\n [local-path (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" (current-directory))])\n (cond\n [(and env-path (file-exists? env-path)) env-path]\n [(file-exists? local-path) local-path]\n [else (error 'build-jsh-android\n \"libjsh_coreutils.a not found. Build it: cd rust-coreutils && cargo build --release\")])))\n(printf \"Coreutils: ~a~n\" rust-coreutils-lib-path)\n\n;; ========== Helper functions ==========\n\n(define (file->c-header input-path output-path array-name size-name)\n (let* ([port (open-file-input-port input-path)]\n [data (get-bytevector-all port)]\n [size (bytevector-length data)])\n (close-port port)\n (call-with-output-file output-path\n (lambda (out)\n (fprintf out \"/* Auto-generated — do not edit */~n\")\n (fprintf out \"static const unsigned char ~a[] = {~n\" array-name)\n (let loop ([i 0])\n (when (< i size)\n (when (= 0 (modulo i 16)) (fprintf out \" \"))\n (fprintf out \"0x~2,'0x\" (bytevector-u8-ref data i))\n (when (< (+ i 1) size) (fprintf out \",\"))\n (when (= 15 (modulo i 16)) (fprintf out \"~n\"))\n (loop (+ i 1))))\n (fprintf out \"~n};~n\")\n (fprintf out \"static const unsigned int ~a = ~a;~n\" size-name size))\n 'replace)\n (printf \" ~a: ~a bytes~n\" output-path size)))\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-android \"Command failed\" cmd)))\n\n(define (existing-sos dir modules)\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" dir m)) modules)))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 1: Compile jsh program ==========\n\n;; Generate jsh-generated.ss from jsh.ss (injecting the feature manifest).\n(unless (file-exists? \"jsh-generated.ss\")\n (let ([source-file \"jsh.ss\"])\n (printf \" Generating jsh-generated.ss from ~a~n\" source-file)\n (unless (file-exists? source-file)\n (error 'build-jsh-android \"Program source not found\" source-file))\n (let ([text (call-with-input-file source-file get-string-all)])\n (call-with-output-file \"jsh-generated.ss\"\n (lambda (out) (display text out))\n 'replace))))\n\n(printf \"[1/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(with-warnings-suppressed\n (lambda ()\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 3])\n (compile-program \"jsh-generated.ss\"))))\n\n(unless (file-exists? \"jsh.so\")\n (fprintf (current-error-port) \"FATAL: jsh.so not created~n\")\n (exit 1))\n\n;; ========== Step 2: Skip WPO (use jsh.so directly) ==========\n\n(printf \"[2/7] Using jsh.so (skipping WPO for Android build)...~n\")\n(define program-so \"jsh.so\")\n\n;; ========== Step 3: Create libs-only boot file ==========\n\n(printf \"[3/7] Creating libs-only boot file...~n\")\n\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (append\n ;; Jerboa runtime + stdlib\n (existing-sos jerboa-dir\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\"\n \"std/regex\"\n \"std/match2\" \"std/sugar\" \"std/result\"\n \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\" \"std/typed\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\"\n \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\" \"std/text/json\"\n \"std/net/tcp\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; Local compat layer\n (filter file-exists? (list \"src/compat/gambit.so\"))\n ;; Coreutils shim\n (filter file-exists? (list \"src/jsh/coreutils-shim.so\"))\n ;; jerboa-crypto (compiled in staging dir)\n (existing-sos stage-jerboa-crypto '(\"jerboa-crypto\"))\n ;; jerboa-ssh (sub-libraries must come before main jerboa-ssh.so)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" stage-jerboa-ssh))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" stage-jerboa-ssh m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" stage-jerboa-ssh)))\n '())\n ;; jsqlite is pure Jerboa and is compiled through normal imports.\n ;; jerboa-coreutils\n (existing-sos coreutils-dir\n '(\"jerboa-coreutils/common\" \"jerboa-coreutils/common/version\"\n \"jerboa-coreutils/common/security\"\n \"jerboa-coreutils/basename\" \"jerboa-coreutils/dirname\"\n \"jerboa-coreutils/link\" \"jerboa-coreutils/unlink\"\n \"jerboa-coreutils/yes\" \"jerboa-coreutils/printenv\"\n \"jerboa-coreutils/sleep\" \"jerboa-coreutils/whoami\"\n \"jerboa-coreutils/logname\" \"jerboa-coreutils/hostname\"\n \"jerboa-coreutils/nproc\" \"jerboa-coreutils/tty\"\n \"jerboa-coreutils/sync\" \"jerboa-coreutils/hostid\"\n \"jerboa-coreutils/cat\" \"jerboa-coreutils/head\"\n \"jerboa-coreutils/tail\" \"jerboa-coreutils/tac\"\n \"jerboa-coreutils/tee\" \"jerboa-coreutils/wc\"\n \"jerboa-coreutils/nl\" \"jerboa-coreutils/fold\"\n \"jerboa-coreutils/expand\" \"jerboa-coreutils/unexpand\"\n \"jerboa-coreutils/fmt\"\n \"jerboa-coreutils/cut\" \"jerboa-coreutils/paste\"\n \"jerboa-coreutils/join\" \"jerboa-coreutils/comm\"\n \"jerboa-coreutils/sort\" \"jerboa-coreutils/uniq\"\n \"jerboa-coreutils/tr\" \"jerboa-coreutils/numfmt\"\n \"jerboa-coreutils/mkdir\" \"jerboa-coreutils/rmdir\"\n \"jerboa-coreutils/mktemp\" \"jerboa-coreutils/touch\"\n \"jerboa-coreutils/readlink\" \"jerboa-coreutils/realpath\"\n \"jerboa-coreutils/ln\" \"jerboa-coreutils/cp\"\n \"jerboa-coreutils/mv\" \"jerboa-coreutils/rm\"\n \"jerboa-coreutils/install\" \"jerboa-coreutils/shred\"\n \"jerboa-coreutils/ls\" \"jerboa-coreutils/chmod\"\n \"jerboa-coreutils/chown\" \"jerboa-coreutils/chgrp\"\n \"jerboa-coreutils/stat\" \"jerboa-coreutils/du\"\n \"jerboa-coreutils/df\" \"jerboa-coreutils/pathchk\"\n \"jerboa-coreutils/date\" \"jerboa-coreutils/id\"\n \"jerboa-coreutils/groups\" \"jerboa-coreutils/who\"\n \"jerboa-coreutils/users\" \"jerboa-coreutils/pinky\"\n \"jerboa-coreutils/uptime\" \"jerboa-coreutils/uname\"\n \"jerboa-coreutils/arch\"\n \"jerboa-coreutils/seq\" \"jerboa-coreutils/expr\"\n \"jerboa-coreutils/basenc\" \"jerboa-coreutils/base64\"\n \"jerboa-coreutils/base32\" \"jerboa-coreutils/od\"\n \"jerboa-coreutils/cksum\" \"jerboa-coreutils/md5sum\"\n \"jerboa-coreutils/sha1sum\" \"jerboa-coreutils/sha224sum\"\n \"jerboa-coreutils/sha256sum\" \"jerboa-coreutils/sha384sum\"\n \"jerboa-coreutils/sha512sum\" \"jerboa-coreutils/b2sum\"\n \"jerboa-coreutils/sum\"\n \"jerboa-coreutils/env\" \"jerboa-coreutils/timeout\"\n \"jerboa-coreutils/nice\" \"jerboa-coreutils/nohup\"\n \"jerboa-coreutils/chroot\" \"jerboa-coreutils/stdbuf\"\n \"jerboa-coreutils/truncate\" \"jerboa-coreutils/mkfifo\"\n \"jerboa-coreutils/mknod\" \"jerboa-coreutils/split\"\n \"jerboa-coreutils/csplit\" \"jerboa-coreutils/dd\"\n \"jerboa-coreutils/dircolors\"\n \"jerboa-coreutils/tsort\" \"jerboa-coreutils/shuf\"\n \"jerboa-coreutils/factor\" \"jerboa-coreutils/pr\"\n \"jerboa-coreutils/ptx\" \"jerboa-coreutils/stty\"\n \"jerboa-coreutils/chcon\" \"jerboa-coreutils/runcon\"\n \"jerboa-coreutils/dir\" \"jerboa-coreutils/vdir\"\n \"jerboa-coreutils/rev\"\n \"jerboa-coreutils/grep/pcre2\" \"jerboa-coreutils/grep\"))\n ;; jerboa-awk\n (existing-sos awk-dir\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed\n (existing-sos sed-dir\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-aws (compiled in staging dir)\n (if has-aws?\n (existing-sos stage-jerboa-aws\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\"\n \"jerboa-aws/time\" \"jerboa-aws/crypto\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/creds\" \"jerboa-aws/request\"\n \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jerboa-fuse (vault) — if available\n (if has-jerboa-fuse?\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" stage-jerboa-fuse m))\n '(\"chez/vault/format\"\n \"chez/fuse/constants\" \"chez/fuse/types\" \"chez/fuse/mount\"\n \"chez/fuse/codec\" \"chez/fuse/secmem\" \"chez/fuse/access\"\n \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\")))\n '())\n ;; jsh modules\n (existing-sos \"src/jsh\"\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\"\n \"mux-transport\" \"mux-relay\" \"mux-router\"\n \"mux-server\" \"mux-client\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"config\" \"startup\" \"sandbox\"\n \"rl\" \"limits\" \"harden\" \"main\"\n \"coreutils\"))))\n\n;; ========== Step 4: Generate C headers with embedded data ==========\n\n(printf \"[4/7] Embedding boot files + program as C headers...~n\")\n(file->c-header program-so \"jsh_program.h\"\n \"jsh_program_data\" \"jsh_program_data_len\")\n(file->c-header petite-boot-path \"jsh_petite_boot.h\"\n \"petite_boot_data\" \"petite_boot_size\")\n(file->c-header scheme-boot-path \"jsh_scheme_boot.h\"\n \"scheme_boot_data\" \"scheme_boot_size\")\n(file->c-header \"jsh.boot\" \"jsh_jsh_boot.h\"\n \"jsh_boot_data\" \"jsh_boot_size\")\n\n;; ========== Step 5: Generate static_boot.c and main C ==========\n\n(printf \"[5/7] Generating C source files...~n\")\n\n(define build-dir\n (format \"~a/jsh-android-build\" (or (getenv \"TMPDIR\") \"/tmp\")))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(define program-c (format \"~a/jsh_main_android.c\" build-dir))\n\n(define gcc \"cc\")\n(define harden-cflags \"-fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2\")\n\n;; static_boot.c — registers embedded boot files with Chez\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"jsh_petite_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_scheme_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_jsh_boot.h\\\"\\n\" out)\n (display \"\\nvoid static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", (void *)petite_boot_data, petite_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", (void *)scheme_boot_data, scheme_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", (void *)jsh_boot_data, jsh_boot_size);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; jsh_main_android.c — main entry point\n(call-with-output-file program-c\n (lambda (out)\n (display \"#define _GNU_SOURCE\\n\" out)\n (display \"#include <stdio.h>\\n#include <stdlib.h>\\n#include <string.h>\\n\" out)\n (display \"#include <unistd.h>\\n#include <fcntl.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"jsh_program.h\\\"\\n\\n\" out)\n (display \"extern void static_boot_init(void);\\n\" out)\n (display \"extern void register_ffi_symbols(void);\\n\\n\" out)\n\n ;; Forward declarations for FFI\n (display \"/* FFI forward declarations */\\n\" out)\n (display \"extern void ffi_ensure_std_fds(void);\\n\\n\" out)\n\n ;; register_ffi_symbols — pre-register all FFI symbols for static builds\n (display \"void register_ffi_symbols(void) {\\n\" out)\n\n ;; Android/Termux: not a fully static build — symbols resolved via dlopen at runtime.\n ;; We don't need to pre-register POSIX or FFI symbols since the binary is dynamically linked.\n ;; Only register symbols that the Scheme code looks up by name via foreign-procedure.\n (display \" /* Android: dynamically linked — most symbols resolved via dlopen */\\n\" out)\n (display \"}\\n\\n\" out)\n\n ;; main\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args as env vars\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Resolve exe path via /proc/self/exe\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" ssize_t len = readlink(\\\"/proc/self/exe\\\", exe_buf, sizeof(exe_buf) - 1);\\n\" out)\n (display \" if (len > 0) { exe_buf[len] = '\\\\0'; setenv(\\\"JSH_EXE\\\", exe_buf, 1); }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; Load program via temp file (memfd /proc/self/fd paths are blocked by SELinux on Android)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" char prog_path[4096];\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-prog-XXXXXX\\\", tmpdir);\\n\" out)\n (display \" int fd = mkstemp(prog_path);\\n\" out)\n (display \" if (fd < 0) { perror(\\\"mkstemp\\\"); return 1; }\\n\" out)\n (display \" if (write(fd, jsh_program_data, jsh_program_data_len) != (ssize_t)jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"write tmpfile\\\"); close(fd); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" close(fd);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Copy C headers to build dir\n(system (format \"cp jsh_program.h jsh_petite_boot.h jsh_scheme_boot.h jsh_jsh_boot.h '~a/'\" build-dir))\n\n;; Generate Android compat header (explicit_bzero not in Bionic's <string.h>)\n(call-with-output-file (format \"~a/android-compat.h\" build-dir)\n (lambda (out)\n (display \"#ifndef ANDROID_COMPAT_H\\n#define ANDROID_COMPAT_H\\n\" out)\n (display \"#ifndef _GNU_SOURCE\\n#define _GNU_SOURCE\\n#endif\\n\" out)\n (display \"#include <string.h>\\n\" out)\n ;; explicit_bzero is not in Bionic's <string.h> on Termux —\n ;; provide an inline fallback unconditionally when missing.\n (display \"#if defined(__ANDROID__)\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n (display \"static inline void explicit_bzero(void *b, size_t l) {\\n\" out)\n (display \" memset(b, 0, l);\\n\" out)\n (display \" __asm__ __volatile__(\\\"\\\" ::: \\\"memory\\\");\\n\" out)\n (display \"}\\n\" out)\n (display \"#endif\\n\" out)\n (display \"#endif\\n\" out)))\n 'replace)\n\n(define android-compat (format \"-include '~a/android-compat.h'\" build-dir))\n\n;; ========== Step 6: Compile C with clang ==========\n\n(printf \"[6/7] Compiling C with clang...~n\")\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags android-compat scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_android.c\n(run-cmd (format \"~a -c -O2 ~a ~a -I'~a' -o '~a/jsh_main_android.o' '~a'\"\n gcc harden-cflags android-compat scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags android-compat build-dir))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; Landlock: ffi_landlock_* provided by ffi-shim.c (returns -1 if syscall unavailable)\n;; jerboa_landlock_* provided by libjerboa_native.a\n;; No separate shim needed.\n\n;; jerboa-ssh shim\n;; -DCHEZ_SSH_NO_OPENSSL on jerboa_ssh_shim.c: use standalone ed25519 from Rust\n;; (libjerboa_native.a provides ed25519_*_standalone symbols)\n;; jerboa_ssh_crypto.c is compiled separately as jerboa-ssh-crypto.o; it still uses\n;; OpenSSL EVP for SSH transport crypto (HMAC/SHA/X25519/ChaCha20-Poly1305).\n;; Termux ships libcrypto.so/libssl.so so we link against those system libs.\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; -DCHEZ_SSH_NO_OPENSSL: use standalone ed25519/AES from Rust, not OpenSSL\n (run-cmd (format \"~a -c -O2 ~a ~a -DCHEZ_SSH_NO_OPENSSL -I'~a/jerboa-ssh' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (let ([crypto-src (format \"~a/jerboa-ssh/jerboa_ssh_crypto.c\" vendor-dir)])\n (if (file-exists? crypto-src)\n ;; -I. for embed-crypto.h (project root)\n (run-cmd (format \"~a -c -O2 ~a ~a -I'~a/jerboa-ssh' -I'~a' -o '~a/ed25519-standalone.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir (current-directory) build-dir crypto-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))))\n (let ([bcrypt-src (format \"~a/jerboa-ssh/bcrypt_pbkdf.c\" vendor-dir)])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a ~a -I'~a/jerboa-ssh' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir)))))\n (begin\n (printf \" Warning: jerboa-ssh shim not found, building without SSH agent~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-crypto shim\n(if (file-exists? jerboa-crypto-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/jerboa-crypto-shim.o' '~a' -Wall\"\n gcc harden-cflags build-dir jerboa-crypto-shim))\n (begin\n (printf \" Warning: jerboa-crypto shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-crypto-shim.o' -\" gcc build-dir))))\n\n;; coreutils FFI shim\n(let ([cu-src (format \"~a/jerboa-coreutils/support/libcoreutils.c\" vendor-dir)])\n (if (file-exists? cu-src)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir cu-src))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir)))))\n\n;; ========== Step 7: Link binary ==========\n\n(printf \"[7/7] Linking jsh-android binary...~n\")\n\n(let* ([objs (format \"~a/jsh_main_android.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o ~a/jerboa-crypto-shim.o ~a/coreutils-ffi.o\"\n build-dir build-dir build-dir build-dir\n build-dir build-dir build-dir build-dir build-dir build-dir)]\n ;; Rust static libs must be wrapped in --whole-archive so all symbols\n ;; are included (Chez resolves them at runtime via dlsym, not at link time)\n [native-flag (format \" -Wl,--whole-archive ~a -Wl,--no-whole-archive\" native-lib-path)]\n [coreutils-flag (format \" -Wl,--whole-archive ~a -Wl,--no-whole-archive\" rust-coreutils-lib-path)]\n ;; Android/Termux link: Chez static libs + shared Bionic + Rust deps\n ;; libssl removed — TLS now via jerboa_tls_* (rustls)\n ;; libcrypto: still needed by vendor/jerboa-ssh/jerboa_ssh_crypto.c and\n ;; vendor/jerboa-crypto/jerboa_crypto_shim.c (EVP_*, HMAC, scrypt).\n ;; The JERBOA_SSH_NO_OPENSSL fallback requires updated vendor sources which\n ;; live outside git tracking; link dynamically against Termux libcrypto.\n [link-libs (format \"-L~a -lkernel ~a/libz.a ~a/liblz4.a -lm -ldl -lpthread -lutil -liconv -lncursesw -lc++_shared -lunwind -lcrypto\"\n chez-tarm64le chez-tarm64le chez-tarm64le)]\n [link-cmd (format \"~a -pie ~a -rdynamic -o jsh-android ~a~a~a ~a -Wl,--allow-multiple-definition\"\n gcc harden-cflags objs native-flag coreutils-flag link-libs)])\n (run-cmd link-cmd))\n\n;; ========== Hardening ==========\n\n(when (file-exists? \"jsh-android\")\n (printf \"~n[harden] Stripping debug symbols (preserving dynamic symbols)...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-android\"))])\n (system \"strip --strip-debug jsh-android\")\n (let ([post-size (file-length (open-file-input-port \"jsh-android\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n (printf \"[harden] Computing integrity hash...~n\")\n (let ([hash-file (format \"~a/_jsh_hash.txt\" (or (getenv \"TMPDIR\") \"/tmp\"))])\n (system (format \"sha256sum jsh-android | cut -d' ' -f1 | tr -d '\\\\n' > '~a'\" hash-file))\n (let ([hash-hex (call-with-input-file hash-file get-string-all)])\n (system (format \"rm -f '~a'\" hash-file))\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-android.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-android.sha256 (32 bytes)~n\"))))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(for-each (lambda (f) (when (file-exists? f) (delete-file f)))\n '(\"jsh_program.h\" \"jsh_petite_boot.h\" \"jsh_scheme_boot.h\" \"jsh_jsh_boot.h\"\n \"jsh.so\" \"jsh.wpo\" \"jsh.boot\"))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Binary created: jsh-android~n~n\")\n(system \"ls -lh jsh-android\")\n(printf \"~n\")\n(system \"file jsh-android\")\n(printf \"~nTest: ./jsh-android -c 'echo Hello from jsh on Android'~n\")\n"} +{"text":";; FILE: jerboa-shell/build-jsh-android.ss\n#!chezscheme\n;;; build-jsh-android.ss — Build jsh binary on Android/Termux (aarch64, Bionic)\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:<stubs> < build-jsh-android.ss\n;;;\n;;; This script:\n;;; 1. Compiles jsh program with WPO\n;;; 2. Creates libs-only boot file\n;;; 3. Generates C files with embedded boot data\n;;; 4. Compiles C with cc (clang)\n;;; 5. Links binary with libkernel.a (static Chez) + shared Bionic libc\n;;;\n;;; The resulting jsh-android binary is a self-contained ELF for aarch64 Android.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table))\n\n;; Suppress format warnings during compilation (Chez warns about ~<space>\n;; directives in format strings used by jsh code)\n(define (with-warnings-suppressed thunk)\n (with-exception-handler\n (lambda (c) (if (warning? c) (void) (raise-continuable c)))\n thunk))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/data/data/com.termux/files/home\"))\n\n;; All deps are vendored inside the repo\n(define vendor-dir\n (or (getenv \"VENDOR\")\n (format \"~a/vendor\" (current-directory))))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (format \"~a/jerboa/lib\" vendor-dir)))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (format \"~a/jerboa\" vendor-dir)))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (format \"~a/jerboa-ssh/src\" vendor-dir)))\n\n(define jerboa-ssh-shim\n (format \"~a/jerboa-ssh/jerboa_ssh_shim.c\" vendor-dir))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jerboa-sqlite/src\" home-dir)))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls)\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (format \"~a/jerboa-crypto/src\" vendor-dir)))\n\n(define jerboa-crypto-shim\n (format \"~a/jerboa-crypto/jerboa_crypto_shim.c\" vendor-dir))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (format \"~a/jerboa-coreutils/lib\" vendor-dir)))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (format \"~a/jerboa-awk/lib\" vendor-dir)))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (format \"~a/jerboa-sed/lib\" vendor-dir)))\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (format \"~a/jerboa-aws/lib\" vendor-dir)))\n\n(define has-aws? (file-exists? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; Staged vendor directories (compiled .sls→.so by build-jsh-android.sh step 1b)\n(define stage-dir\n (or (getenv \"STAGE\")\n (format \"~a/android-stage\" (current-directory))))\n\n(define stage-jerboa-crypto (format \"~a/jerboa-crypto\" stage-dir))\n(define stage-jerboa-ssh (format \"~a/jerboa-ssh\" stage-dir))\n(define stage-jerboa-aws (format \"~a/jerboa-aws\" stage-dir))\n(define stage-jerboa-fuse (format \"~a/jerboa-fuse\" stage-dir))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/vault.sls\" stage-jerboa-fuse)))\n\n;; Chez Scheme static installation\n(define chez-tarm64le\n (or (getenv \"CHEZ_TARM64LE\")\n (let ([prefix \"/data/data/com.termux/files/usr/lib\"])\n (let ([dirs (directory-list prefix)])\n (let ([csv-dir (find (lambda (d)\n (and (> (string-length d) 3)\n (string=? \"csv\" (substring d 0 3))))\n dirs)])\n (if csv-dir\n (format \"~a/~a/tarm64le\" prefix csv-dir)\n (error 'build \"Cannot find Chez tarm64le directory\")))))))\n\n(define scheme-h-dir chez-tarm64le)\n(define petite-boot-path (format \"~a/petite.boot\" chez-tarm64le))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-tarm64le))\n\n(printf \"Chez static: ~a~n\" chez-tarm64le)\n(printf \"Jerboa: ~a~n\" jerboa-dir)\n(printf \"~n\")\n\n;; Rust native library (libjerboa_native.a — crypto, TLS, integrity, etc.)\n;; Built by build-jsh-android.sh or manually: cd ~/jerboa/jerboa-native-rs && cargo build --release\n(define native-lib-path\n (let ([env-path (getenv \"JERBOA_NATIVE_LIB\")]\n [vendor-path (format \"~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a\" vendor-dir)]\n [home-path (format \"~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a\" home-dir)])\n (cond\n [(and env-path (file-exists? env-path)) env-path]\n [(file-exists? vendor-path) vendor-path]\n [(file-exists? home-path) home-path]\n [else (error 'build-jsh-android\n \"libjerboa_native.a not found. Build it: cd ~/jerboa/jerboa-native-rs && cargo build --release\")])))\n(printf \"Native lib: ~a~n\" native-lib-path)\n\n;; Rust coreutils (libjsh_coreutils.a — ls, cat, grep, etc.)\n;; Built by build-jsh-android.sh or manually: cd rust-coreutils && cargo build --release\n(define rust-coreutils-lib-path\n (let ([env-path (getenv \"JSH_COREUTILS_LIB\")]\n [local-path (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" (current-directory))])\n (cond\n [(and env-path (file-exists? env-path)) env-path]\n [(file-exists? local-path) local-path]\n [else (error 'build-jsh-android\n \"libjsh_coreutils.a not found. Build it: cd rust-coreutils && cargo build --release\")])))\n(printf \"Coreutils: ~a~n\" rust-coreutils-lib-path)\n\n;; ========== Helper functions ==========\n\n(define (file->c-header input-path output-path array-name size-name)\n (let* ([port (open-file-input-port input-path)]\n [data (get-bytevector-all port)]\n [size (bytevector-length data)])\n (close-port port)\n (call-with-output-file output-path\n (lambda (out)\n (fprintf out \"/* Auto-generated — do not edit */~n\")\n (fprintf out \"static const unsigned char ~a[] = {~n\" array-name)\n (let loop ([i 0])\n (when (< i size)\n (when (= 0 (modulo i 16)) (fprintf out \" \"))\n (fprintf out \"0x~2,'0x\" (bytevector-u8-ref data i))\n (when (< (+ i 1) size) (fprintf out \",\"))\n (when (= 15 (modulo i 16)) (fprintf out \"~n\"))\n (loop (+ i 1))))\n (fprintf out \"~n};~n\")\n (fprintf out \"static const unsigned int ~a = ~a;~n\" size-name size))\n 'replace)\n (printf \" ~a: ~a bytes~n\" output-path size)))\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-android \"Command failed\" cmd)))\n\n(define (existing-sos dir modules)\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" dir m)) modules)))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 1: Compile jsh program ==========\n\n;; Generate jsh-generated.ss from jsh.ss (injecting the feature manifest).\n(unless (file-exists? \"jsh-generated.ss\")\n (let ([source-file \"jsh.ss\"])\n (printf \" Generating jsh-generated.ss from ~a~n\" source-file)\n (unless (file-exists? source-file)\n (error 'build-jsh-android \"Program source not found\" source-file))\n (let ([text (call-with-input-file source-file get-string-all)])\n (call-with-output-file \"jsh-generated.ss\"\n (lambda (out) (display text out))\n 'replace))))\n\n(printf \"[1/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(with-warnings-suppressed\n (lambda ()\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 3])\n (compile-program \"jsh-generated.ss\"))))\n\n(unless (file-exists? \"jsh.so\")\n (fprintf (current-error-port) \"FATAL: jsh.so not created~n\")\n (exit 1))\n\n;; ========== Step 2: Skip WPO (use jsh.so directly) ==========\n\n(printf \"[2/7] Using jsh.so (skipping WPO for Android build)...~n\")\n(define program-so \"jsh.so\")\n\n;; ========== Step 3: Create libs-only boot file ==========\n\n(printf \"[3/7] Creating libs-only boot file...~n\")\n\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (append\n ;; Jerboa runtime + stdlib\n (existing-sos jerboa-dir\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\"\n \"std/regex\"\n \"std/match2\" \"std/sugar\" \"std/result\"\n \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\" \"std/typed\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\"\n \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\" \"std/text/json\"\n \"std/net/tcp\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; Local compat layer\n (filter file-exists? (list \"src/compat/gambit.so\"))\n ;; Coreutils shim\n (filter file-exists? (list \"src/jsh/coreutils-shim.so\"))\n ;; jerboa-crypto (compiled in staging dir)\n (existing-sos stage-jerboa-crypto '(\"jerboa-crypto\"))\n ;; jerboa-ssh (sub-libraries must come before main jerboa-ssh.so)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" stage-jerboa-ssh))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" stage-jerboa-ssh m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" stage-jerboa-ssh)))\n '())\n ;; jsqlite is pure Jerboa and is compiled through normal imports.\n ;; jerboa-coreutils\n (existing-sos coreutils-dir\n '(\"jerboa-coreutils/common\" \"jerboa-coreutils/common/version\"\n \"jerboa-coreutils/common/security\"\n \"jerboa-coreutils/basename\" \"jerboa-coreutils/dirname\"\n \"jerboa-coreutils/link\" \"jerboa-coreutils/unlink\"\n \"jerboa-coreutils/yes\" \"jerboa-coreutils/printenv\"\n \"jerboa-coreutils/sleep\" \"jerboa-coreutils/whoami\"\n \"jerboa-coreutils/logname\" \"jerboa-coreutils/hostname\"\n \"jerboa-coreutils/nproc\" \"jerboa-coreutils/tty\"\n \"jerboa-coreutils/sync\" \"jerboa-coreutils/hostid\"\n \"jerboa-coreutils/cat\" \"jerboa-coreutils/head\"\n \"jerboa-coreutils/tail\" \"jerboa-coreutils/tac\"\n \"jerboa-coreutils/tee\" \"jerboa-coreutils/wc\"\n \"jerboa-coreutils/nl\" \"jerboa-coreutils/fold\"\n \"jerboa-coreutils/expand\" \"jerboa-coreutils/unexpand\"\n \"jerboa-coreutils/fmt\"\n \"jerboa-coreutils/cut\" \"jerboa-coreutils/paste\"\n \"jerboa-coreutils/join\" \"jerboa-coreutils/comm\"\n \"jerboa-coreutils/sort\" \"jerboa-coreutils/uniq\"\n \"jerboa-coreutils/tr\" \"jerboa-coreutils/numfmt\"\n \"jerboa-coreutils/mkdir\" \"jerboa-coreutils/rmdir\"\n \"jerboa-coreutils/mktemp\" \"jerboa-coreutils/touch\"\n \"jerboa-coreutils/readlink\" \"jerboa-coreutils/realpath\"\n \"jerboa-coreutils/ln\" \"jerboa-coreutils/cp\"\n \"jerboa-coreutils/mv\" \"jerboa-coreutils/rm\"\n \"jerboa-coreutils/install\" \"jerboa-coreutils/shred\"\n \"jerboa-coreutils/ls\" \"jerboa-coreutils/chmod\"\n \"jerboa-coreutils/chown\" \"jerboa-coreutils/chgrp\"\n \"jerboa-coreutils/stat\" \"jerboa-coreutils/du\"\n \"jerboa-coreutils/df\" \"jerboa-coreutils/pathchk\"\n \"jerboa-coreutils/date\" \"jerboa-coreutils/id\"\n \"jerboa-coreutils/groups\" \"jerboa-coreutils/who\"\n \"jerboa-coreutils/users\" \"jerboa-coreutils/pinky\"\n \"jerboa-coreutils/uptime\" \"jerboa-coreutils/uname\"\n \"jerboa-coreutils/arch\"\n \"jerboa-coreutils/seq\" \"jerboa-coreutils/expr\"\n \"jerboa-coreutils/basenc\" \"jerboa-coreutils/base64\"\n \"jerboa-coreutils/base32\" \"jerboa-coreutils/od\"\n \"jerboa-coreutils/cksum\" \"jerboa-coreutils/md5sum\"\n \"jerboa-coreutils/sha1sum\" \"jerboa-coreutils/sha224sum\"\n \"jerboa-coreutils/sha256sum\" \"jerboa-coreutils/sha384sum\"\n \"jerboa-coreutils/sha512sum\" \"jerboa-coreutils/b2sum\"\n \"jerboa-coreutils/sum\"\n \"jerboa-coreutils/env\" \"jerboa-coreutils/timeout\"\n \"jerboa-coreutils/nice\" \"jerboa-coreutils/nohup\"\n \"jerboa-coreutils/chroot\" \"jerboa-coreutils/stdbuf\"\n \"jerboa-coreutils/truncate\" \"jerboa-coreutils/mkfifo\"\n \"jerboa-coreutils/mknod\" \"jerboa-coreutils/split\"\n \"jerboa-coreutils/csplit\" \"jerboa-coreutils/dd\"\n \"jerboa-coreutils/dircolors\"\n \"jerboa-coreutils/tsort\" \"jerboa-coreutils/shuf\"\n \"jerboa-coreutils/factor\" \"jerboa-coreutils/pr\"\n \"jerboa-coreutils/ptx\" \"jerboa-coreutils/stty\"\n \"jerboa-coreutils/chcon\" \"jerboa-coreutils/runcon\"\n \"jerboa-coreutils/dir\" \"jerboa-coreutils/vdir\"\n \"jerboa-coreutils/rev\"\n \"jerboa-coreutils/grep/pcre2\" \"jerboa-coreutils/grep\"))\n ;; jerboa-awk\n (existing-sos awk-dir\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed\n (existing-sos sed-dir\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-aws (compiled in staging dir)\n (if has-aws?\n (existing-sos stage-jerboa-aws\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\"\n \"jerboa-aws/time\" \"jerboa-aws/crypto\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/creds\" \"jerboa-aws/request\"\n \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jerboa-fuse (vault) — if available\n (if has-jerboa-fuse?\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" stage-jerboa-fuse m))\n '(\"chez/vault/format\"\n \"chez/fuse/constants\" \"chez/fuse/types\" \"chez/fuse/mount\"\n \"chez/fuse/codec\" \"chez/fuse/secmem\" \"chez/fuse/access\"\n \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\")))\n '())\n ;; jsh modules\n (existing-sos \"src/jsh\"\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\"\n \"mux-transport\" \"mux-relay\" \"mux-router\"\n \"mux-server\" \"mux-client\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"config\" \"startup\" \"sandbox\"\n \"rl\" \"limits\" \"harden\" \"main\"\n \"coreutils\"))))\n\n;; ========== Step 4: Generate C headers with embedded data ==========\n\n(printf \"[4/7] Embedding boot files + program as C headers...~n\")\n(file->c-header program-so \"jsh_program.h\"\n \"jsh_program_data\" \"jsh_program_data_len\")\n(file->c-header petite-boot-path \"jsh_petite_boot.h\"\n \"petite_boot_data\" \"petite_boot_size\")\n(file->c-header scheme-boot-path \"jsh_scheme_boot.h\"\n \"scheme_boot_data\" \"scheme_boot_size\")\n(file->c-header \"jsh.boot\" \"jsh_jsh_boot.h\"\n \"jsh_boot_data\" \"jsh_boot_size\")\n\n;; ========== Step 5: Generate static_boot.c and main C ==========\n\n(printf \"[5/7] Generating C source files...~n\")\n\n(define build-dir\n (format \"~a/jsh-android-build\" (or (getenv \"TMPDIR\") \"/tmp\")))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(define program-c (format \"~a/jsh_main_android.c\" build-dir))\n\n(define gcc \"cc\")\n(define harden-cflags \"-fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2\")\n\n;; static_boot.c — registers embedded boot files with Chez\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"jsh_petite_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_scheme_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_jsh_boot.h\\\"\\n\" out)\n (display \"\\nvoid static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", (void *)petite_boot_data, petite_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", (void *)scheme_boot_data, scheme_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", (void *)jsh_boot_data, jsh_boot_size);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; jsh_main_android.c — main entry point\n(call-with-output-file program-c\n (lambda (out)\n (display \"#define _GNU_SOURCE\\n\" out)\n (display \"#include <stdio.h>\\n#include <stdlib.h>\\n#include <string.h>\\n\" out)\n (display \"#include <unistd.h>\\n#include <fcntl.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"jsh_program.h\\\"\\n\\n\" out)\n (display \"extern void static_boot_init(void);\\n\" out)\n (display \"extern void register_ffi_symbols(void);\\n\\n\" out)\n\n ;; Forward declarations for FFI\n (display \"/* FFI forward declarations */\\n\" out)\n (display \"extern void ffi_ensure_std_fds(void);\\n\\n\" out)\n\n ;; register_ffi_symbols — pre-register all FFI symbols for static builds\n (display \"void register_ffi_symbols(void) {\\n\" out)\n\n ;; Android/Termux: not a fully static build — symbols resolved via dlopen at runtime.\n ;; We don't need to pre-register POSIX or FFI symbols since the binary is dynamically linked.\n ;; Only register symbols that the Scheme code looks up by name via foreign-procedure.\n (display \" /* Android: dynamically linked — most symbols resolved via dlopen */\\n\" out)\n (display \"}\\n\\n\" out)\n\n ;; main\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args as env vars\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Resolve exe path via /proc/self/exe\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" ssize_t len = readlink(\\\"/proc/self/exe\\\", exe_buf, sizeof(exe_buf) - 1);\\n\" out)\n (display \" if (len > 0) { exe_buf[len] = '\\\\0'; setenv(\\\"JSH_EXE\\\", exe_buf, 1); }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; Load program via temp file (memfd /proc/self/fd paths are blocked by SELinux on Android)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" char prog_path[4096];\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-prog-XXXXXX\\\", tmpdir);\\n\" out)\n (display \" int fd = mkstemp(prog_path);\\n\" out)\n (display \" if (fd < 0) { perror(\\\"mkstemp\\\"); return 1; }\\n\" out)\n (display \" if (write(fd, jsh_program_data, jsh_program_data_len) != (ssize_t)jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"write tmpfile\\\"); close(fd); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" close(fd);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Copy C headers to build dir\n(system (format \"cp jsh_program.h jsh_petite_boot.h jsh_scheme_boot.h jsh_jsh_boot.h '~a/'\" build-dir))\n\n;; Generate Android compat header (explicit_bzero not in Bionic's <string.h>)\n(call-with-output-file (format \"~a/android-compat.h\" build-dir)\n (lambda (out)\n (display \"#ifndef ANDROID_COMPAT_H\\n#define ANDROID_COMPAT_H\\n\" out)\n (display \"#ifndef _GNU_SOURCE\\n#define _GNU_SOURCE\\n#endif\\n\" out)\n (display \"#include <string.h>\\n\" out)\n ;; explicit_bzero is not in Bionic's <string.h> on Termux —\n ;; provide an inline fallback unconditionally when missing.\n (display \"#if defined(__ANDROID__)\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n (display \"static inline void explicit_bzero(void *b, size_t l) {\\n\" out)\n (display \" memset(b, 0, l);\\n\" out)\n (display \" __asm__ __volatile__(\\\"\\\" ::: \\\"memory\\\");\\n\" out)\n (display \"}\\n\" out)\n (display \"#endif\\n\" out)\n (display \"#endif\\n\" out)))\n 'replace)\n\n(define android-compat (format \"-include '~a/android-compat.h'\" build-dir))\n\n;; ========== Step 6: Compile C with clang ==========\n\n(printf \"[6/7] Compiling C with clang...~n\")\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags android-compat scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_android.c\n(run-cmd (format \"~a -c -O2 ~a ~a -I'~a' -o '~a/jsh_main_android.o' '~a'\"\n gcc harden-cflags android-compat scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags android-compat build-dir))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; Landlock: ffi_landlock_* provided by ffi-shim.c (returns -1 if syscall unavailable)\n;; jerboa_landlock_* provided by libjerboa_native.a\n;; No separate shim needed.\n\n;; jerboa-ssh shim\n;; -DCHEZ_SSH_NO_OPENSSL on jerboa_ssh_shim.c: use standalone ed25519 from Rust\n;; (libjerboa_native.a provides ed25519_*_standalone symbols)\n;; jerboa_ssh_crypto.c is compiled separately as jerboa-ssh-crypto.o; it still uses\n;; OpenSSL EVP for SSH transport crypto (HMAC/SHA/X25519/ChaCha20-Poly1305).\n;; Termux ships libcrypto.so/libssl.so so we link against those system libs.\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; -DCHEZ_SSH_NO_OPENSSL: use standalone ed25519/AES from Rust, not OpenSSL\n (run-cmd (format \"~a -c -O2 ~a ~a -DCHEZ_SSH_NO_OPENSSL -I'~a/jerboa-ssh' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (let ([crypto-src (format \"~a/jerboa-ssh/jerboa_ssh_crypto.c\" vendor-dir)])\n (if (file-exists? crypto-src)\n ;; -I. for embed-crypto.h (project root)\n (run-cmd (format \"~a -c -O2 ~a ~a -I'~a/jerboa-ssh' -I'~a' -o '~a/ed25519-standalone.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir (current-directory) build-dir crypto-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))))\n (let ([bcrypt-src (format \"~a/jerboa-ssh/bcrypt_pbkdf.c\" vendor-dir)])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a ~a -I'~a/jerboa-ssh' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir)))))\n (begin\n (printf \" Warning: jerboa-ssh shim not found, building without SSH agent~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-crypto shim\n(if (file-exists? jerboa-crypto-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/jerboa-crypto-shim.o' '~a' -Wall\"\n gcc harden-cflags build-dir jerboa-crypto-shim))\n (begin\n (printf \" Warning: jerboa-crypto shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-crypto-shim.o' -\" gcc build-dir))))\n\n;; coreutils FFI shim\n(let ([cu-src (format \"~a/jerboa-coreutils/support/libcoreutils.c\" vendor-dir)])\n (if (file-exists? cu-src)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir cu-src))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir)))))\n\n;; ========== Step 7: Link binary ==========\n\n(printf \"[7/7] Linking jsh-android binary...~n\")\n\n(let* ([objs (format \"~a/jsh_main_android.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o ~a/jerboa-crypto-shim.o ~a/coreutils-ffi.o\"\n build-dir build-dir build-dir build-dir\n build-dir build-dir build-dir build-dir build-dir build-dir)]\n ;; Rust static libs must be wrapped in --whole-archive so all symbols\n ;; are included (Chez resolves them at runtime via dlsym, not at link time)\n [native-flag (format \" -Wl,--whole-archive ~a -Wl,--no-whole-archive\" native-lib-path)]\n [coreutils-flag (format \" -Wl,--whole-archive ~a -Wl,--no-whole-archive\" rust-coreutils-lib-path)]\n ;; Android/Termux link: Chez static libs + shared Bionic + Rust deps\n ;; libssl removed — TLS now via jerboa_tls_* (rustls)\n ;; libcrypto: still needed by vendor/jerboa-ssh/jerboa_ssh_crypto.c and\n ;; vendor/jerboa-crypto/jerboa_crypto_shim.c (EVP_*, HMAC, scrypt).\n ;; The JERBOA_SSH_NO_OPENSSL fallback requires updated vendor sources which\n ;; live outside git tracking; link dynamically against Termux libcrypto.\n [link-libs (format \"-L~a -lkernel ~a/libz.a ~a/liblz4.a -lm -ldl -lpthread -lutil -liconv -lncursesw -lc++_shared -lunwind -lcrypto\"\n chez-tarm64le chez-tarm64le chez-tarm64le)]\n [link-cmd (format \"~a -pie ~a -rdynamic -o jsh-android ~a~a~a ~a -Wl,--allow-multiple-definition\"\n gcc harden-cflags objs native-flag coreutils-flag link-libs)])\n (run-cmd link-cmd))\n\n;; ========== Hardening ==========\n\n(when (file-exists? \"jsh-android\")\n (printf \"~n[harden] Stripping debug symbols (preserving dynamic symbols)...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-android\"))])\n (system \"strip --strip-debug jsh-android\")\n (let ([post-size (file-length (open-file-input-port \"jsh-android\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n (printf \"[harden] Computing integrity hash...~n\")\n (let ([hash-file (format \"~a/_jsh_hash.txt\" (or (getenv \"TMPDIR\") \"/tmp\"))])\n (system (format \"sha256sum jsh-android | cut -d' ' -f1 | tr -d '\\\\n' > '~a'\" hash-file))\n (let ([hash-hex (call-with-input-file hash-file get-string-all)])\n (system (format \"rm -f '~a'\" hash-file))\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-android.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-android.sha256 (32 bytes)~n\"))))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(for-each (lambda (f) (when (file-exists? f) (delete-file f)))\n '(\"jsh_program.h\" \"jsh_petite_boot.h\" \"jsh_scheme_boot.h\" \"jsh_jsh_boot.h\"\n \"jsh.so\" \"jsh.wpo\" \"jsh.boot\"))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Binary created: jsh-android~n~n\")\n(system \"ls -lh jsh-android\")\n(printf \"~n\")\n(system \"file jsh-android\")\n(printf \"~nTest: ./jsh-android -c 'echo Hello from jsh on Android'~n\")\n"} {"text":";; FILE: jerboa-shell/functions.ss\n;;; functions.ss — Shell functions and aliases for gsh\n\n(export #t)\n(import :std/sugar\n :std/format\n :std/iter\n :jsh/ast\n :jsh/environment\n :jsh/ffi)\n\n;;; --- Shell functions ---\n\n(defstruct shell-function (name body redirections lineno source-file) transparent: #t)\n\n;; Define a shell function\n(def (function-define! env name body (redirections []) (lineno #f) (source-file #f))\n (hash-put! (shell-environment-functions env) name\n (make-shell-function name body redirections lineno source-file)))\n\n;; Look up a function by name\n(def (function-lookup env name)\n (hash-get (shell-environment-functions env) name))\n\n;; Unset a function\n(def (function-unset! env name)\n (hash-remove! (shell-environment-functions env) name))\n\n;; List all function names\n(def (function-list env)\n (hash-keys (shell-environment-functions env)))\n\n;; Call a function with arguments\n;; execute-fn is the executor callback to avoid circular dependency\n;; Returns exit status\n(def (function-call func args env execute-fn)\n (let* ((child-env (env-push-scope env))\n ;; Set positional parameters\n (_ (env-set-positional! child-env args))\n ;; Set FUNCNAME\n (_ (env-set! child-env \"FUNCNAME\" (shell-function-name func))))\n ;; Execute the function body\n ;; break/continue/return are handled by the caller\n (let ((status\n (with-catch\n (lambda (e)\n (if (return-exception? e)\n (return-exception-status e)\n (raise e)))\n (lambda ()\n (let ((s (execute-fn (shell-function-body func) child-env)))\n ;; Copy last-status back to parent\n (env-set-last-status! env s)\n s)))))\n ;; Clean up exported locals: when a local variable was exported in the\n ;; child scope, restore the OS environment to match the parent scope\n (cleanup-exported-locals! child-env env)\n status)))\n\n;; After function return, restore OS environment for variables that were\n;; local+exported in the child scope but shouldn't persist in parent\n(def (cleanup-exported-locals! child-env parent-env)\n (for-each\n (lambda (pair)\n (let ((name (car pair))\n (var (cdr pair)))\n (when (and (shell-var-local? var) (shell-var-exported? var))\n ;; Check if parent scope has this variable\n (let ((parent-var (env-get-raw-var parent-env name)))\n (if (and parent-var (shell-var-exported? parent-var))\n ;; Parent has it exported — restore parent's value\n (let ((v (shell-var-scalar-value parent-var)))\n (if v (setenv name v) (ffi-unsetenv name)))\n ;; Parent doesn't have it exported — remove from OS env\n (ffi-unsetenv name))))))\n (hash->list (shell-environment-vars child-env))))\n\n;;; --- Return exception ---\n;; Used to implement 'return' from functions\n\n(defstruct return-exception (status) transparent: #t)\n\n(def (shell-return! (status 0))\n (raise (make-return-exception status)))\n\n;;; --- Break/Continue exceptions ---\n;; Used to implement 'break' and 'continue' in loops\n\n(defstruct break-exception (levels) transparent: #t)\n(defstruct continue-exception (levels) transparent: #t)\n\n;; Track loop nesting depth — break/continue are only valid inside loops\n(def *loop-depth* (make-parameter 0))\n\n(def (shell-break! (levels 1))\n (if (> (*loop-depth*) 0)\n (raise (make-break-exception levels))\n (begin\n (fprintf (current-error-port) \"break: only meaningful in a `for', `while', or `until' loop~n\")\n ;; In bash, break/continue outside a loop just warns (doesn't abort subshell)\n 0)))\n\n(def (shell-continue! (levels 1))\n (if (> (*loop-depth*) 0)\n (raise (make-continue-exception levels))\n (begin\n (fprintf (current-error-port) \"continue: only meaningful in a `for', `while', or `until' loop~n\")\n ;; In bash, break/continue outside a loop just warns (doesn't abort subshell)\n 0)))\n\n;;; --- Errexit exception ---\n;; Raised when set -e is active and a command fails outside a condition context\n\n(defstruct errexit-exception (status) transparent: #t)\n\n;;; --- Nounset exception ---\n;; Raised when set -u is active and an unbound variable is referenced\n\n(defstruct nounset-exception (status) transparent: #t)\n\n;;; --- Subshell exit exception ---\n;; Raised by `exit` builtin when running inside a subshell\n\n(defstruct subshell-exit-exception (status) transparent: #t)\n\n;;; --- Aliases ---\n\n;; Set an alias\n(def (alias-set! env name value)\n (hash-put! (shell-environment-aliases env) name value))\n\n;; Get an alias value\n(def (alias-get env name)\n (hash-get (shell-environment-aliases env) name))\n\n;; Remove an alias\n(def (alias-unset! env name)\n (hash-remove! (shell-environment-aliases env) name))\n\n;; Remove all aliases\n(def (alias-clear! env)\n ;; Replace with new empty table\n (for-each\n (lambda (pair)\n (hash-remove! (shell-environment-aliases env) (car pair)))\n (hash->list (shell-environment-aliases env))))\n\n;; List all aliases as alist\n(def (alias-list env)\n (hash->list (shell-environment-aliases env)))\n\n;; Expand aliases in a word (first word of simple command)\n;; Returns expanded string or #f if no alias\n(def (alias-expand env word)\n (let ((val (alias-get env word)))\n (if val\n ;; If alias ends with space, the next word should also be checked\n val\n #f)))\n\n;; Check if alias value ends with space (triggers next-word expansion)\n(def (alias-continues? value)\n (and (> (string-length value) 0)\n (char=? (string-ref value (- (string-length value) 1)) #\\space)))\n"} {"text":";; FILE: jerboa-shell/build-jsh-freebsd-cross.ss\n#!chezscheme\n;;; build-jsh-freebsd-cross.ss — Cross-compile jsh from macOS arm64 to FreeBSD 14 amd64\n;;;\n;;; Usage:\n;;; JERBOA_HOME=/Users/user/mine/jerboa scheme --libdirs <libs> \\\n;;; --script build-jsh-freebsd-cross.ss\n;;;\n;;; Sibling to build-jsh-cross.ss (Linux x86_64 musl). Uses:\n;;; - $JERBOA_HOME/.chez-cross-ta6fb/ — cross-built Chez install for FreeBSD\n;;; - $JERBOA_HOME/build/chez/xc-ta6fb/s/xpatch — host compiler → ta6fb emit\n;;; - $JERBOA_HOME/support/cross-cc-freebsd-amd64 — macOS clang+lld wrapper\n;;;\n;;; Produces: jsh-freebsd-amd64 (dynamic FreeBSD x86_64 ELF, depends on\n;;; libc.so.7, libm.so, libthr.so, libutil.so from the host system)\n;;;\n;;; WPO-as-program pattern, same as build-jsh-cross.ss.\n\n(import (chezscheme))\n\n;; ── Params ──────────────────────────────────────────────────────────────────\n(define jerboa-home\n (or (getenv \"JERBOA_HOME\") \"/Users/user/mine/jerboa\"))\n\n(define cross-prefix (format \"~a/.chez-cross-ta6fb\" jerboa-home))\n(define xpatch (format \"~a/build/chez/xc-ta6fb/s/xpatch\" jerboa-home))\n(define cross-cc (or (getenv \"CROSS_CC\")\n (format \"~a/support/cross-cc-freebsd-amd64\" jerboa-home)))\n\n(define output \"jsh-freebsd-amd64\")\n(define source-script \"jsh.ss\")\n(define entry-script \"jsh-generated.ss\")\n(define ffi-shim \"ffi-shim.c\")\n\n;; Feature resolution — derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; jerboa-native Rust static lib — FreeBSD cross-built.\n(define jerboa-native-a\n (or (getenv \"JERBOA_NATIVE_A\")\n (format \"~a/jerboa-native-rs/target/x86_64-unknown-freebsd/release/libjerboa_native.a\"\n jerboa-home)))\n\n(define cross-csv-dir\n (let ([lib (format \"~a/lib\" cross-prefix)])\n (unless (file-directory? lib)\n (error 'build-jsh-freebsd-cross \"cross prefix lib dir missing\" lib))\n (let* ([entries (directory-list lib)]\n [csvs (filter (lambda (e)\n (and (>= (string-length e) 3)\n (string=? (substring e 0 3) \"csv\")))\n entries)])\n (when (null? csvs)\n (error 'build-jsh-freebsd-cross \"no csv* in cross lib\" lib))\n (format \"~a/~a/ta6fb\" lib (car csvs)))))\n\n(define (require-file p)\n (unless (file-exists? p)\n (error 'build-jsh-freebsd-cross \"missing file\" p)))\n\n(require-file xpatch)\n(require-file (format \"~a/libkernel.a\" cross-csv-dir))\n(require-file (format \"~a/scheme.h\" cross-csv-dir))\n(require-file (format \"~a/petite.boot\" cross-csv-dir))\n(require-file (format \"~a/scheme.boot\" cross-csv-dir))\n(require-file source-script)\n(require-file ffi-shim)\n(require-file jerboa-native-a)\n\n(printf \"==> build-jsh-freebsd-cross~n\")\n(printf \" JERBOA_HOME: ~a~n\" jerboa-home)\n(printf \" cross csv-dir: ~a~n\" cross-csv-dir)\n(printf \" xpatch: ~a~n\" xpatch)\n(printf \" cross-cc: ~a~n\" cross-cc)\n(printf \" output: ~a~n\" output)\n(printf \"~n\")\n\n;; ── Stage 1: load xpatch (target=ta6fb emit mode) ──────────────────────────\n(define orig-libdirs (library-directories))\n(printf \"==> [1/6] loading xpatch (compiler -> ta6fb emit mode)~n\")\n(load xpatch)\n(library-directories orig-libdirs)\n\n(compile-imported-libraries #t)\n(generate-wpo-files #t)\n\n;; ── Stage 1.5: apply vendor patches ────────────────────────────────────────\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE compile-program so the WPO pulls the patched\n;; source (allow-proxy is imported transitively, not via libs-to-bundle).\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss \"vendor/jerboa/lib/std/net/allow-proxy.ss\"]\n [ap-sls \"vendor/jerboa/lib/std/net/allow-proxy.sls\"]\n [ap-so \"vendor/jerboa/lib/std/net/allow-proxy.so\"]\n [ap-wpo \"vendor/jerboa/lib/std/net/allow-proxy.wpo\"])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \"==> [1.5/6] applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n;; ── Stage 2: generate jsh-generated.ss with feature manifest ───────────────\n;; jsh.ss carries a placeholder `(define *jsh-enabled-features* '())`.\n;; jsh-generate.ss rewrites that line based on *enabled-features* so the\n;; runtime `,features` command reports what was actually built.\n(printf \"==> [2/6] generate-jsh-program (features: ~a)~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"==> [2.5/6] compile-program ~a~n\" entry-script)\n(compile-program entry-script)\n\n;; ── Stage 3: compile-whole-program → wpo .so ───────────────────────────────\n(define wpo-output (string-append output \".wp.so\"))\n(printf \"==> [3/6] compile-whole-program jsh-generated.wpo -> ~a~n\" wpo-output)\n(compile-whole-program \"jsh-generated.wpo\" wpo-output #t)\n\n;; ── Stage 3.5: ensure lazy-imported libs are compiled, build jsh-libs.boot ─\n(define jerboa-lib-dir \"vendor/jerboa/lib\")\n(define libs-to-bundle\n '(\"jerboa/core\"\n \"jerboa/runtime\"\n \"std/error/conditions\"\n \"std/error\"\n \"std/format\"\n \"std/sort\"\n \"std/pregexp\"\n \"std/sugar\"\n \"std/misc/alist\"\n \"std/misc/string\"\n \"std/misc/string-more\"\n \"std/misc/list\"\n \"std/misc/lru-cache\"\n \"std/misc/trie\"\n \"std/misc/thread\"\n \"std/text/glob\"\n \"std/os/path\"\n \"std/os/platform\"\n \"std/os/posix\"\n \"std/os/limits\"\n \"std/os/supervise\"\n \"std/os/limits/sandbox\"\n \"std/os/tracefs\"\n \"std/net/allowlist\"))\n\n(printf \"==> [3.5/6] fill in missing .so files for boot bundle~n\")\n(for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" jerboa-lib-dir m)]\n [so (format \"~a/~a.so\" jerboa-lib-dir m)])\n (cond\n [(file-exists? so)\n (printf \" keep ~a~n\" so)]\n [(file-exists? sls)\n (printf \" compile-library ~a (missing .so)~n\" sls)\n (compile-library sls)]\n [else\n (printf \" skip (no .sls): ~a~n\" sls)])))\n libs-to-bundle)\n\n(define libs-boot \"jsh-libs.boot\")\n(define libs-boot-inputs\n (let loop ([ms libs-to-bundle] [acc '()])\n (cond\n [(null? ms) (reverse acc)]\n [else\n (let ([so (format \"~a/~a.so\" jerboa-lib-dir (car ms))])\n (loop (cdr ms) (if (file-exists? so) (cons so acc) acc)))])))\n(printf \" make-boot-file ~a (~a libs)~n\" libs-boot (length libs-boot-inputs))\n(apply make-boot-file libs-boot '(\"petite\" \"scheme\") libs-boot-inputs)\n\n;; ── Stage 4: write helpers ─────────────────────────────────────────────────\n(define (embed-as-c-array in-path var-name out-path)\n (let* ([bv (call-with-port (open-file-input-port in-path) get-bytevector-all)]\n [n (bytevector-length bv)])\n (call-with-port (open-file-output-port out-path\n (file-options no-fail)\n (buffer-mode block)\n (native-transcoder))\n (lambda (out)\n (display (format \"static const unsigned char ~a[] = {\\n\" var-name) out)\n (let loop ([i 0])\n (when (< i n)\n (display (format \"0x~2,'0x,\" (bytevector-u8-ref bv i)) out)\n (when (= (mod (+ i 1) 16) 0) (newline out))\n (loop (+ i 1))))\n (when (positive? n) (newline out))\n (display \"};\\n\" out)\n (display (format \"static const unsigned int ~a_size = sizeof(~a);\\n\"\n var-name var-name)\n out)))\n (printf \" embed ~a (~a bytes) -> ~a~n\" in-path n out-path)))\n\n(printf \"==> [4/6] embed boot files + program as C arrays~n\")\n(embed-as-c-array (format \"~a/petite.boot\" cross-csv-dir) \"petite_boot\" \"petite_boot.h\")\n(embed-as-c-array (format \"~a/scheme.boot\" cross-csv-dir) \"scheme_boot\" \"scheme_boot.h\")\n(embed-as-c-array libs-boot \"jsh_libs_boot\" \"jsh_libs_boot.h\")\n(embed-as-c-array wpo-output \"jsh_program\" \"jsh_program.h\")\n\n;; ── Stage 5: generate main.c (FreeBSD flavor) ──────────────────────────────\n(define main-c-path (string-append output \"-main.c\"))\n\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([line (get-line p)])\n (cond\n [(eof-object? line) (reverse acc)]\n [(or (zero? (string-length line))\n (char=? (string-ref line 0) #\\;)\n (char=? (string-ref line 0) #\\#))\n (loop acc)]\n [else (loop (cons line acc))]))))))\n\n(define ffi-shim-symbols\n (read-symbol-list \"ffi-shim-symbols.list\"))\n\n;; libjerboa_native.a exports for x86_64-unknown-freebsd.\n;; Differs from the Linux list — Linux-only symbols (epoll, inotify, eventfd,\n;; landlock, seccomp) are not built on FreeBSD and are emitted as C stubs\n;; in freebsd-stub-symbols below.\n(define jerboa-native-symbols\n '(\"jerboa_aead_open\" \"jerboa_aead_seal\"\n \"jerboa_antidebug_check_all\" \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_check_ld_preload\" \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_ptrace\" \"jerboa_antidebug_timing_check\"\n \"jerboa_aproc_close\" \"jerboa_aproc_dup\" \"jerboa_aproc_killpg\"\n \"jerboa_aproc_set_nonblock\" \"jerboa_aproc_spawn\" \"jerboa_aproc_spawn_pty\"\n \"jerboa_aproc_wait4\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_chacha20_open\" \"jerboa_chacha20_seal\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_freebsd_is_traced\" \"jerboa_freebsd_process_count\"\n \"jerboa_hkdf_sha256\" \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_integrity_hash_file\" \"jerboa_integrity_hash_region\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_sign_verify\"\n \"jerboa_integrity_verify_hash\"\n \"jerboa_kill_probe\"\n \"jerboa_last_error\" \"jerboa_md5\" \"jerboa_mlockall\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_prctl_set_name\" \"jerboa_proc_self_exe\"\n \"jerboa_random_bytes\"\n \"jerboa_regex_captures\" \"jerboa_regex_compile\" \"jerboa_regex_compile_ex\"\n \"jerboa_regex_find\" \"jerboa_regex_find_at\" \"jerboa_regex_free\"\n \"jerboa_regex_group_count\" \"jerboa_regex_is_match\" \"jerboa_regex_replace_all\"\n \"jerboa_scrypt\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_random_fill\"\n \"jerboa_secure_wipe\"\n \"jerboa_setproctitle\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_start\"\n \"jerboa_socks5_server_stats\" \"jerboa_socks5_server_stop\"\n \"jerboa_timing_safe_equal\"\n \"jerboa_x25519_diffie_hellman\" \"jerboa_x25519_generate_keypair\"\n \"jerboa_x25519_public_from_private\"))\n\n;; Linux-only jerboa_* symbols — emit returning-error C stubs on FreeBSD.\n;; Mirrors build-jsh-freebsd.ss's pattern.\n(define freebsd-stub-symbols\n '(\"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_eventfd_create\" \"jerboa_eventfd_drain\" \"jerboa_eventfd_signal\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_create_ruleset\"\n \"jerboa_landlock_add_path_rule\" \"jerboa_landlock_add_net_rule\"\n \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n\n;; POSIX libc functions called directly via foreign-procedure (no shim).\n;; Note: __errno_location is Linux glibc — replaced by freebsd_errno_location\n;; wrapper below (which also satisfies FreeBSD's __error).\n(define posix-symbols\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"setpriority\"\n \"socket\" \"bind\" \"setsockopt\" \"getsockname\"\n \"htons\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"\n \"flock\" \"fsync\" \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"pread\" \"pwrite\" \"readlink\" \"realpath\" \"strerror\"\n \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"))\n\n(define posix-wrapped-symbols '(\"open\" \"fcntl\" \"mkfifo\" \"umask\" \"mkdir\"))\n\n;; Same weak-stub list as the Linux cross — symbols Scheme may dlsym at runtime\n;; but that are not statically linked into this build.\n(define weak-stub-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"\n \"jerboa_ssl_init\" \"jerboa_ssl_cleanup\"\n \"jerboa_ssl_connect\" \"jerboa_ssl_write\" \"jerboa_ssl_read\"\n \"jerboa_ssl_read_all\" \"jerboa_ssl_free_buf\" \"jerboa_ssl_close\"\n \"jerboa_ssl_memcpy\"\n \"jerboa_tcp_listen\" \"jerboa_tcp_accept\"\n \"jerboa_tcp_connect\" \"jerboa_tcp_close\"\n \"jerboa_tcp_read\" \"jerboa_tcp_write\" \"jerboa_tcp_read_all\"\n \"jerboa_tcp_set_timeout\"\n \"jerboa_ssl_server_ctx\" \"jerboa_ssl_server_accept\" \"jerboa_ssl_server_ctx_free\"\n \"jerboa_tcp_conn_wrap\" \"jerboa_conn_write\" \"jerboa_conn_read\"\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"\n \"jsh_coreutils_init\"\n \"jsh_ls\" \"jsh_dir\" \"jsh_vdir\" \"jsh_stat\" \"jsh_du\" \"jsh_df\"\n \"jsh_dircolors\" \"jsh_pathchk\"\n \"jsh_cat\" \"jsh_cp\" \"jsh_mv\" \"jsh_rm\" \"jsh_ln\"\n \"jsh_mkdir\" \"jsh_rmdir\" \"jsh_mktemp\" \"jsh_touch\"\n \"jsh_link\" \"jsh_unlink\" \"jsh_readlink\" \"jsh_cu_realpath\"\n \"jsh_install\" \"jsh_shred\" \"jsh_truncate\" \"jsh_mkfifo\" \"jsh_mknod\" \"jsh_dd\"\n \"jsh_chmod\" \"jsh_chown\" \"jsh_chgrp\"\n \"jsh_head\" \"jsh_tail\" \"jsh_tac\" \"jsh_tee\" \"jsh_wc\" \"jsh_nl\"\n \"jsh_fold\" \"jsh_expand\" \"jsh_unexpand\" \"jsh_fmt\"\n \"jsh_cut\" \"jsh_paste\" \"jsh_join\" \"jsh_comm\"\n \"jsh_sort\" \"jsh_uniq\" \"jsh_tr\" \"jsh_numfmt\"\n \"jsh_grep\"\n \"jsh_id\" \"jsh_whoami\" \"jsh_hostname\" \"jsh_uname\" \"jsh_uptime\"\n \"jsh_who\" \"jsh_groups\" \"jsh_users\" \"jsh_pinky\" \"jsh_logname\"\n \"jsh_arch\" \"jsh_nproc\" \"jsh_tty\" \"jsh_hostid\" \"jsh_date\"\n \"jsh_seq\" \"jsh_expr\" \"jsh_factor\"\n \"jsh_base64\" \"jsh_base32\" \"jsh_basenc\" \"jsh_od\"\n \"jsh_cksum\" \"jsh_md5sum\" \"jsh_sha1sum\" \"jsh_sha224sum\"\n \"jsh_sha256sum\" \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_b2sum\" \"jsh_sum\"\n \"jsh_env\" \"jsh_timeout\" \"jsh_nice\" \"jsh_nohup\" \"jsh_chroot\"\n \"jsh_kill\"\n \"jsh_echo\" \"jsh_printf\" \"jsh_sleep\" \"jsh_yes\" \"jsh_printenv\"\n \"jsh_pwd\" \"jsh_sync\" \"jsh_test\" \"jsh_shuf\" \"jsh_split\" \"jsh_csplit\"\n \"jsh_tsort\" \"jsh_stty\" \"jsh_pr\" \"jsh_ptx\"\n \"jsh_basename\" \"jsh_dirname\"\n \"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\"\n \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\"\n \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\" \"jerboa_x509_cert_fingerprint\"\n \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\"\n \"pcre2_compile_8\" \"pcre2_match_8\"\n \"pcre2_match_data_create_from_pattern_8\" \"pcre2_match_data_free_8\"\n \"pcre2_get_ovector_pointer_8\" \"pcre2_get_ovector_count_8\"\n \"pcre2_code_free_8\"\n \"EVP_CIPHER_CTX_ctrl\" \"EVP_CIPHER_CTX_free\" \"EVP_CIPHER_CTX_new\"\n \"EVP_DecryptFinal_ex\" \"EVP_DecryptInit_ex\" \"EVP_DecryptUpdate\"\n \"EVP_EncryptFinal_ex\" \"EVP_EncryptInit_ex\" \"EVP_EncryptUpdate\"\n \"EVP_aes_256_gcm\" \"EVP_sha256\" \"PKCS5_PBKDF2_HMAC\" \"RAND_bytes\"\n \"jerboa_ssh_aes256_ctr_free\" \"jerboa_ssh_aes256_ctr_init\"\n \"jerboa_ssh_aes256_ctr_process\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_ed25519_derive_pubkey\" \"jerboa_ssh_ed25519_sign\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_hmac_sha256\"\n \"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\"\n \"jerboa_ssh_tcp_accept\" \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_connect\"\n \"jerboa_ssh_tcp_listen\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_set_nodelay\"\n \"jerboa_ssh_tcp_write\"\n \"coreutils_gid_to_name\" \"coreutils_uid_to_name\"\n \"coreutils_ls_lstat\" \"coreutils_ls_readlink\" \"coreutils_ls_stat_get\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_terminal_height\" \"coreutils_terminal_width\"\n \"coreutils_time_format\"\n \"sandbox_init\" \"sandbox_free_error\"))\n\n(define (emit-c out)\n (display \"/* Generated by build-jsh-freebsd-cross.ss — do not edit by hand. */\\n\" out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"petite_boot.h\\\"\\n\" out)\n (display \"#include \\\"scheme_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_libs_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_program.h\\\"\\n\\n\" out)\n ;; Dynamic-linked FreeBSD binary: keep libc's real dlopen/dlsym/dlerror.\n ;; (load-shared-object #f) returns the main exe handle, dlsym(RTLD_DEFAULT, ...)\n ;; finds symbols thanks to -Wl,--export-dynamic on the final link.\n ;; FreeBSD errno compatibility: __errno_location is glibc; __error is FreeBSD libc.\n ;; Define a wrapper that returns &errno and register it under both names.\n (display \"/* FreeBSD errno compatibility */\\n\" out)\n (display \"static int *freebsd_errno_location(void) { return &errno; }\\n\\n\" out)\n ;; extern decls for ffi-shim.c symbols\n (display \"/* ffi-shim.c — auto-generated from ffi-shim-symbols.list */\\n\" out)\n (for-each (lambda (n) (fprintf out \"extern void ~a();\\n\" n)) ffi-shim-symbols)\n ;; extern decls for libjerboa_native.a symbols (those that exist for FreeBSD)\n (display \"\\n/* libjerboa_native.a — crypto feature on (FreeBSD subset) */\\n\" out)\n (for-each (lambda (n) (fprintf out \"extern void ~a();\\n\" n)) jerboa-native-symbols)\n ;; Linux-only jerboa_* — C stubs returning -1.\n (display \"\\n/* Linux-only jerboa_* — stubs return -1 on FreeBSD */\\n\" out)\n (for-each (lambda (n)\n (fprintf out \"static int ~a() { return -1; }\\n\" n))\n freebsd-stub-symbols)\n ;; POSIX wrappers\n (display \"\\n/* Wrappers for variadic/macro POSIX */\\n\" out)\n (display \"static int wrap_open(const char *p, int f, int m) { return open(p, f, m); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int c, int a) { return fcntl(fd, c, a); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *p, int m) { return mkfifo(p, (mode_t)m); }\\n\" out)\n (display \"static int wrap_umask(int m) { return (int)umask((mode_t)m); }\\n\" out)\n (display \"static int wrap_mkdir(const char *p, int m) { return mkdir(p, (mode_t)m); }\\n\\n\" out)\n ;; weak stubs for symbols not linked into the cross build\n (display \"/* Weak stubs for symbols not linked into cross build */\\n\" out)\n (for-each (lambda (n)\n (fprintf out\n \"__attribute__((weak)) long ~a() { fprintf(stderr, \\\"[jsh-freebsd-amd64-weak] ~a\\\\n\\\"); fflush(stderr); return 0; }\\n\"\n n n))\n weak-stub-symbols)\n ;; embed_* — defined in ffi-shim.c via embed-crypto.h\n (display \"\\n/* embed_* — defined in ffi-shim.c via embed-crypto.h */\\n\" out)\n (display \"extern void embed_encrypt();\\n\" out)\n (display \"extern void embed_decrypt();\\n\" out)\n (display \"extern void embed_pbkdf2_sha256();\\n\" out)\n (display \"extern void embed_random_bytes();\\n\" out)\n (display \"extern void embed_read_passphrase();\\n\" out)\n ;; register all symbols at startup\n (newline out)\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n ffi-shim-symbols)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n jerboa-native-symbols)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n freebsd-stub-symbols)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n posix-symbols)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" n n))\n posix-wrapped-symbols)\n ;; FreeBSD errno: register under both glibc and libc names\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)freebsd_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)freebsd_errno_location);\\n\" out)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n weak-stub-symbols)\n (display \" Sforeign_symbol(\\\"embed_encrypt\\\", (void*)embed_encrypt);\\n\" out)\n (display \" Sforeign_symbol(\\\"embed_decrypt\\\", (void*)embed_decrypt);\\n\" out)\n (display \" Sforeign_symbol(\\\"embed_pbkdf2_sha256\\\", (void*)embed_pbkdf2_sha256);\\n\" out)\n (display \" Sforeign_symbol(\\\"embed_random_bytes\\\", (void*)embed_random_bytes);\\n\" out)\n (display \" Sforeign_symbol(\\\"embed_read_passphrase\\\", (void*)embed_read_passphrase);\\n\" out)\n (display \"}\\n\\n\" out)\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; argv → JSH_ARGn env-var forwarding\n (display \" char countbuf[32];\\n\" out)\n (display \" snprintf(countbuf, sizeof(countbuf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", countbuf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" char name[32];\\n\" out)\n (display \" snprintf(name, sizeof(name), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(name, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Resolve exe path via sysctl (FreeBSD has no /proc/self/exe by default)\n (display \" {\\n\" out)\n (display \" int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" size_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (sysctl(mib, 4, exe_buf, &exe_len, NULL, 0) == 0) {\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; FreeBSD: always use tmpfile for the WPO program. memfd_create exists on\n ;; FreeBSD 13+ but /dev/fd/N requires fdescfs which isn't mounted by default.\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\"); if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-prog-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program, 1, jsh_program_size, fp) != jsh_program_size) {\\n\" out)\n (display \" perror(\\\"fwrite\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n ;; Boot Chez.\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", (void *)petite_boot, petite_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", (void *)scheme_boot, scheme_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh-libs\\\", (void *)jsh_libs_boot, jsh_libs_boot_size);\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n (display \" const char *prog_argv[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_program(prog_path, 1, prog_argv);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n\n(call-with-port (open-file-output-port main-c-path\n (file-options no-fail) (buffer-mode block) (native-transcoder))\n emit-c)\n(printf \"==> [5/6] generated ~a (~a ffi-shim + ~a native + ~a freebsd-stubs + ~a posix + ~a weak)~n\"\n main-c-path\n (length ffi-shim-symbols)\n (length jerboa-native-symbols)\n (length freebsd-stub-symbols)\n (length posix-symbols)\n (length weak-stub-symbols))\n\n;; ── Stage 6: compile + link with cross-cc ──────────────────────────────────\n(printf \"==> [6/6] compile + link with ~a~n\" cross-cc)\n;; FreeBSD differences from Linux musl:\n;; - Dynamic link (no -static): FreeBSD libc uses symbol versioning\n;; (e.g. wait4@FBSD_1.0) that libc.a + libc_nonshared.a can't satisfy.\n;; The standard FreeBSD distribution model is dynamic linking against\n;; the system libc.so.7 / libm.so / libthr.so / libutil.so.\n;; - No -ldl needed (dlopen lives in libc on FreeBSD)\n;; - -lpthread = libthr (FreeBSD's POSIX thread library)\n;; - -lutil for openpty (Rust pty crate)\n;; - -Wl,--export-dynamic so dlsym(RTLD_DEFAULT, ...) finds main-exe symbols\n(define link-cmd\n (format\n \"~a -O2 -Wl,--export-dynamic -I~a -o ~a ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a ~a -lm -lpthread -lutil\"\n cross-cc cross-csv-dir output main-c-path ffi-shim\n cross-csv-dir cross-csv-dir cross-csv-dir jerboa-native-a))\n(printf \" ~a~n\" link-cmd)\n(let ([rc (system link-cmd)])\n (unless (zero? rc)\n (error 'build-jsh-freebsd-cross \"cross-link failed\" rc)))\n\n(printf \"~n=== Build complete: ~a ===~n\" output)\n(system (format \"ls -lh ~a\" output))\n(system (format \"file ~a\" output))\n"} {"text":";; FILE: jerboa-shell/finding.md\n# Jerboa Shell - What's Missing\n\n## Current State\nThe project is a full POSIX shell with:\n- Job control (fg, bg, kill)\n- File descriptor redirection (2>&1, <, >, >>)\n- Command history with arrow key navigation\n- Tab completion for commands and paths\n- Process management (wait, jobs, disown)\n- Signal handling for SIGINT/SIGQUIT\n- Prompt customization (PS1-style prompt variables)\n\n## What's Missing\n\n### 1. Process Substitution\n`<(command)` and `>(command)` syntax is not implemented.\n\n**Why it matters:** This is a bash feature that allows piping to/from processes without temporary files. It's used in advanced shell scripting.\n\n**Effort:** Medium - requires implementing subshell execution with pipe redirection.\n\n### 2. Here Documents\n`cat << EOF` style input is not implemented.\n\n**Why it matters:** This is a standard shell feature for multi-line string input to commands.\n\n**Effort:** Medium - requires parsing the here document delimiter and handling it as input to a command.\n\n### 3. Advanced Redirection\nSome complex redirection patterns may not work (e.g., `command {fd}<file`).\n\n**Why it matters:** Bash supports fd redirection syntax that may not be fully implemented.\n\n**Effort:** Medium - requires testing and implementing missing patterns.\n\n### 4. Bash-style Aliases\nWhile alias/unalias commands exist, they may need refinement.\n\n**Why it matters:** Bash aliases have more features (e.g., alias expansion in command context).\n\n**Effort:** Small - review current implementation and add missing features.\n\n### 5. Process Substitution with Named Pipes\n`<(command)` creates a named pipe that can be read from.\n\n**Why it matters:** This is a core bash feature used in many scripts.\n\n**Effort:** Medium - requires creating named pipes and managing their lifecycle.\n\n### 6. Here String\n`command <<< \"string\"` syntax is not implemented.\n\n**Why it matters:** Another bash feature for providing strings as command input.\n\n**Effort:** Medium - requires parsing the <<< operator and treating the string as stdin.\n\n### 7. Command Substitution with Process Substitution\n`$(command)` inside `<(command2)` or `>(command2)`.\n\n**Why it matters:** Nested command substitution with process substitution.\n\n**Effort:** Medium - requires proper nesting handling.\n\n### 8. Signal Trapping\n`trap 'command' SIGTERM` syntax is not implemented.\n\n**Why it matters:** Bash allows trapping signals with custom handlers.\n\n**Effort:** Medium - requires implementing signal handler registration and execution.\n\n### 9. Arithmetic Expansion\n`$((expression))` syntax is not implemented.\n\n**Why it matters:** Bash arithmetic expansion with variable substitution.\n\n**Effort:** Medium - requires implementing the arithmetic expression parser.\n\n### 10. Parameter Expansion Extensions\n`${var#pattern}`, `${var##pattern}`, `${var%pattern}`, `${var%%pattern}`, `${var:pos}`, `${var:pos:len}`, `${var/pattern/repl}`, etc.\n\n**Why it matters:** Bash parameter expansion is extensive and used heavily in shell scripts.\n\n**Effort:** Large - requires implementing all the parameter expansion variants.\n\n## Recommendations\n\n1. **Start with process substitution** (`<(command)`) - it's the highest-value missing feature\n2. **Add here documents** (`cat << EOF`) - standard shell feature\n3. **Implement signal trapping** (`trap 'command' SIGTERM`) - needed for robust scripts\n4. **Add arithmetic expansion** (`$((1 + 2))`) - used in many scripts\n5. **Add here strings** (`command <<< \"string\"`) - related to here documents\n\nThe shell is already quite sophisticated for a POSIX shell. The gaps are mainly around advanced bash features that users expect.\n"} @@ -2059,7 +2059,7 @@ {"text":";; FILE: jerboa-shell/bench-smp.chez.ss\n#!chezscheme\n(import\n (except (chezscheme) void box box? unbox set-box! andmap\n ormap iota last-pair find \\x31;+ \\x31;- fx/ fx1+ fx1- error\n error? raise with-exception-handler identifier? hash-table?\n make-hash-table filter remove partition fold-right\n path-extension)\n (compat gambit-compat)\n (compat format)\n (compat misc))\n\n(define (fmt-secs s)\n (let ([ms (inexact->exact (round (* s 1000)))])\n (string-append (number->string ms) \"ms\")))\n\n(define (log! msg)\n (display msg (current-error-port))\n (flush-output-port (current-error-port)))\n\n(define (fib n)\n (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))\n\n(define (ack m n)\n (cond\n [(= m 0) (+ n 1)]\n [(= n 0) (ack (- m 1) 1)]\n [else (ack (- m 1) (ack m (- n 1)))]))\n\n(define (tak x y z)\n (if (<= x y)\n z\n (tak (tak (- x 1) y z)\n (tak (- y 1) z x)\n (tak (- z 1) x y))))\n\n(define (collatz-len n)\n (let loop ([n n] [len 0])\n (if (<= n 1)\n len\n (loop\n (if (even? n) (quotient n 2) (+ (* 3 n) 1))\n (+ len 1)))))\n\n(define (collatz-sum limit)\n (let loop ([i 1] [sum 0])\n (if (> i limit)\n sum\n (loop (+ i 1) (+ sum (collatz-len i))))))\n\n(define (matmul-bench size)\n (let ([a (make-vector (* size size) 1.0)]\n [b (make-vector (* size size) 2.0)]\n [c (make-vector (* size size) 0.0)])\n (let loopi ([i 0])\n (when (< i size)\n (let loopj ([j 0])\n (when (< j size)\n (let loopk ([k 0] [sum 0.0])\n (if (< k size)\n (loopk\n (+ k 1)\n (+ sum\n (* (vector-ref a (+ (* i size) k))\n (vector-ref b (+ (* k size) j)))))\n (vector-set! c (+ (* i size) j) sum)))\n (loopj (+ j 1))))\n (loopi (+ i 1))))\n (vector-ref c 0)))\n\n(define num-cores 8)\n\n(define (run-benchmarks-smp)\n (\\x23;\\x23;set-parallelism-level! num-cores)\n (\\x23;\\x23;startup-parallelism!)\n (thread-sleep! 0.1)\n (log!\n (format\n \"\\n=== bench-smp (~a cores, ~a processors) ===\\n\"\n num-cores\n (\\x23;\\x23;current-vm-processor-count)))\n (let ([benchmarks (list (cons \"fib(38) \" (lambda () (fib 38)))\n (cons \"ack(3,10) \" (lambda () (ack 3 10)))\n (cons \"tak(30,20,10)\" (lambda () (tak 30 20 10)))\n (cons\n \"collatz 500k \"\n (lambda () (collatz-sum 500000)))\n (cons\n \"matmul 150 \"\n (lambda () (matmul-bench 150))))])\n (log! \"\\n--- sequential (8x each) ---\\n\")\n (let ([seq-times (map (lambda (b)\n (let ([start (\\x23;\\x23;process-statistics)])\n (let loop ([i 0])\n (when (< i num-cores)\n ((cdr b))\n (loop (+ i 1))))\n (let* ([end (\\x23;\\x23;process-statistics)]\n [wall (- (f64vector-ref end 2)\n (f64vector-ref start 2))])\n (log!\n (format\n \" ~a ~a wall\\n\"\n (car b)\n (fmt-secs wall)))\n (cons (car b) wall))))\n benchmarks)])\n (log! \"\\n--- parallel (8 threads) ---\\n\")\n (let ([par-times (map (lambda (b)\n (let* ([start (\\x23;\\x23;process-statistics)]\n [threads (map (lambda (i)\n (thread-start!\n (make-thread\n (cdr b))))\n (iota num-cores))]\n [_ (for-each thread-join! threads)]\n [end (\\x23;\\x23;process-statistics)]\n [wall (- (f64vector-ref end 2)\n (f64vector-ref start 2))])\n (log!\n (format\n \" ~a ~a wall\\n\"\n (car b)\n (fmt-secs wall)))\n (cons (car b) wall)))\n benchmarks)])\n (log! \"\\n--- speedup ---\\n\")\n (for-each\n (lambda (seq par)\n (let* ([s (cdr seq)]\n [p (cdr par)]\n [speedup (if (> p 0) (/ s p) 0.0)])\n (log! (format \" ~a ~1,1fx\\n\" (car seq) speedup))))\n seq-times\n par-times))))\n [log! \"=== done ===\\n\"])\n\n(run-benchmarks-smp)\n"} {"text":";; FILE: jerboa-shell/tmux.md\n# jsh Multiplexer: tmux-like Session Management\n\n## Overview\n\nAdd tmux-like capabilities directly to jsh so that a single `jsh` process can manage\nmultiple terminal sessions, survive disconnection, and allow re-attachment — all without\nrequiring tmux itself. Users get tmux-style keybindings and session/window/pane management\nas a first-class shell feature.\n\n---\n\n## Two Core Features\n\n### Feature 1: Server/Client Architecture (Detach & Reattach)\n\n```\nTerminal A Background\n┌──────────┐ attach ┌──────────────────┐\n│ jsh │───────────────────→│ jsh --server │\n│ (client) │←───────────────────│ (daemon process) │\n└──────────┘ pty relay │ │\n │ session 0: bash │\nTerminal B │ session 1: vim │\n┌──────────┐ attach │ session 2: htop │\n│ jsh │───────────────────→│ │\n│ (client) │←───────────────────│ │\n└──────────┘ └──────────────────────┘\n```\n\n**Usage:**\n\n```bash\n# Start a new server (daemonizes, survives HUP)\njsh --server # start server, print socket path\njsh --server --name work # named server\njsh --server --password # prompt for password (not echoed)\njsh --server --password-file ~/.jsh-pass # read password from file\n\n# Attach to existing server\njsh --attach # attach to default server\njsh --attach --name work # attach to named server\njsh -A # short form\n# (prompted for password if server requires one)\n\n# List running servers\njsh --list-servers # show active servers + session counts\n\n# Change or remove password on a running server\n# ,password # set/change password (prompted)\n# ,password --clear # remove password requirement\n\n# Detach from inside a session\n# Ctrl-b d # tmux-style keybinding\n# ,detach # meta-command\n```\n\n**Output replay on reconnect.** Each pane keeps a ring of its recent output. On\ndetach or a dropped connection the client prints a resume hint with the byte\noffset it reached; reconnect with `--from OFFSET` to replay output missed while\naway before resuming live:\n\n```\njsh: to resume this stream: ,mux attach HOST:PORT --from 18432\n,mux attach HOST:PORT --from 18432 # (or ,attach --from OFFSET for a local server)\n```\n\n### Feature 2: Multi-Session Multiplexing (Windows & Panes)\n\n```\n┌─────────────────────────────────────────────────┐\n│ [0: jsh] [1: vim]* [2: logs] Ctrl-b ? │\n├────────────────────────┬────────────────────────┤\n│ ~/project │ ~/project │\n│ $ make build │ $ tail -f app.log │\n│ Building... │ [2026-03-19] INFO ... │\n│ Done. │ [2026-03-19] WARN ... │\n│ $ │ │\n│ │ │\n│ │ │\n│ │ │\n├────────────────────────┴────────────────────────┤\n│ [session: work] [window: 1/3] [pane: 0] │\n└─────────────────────────────────────────────────┘\n```\n\n**Keybindings (tmux-compatible prefix: Ctrl-b):**\n\n| Key | Action |\n|-----|--------|\n| `Ctrl-b c` | Create new window |\n| `Ctrl-b C` | Create new sudo `jsh` window |\n| `Ctrl-b n` / `Ctrl-b p` | Next / previous window |\n| `Ctrl-b 0-9` | Switch to window N |\n| `Ctrl-b %` | Split pane vertically |\n| `Ctrl-b \"` | Split pane horizontally |\n| `Ctrl-b S` | Split pane with sudo `jsh` |\n| `Ctrl-b o` | Cycle to next pane |\n| `Ctrl-b Arrow` | Move to pane in direction |\n| `Ctrl-b x` | Kill current pane |\n| `Ctrl-b &` | Kill current window |\n| `Ctrl-b d` | Detach from server |\n| `Ctrl-b W` | Save session (layout + scrollback) to snapshot `default` |\n| `Ctrl-b E` | Restore snapshot `default` (fresh shells, scrollback replayed) |\n| `Ctrl-b z` | Toggle pane zoom (fullscreen) |\n| `Ctrl-b [` | Enter scroll/copy mode |\n| `Ctrl-b ]` | Paste from copy buffer |\n| `Ctrl-b ,` | Rename current window |\n| `Ctrl-b w` | List windows (interactive chooser) |\n| `Ctrl-b s` | List sessions (interactive chooser) |\n| `Ctrl-b :` | Command prompt (like tmux command mode) |\n| `Ctrl-b ?` | Show keybinding help |\n\nSudo panes run sudo inside the pane PTY, so sudo prompts there directly. By\ndefault they pass the invoking user's `JSH_EMBED_OVERLAY_DIR` (or\n`$HOME/.jsh/embed/`) through `/usr/bin/env` and chown new overlay files back to\nthe invoking uid/gid. Set `JSH_MUX_SUDO_SHARE_EMBED=0` to use root's overlay\ninstead. Set `JSH_MUX_SUDO_FORCE_PROMPT=1` to add `sudo -k`,\n`JSH_MUX_SUDO_TARGET=/path/to/jsh` to override the target binary, or\n`JSH_MUX_ENV_COMMAND=/path/to/env` if `/usr/bin/env` is not correct.\n\n---\n\n## Architecture\n\n### Component Diagram\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ jsh Server Process │\n│ (setsid, daemonized) │\n│ │\n│ ┌──────────────────────────────────────────────────┐ │\n│ │ Session Manager │ │\n│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │\n│ │ │ Session 0│ │ Session 1│ │ Session 2│ ... │ │\n│ │ │ │ │ │ │ │ │ │\n│ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │\n│ │ │ │Win 0 │ │ │ │Win 0 │ │ │ │Win 0 │ │ │ │\n│ │ │ │┌────┐│ │ │ │┌────┐│ │ │ │┌────┐│ │ │ │\n│ │ │ ││Pane││ │ │ ││Pane││ │ │ ││Pane││ │ │ │\n│ │ │ ││PTY ││ │ │ ││PTY ││ │ │ ││PTY ││ │ │ │\n│ │ │ │└────┘│ │ │ │└────┘│ │ │ │└────┘│ │ │ │\n│ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ │\n│ │ └──────────┘ └──────────┘ └──────────┘ │ │\n│ └──────────────────────────────────────────────────┘ │\n│ │ │\n│ ┌───────────────────────┴──────────────────────────┐ │\n│ │ Client Connection Manager │ │\n│ │ Unix socket: /run/user/$UID/jsh/default.sock │ │\n│ └──────────────────────────────────────────────────┘ │\n│ │ │\n└──────────────────────────┼──────────────────────────────┘\n │ Unix domain socket\n ┌────────────┼────────────────┐\n │ │ │\n ┌────┴────┐ ┌────┴────┐ ┌────┴────┐\n │Client A │ │Client B │ │Client C │\n │(terminal│ │(terminal│ │(terminal│\n │attached)│ │attached)│ │attached)│\n └─────────┘ └─────────┘ └─────────┘\n```\n\n### Data Structures\n\n```scheme\n;; A session groups related windows\n(defstruct session\n id: fixnum ; unique session id\n name: string ; human-readable name\n windows: (list-of window)\n active-window: fixnum ; index into windows\n created: fixnum ; timestamp\n env: hashtable) ; session-level environment overrides\n\n;; A window contains one or more panes in a layout\n(defstruct window\n id: fixnum\n name: string ; user-settable, or auto from running command\n panes: (list-of pane)\n active-pane: fixnum\n layout: layout) ; how panes are arranged\n\n;; A pane is a single shell instance backed by a PTY\n(defstruct pane\n id: fixnum\n pty-master: fixnum ; master fd\n pty-slave: fixnum ; slave fd (held for lifecycle)\n pid: fixnum ; child shell process pid\n cols: fixnum ; pane width in columns\n rows: fixnum ; pane height in rows\n x: fixnum ; pane origin column in window\n y: fixnum ; pane origin row in window\n scrollback: bytevector ; ring buffer of output history\n scroll-pos: fixnum ; current scroll position (for copy mode)\n title: string ; pane title (from OSC escape or command)\n zoomed?: boolean) ; is this pane in zoom mode?\n\n;; Layout tree for splitting\n(defstruct layout\n type: symbol ; 'horizontal | 'vertical | 'leaf\n ratio: flonum ; split ratio (0.0-1.0)\n children: (or pane (list-of layout)))\n\n;; Server authentication state\n(defstruct server-auth\n enabled?: boolean ; is password required?\n password-hash: (or #f bytevector) ; Argon2id hash of password\n salt: (or #f bytevector) ; 16-byte random salt\n max-attempts: fixnum ; max failed attempts before lockout (default 5)\n lockout-sec: fixnum ; lockout duration in seconds (default 60)\n failed-ips: hashtable) ; peer-cred uid → (attempts . last-fail-time)\n\n;; Client connection state\n(defstruct client\n fd: fixnum ; Unix socket fd\n session-id: fixnum ; which session this client is viewing\n cols: fixnum ; client terminal width\n rows: fixnum ; client terminal height\n attached?: boolean ; is client actively attached?\n authenticated?: boolean ; has client passed auth challenge?\n tx-cipher: (or #f cipher-state) ; encrypt outgoing (server→client)\n rx-cipher: (or #f cipher-state)) ; decrypt incoming (client→server)\n\n;; Per-direction encryption state (ChaCha20-Poly1305)\n(defstruct cipher-state\n key: bytevector ; 32-byte symmetric key\n nonce-counter: fixnum) ; monotonic counter, starts at 0\n```\n\n### Wire Protocol (Client <-> Server)\n\nBinary message framing over Unix domain socket:\n\n```\n┌──────────┬──────────┬──────────────────────┐\n│ Type (1B)│ Len (4B) │ Payload (variable) │\n└──────────┴──────────┴──────────────────────┘\n```\n\n**Message Types:**\n\n| Type | Direction | Payload | Purpose |\n|------|-----------|---------|---------|\n| `0x01` ATTACH | C→S | `{session_name, cols, rows}` | Attach to / create session |\n| `0x02` DETACH | C→S | (empty) | Graceful detach |\n| `0x03` INPUT | C→S | raw bytes | Keyboard input from client |\n| `0x04` RESIZE | C→S | `{cols, rows}` | Client terminal resized |\n| `0x05` OUTPUT | S→C | raw bytes | PTY output to display |\n| `0x06` REDRAW | S→C | full screen bytes | Complete screen redraw |\n| `0x07` CMD | C→S | `{command, args}` | Multiplexer command (new-window, etc.) |\n| `0x08` STATUS | S→C | `{windows, active, pane_info}` | Status bar update |\n| `0x09` ERROR | S→C | error string | Error message |\n| `0x0A` PING | C→S | (empty) | Keepalive |\n| `0x0B` PONG | S→C | (empty) | Keepalive response |\n| `0x0C` SESSION_LIST | S→C | `[{id, name, windows, attached}]` | Response to list command |\n| `0x10` AUTH_REQUIRED | S→C | `{salt, challenge}` | Server requires authentication |\n| `0x11` AUTH_RESPONSE | C→S | `{proof}` | Client sends password proof |\n| `0x12` AUTH_OK | S→C | (empty) | Authentication succeeded |\n| `0x13` AUTH_FAIL | S→C | `{reason, retry_after_sec}` | Authentication failed |\n| `0x14` AUTH_CHANGE | C→S | `{old_proof, new_hash, new_salt}` | Change password (from attached client) |\n| `0x15` KEY_EXCHANGE | C→S | `{client_pubkey}` | Ephemeral X25519 public key (no-password encryption) |\n\n---\n\n## Implementation Plan\n\n### Phase 0: PTY Support in FFI (Foundation)\n\n**Goal:** Add pseudoterminal allocation to `ffi-shim.c` so the server can create\nPTY pairs for each pane.\n\n**New FFI functions:**\n\n```c\n// Allocate a new PTY pair, returns master fd (slave path via ptsname)\nint ffi_pty_open(void); // posix_openpt + grantpt + unlockpt\nchar* ffi_pty_slave_name(int master_fd); // ptsname(master_fd)\nint ffi_pty_open_slave(int master_fd); // open(ptsname(master_fd))\nvoid ffi_pty_set_size(int fd, int cols, int rows); // ioctl TIOCSWINSZ\nvoid ffi_pty_get_size(int fd, int* cols, int* rows); // ioctl TIOCGWINSZ\n\n// Daemonization\nint ffi_daemonize(void); // fork, setsid, fork, chdir(\"/\"), close fds\nint ffi_write_pidfile(const char* path); // write getpid() to file\n```\n\n**Files modified:**\n- `ffi-shim.c` — add PTY and daemon functions\n- `src/jsh/ffi.sls` — add Scheme bindings\n\n**Tests:**\n- Allocate PTY, write to master, read from slave\n- Set/get PTY size\n- Fork child into PTY slave, verify I/O\n\n**Estimated complexity:** Small. ~150 lines of C, ~50 lines of Scheme bindings.\n\n---\n\n### Phase 1: Server Daemon Mode\n\n**Goal:** `jsh --server` starts a background daemon listening on a Unix socket.\n\n**Startup sequence:**\n\n```\njsh --server [--name NAME] [--password | --password-file PATH]\n │\n ├─ 1. Check for existing server (socket file exists + responsive)\n │ → if exists and alive: print error, exit\n │ → if exists and dead: remove stale socket, continue\n │\n ├─ 1.5. If --password or --password-file:\n │ Read/prompt for password, hash with Argon2id,\n │ write NAME.auth (mode 0600), zero password from memory\n │\n ├─ 2. Daemonize (double-fork, setsid, close stdio)\n │\n ├─ 3. Write PID file: /run/user/$UID/jsh/NAME.pid\n │\n ├─ 4. Create Unix socket: /run/user/$UID/jsh/NAME.sock\n │ (fallback: ~/.jsh/NAME.sock if /run/user not available)\n │ Socket directory mode 0700 (owner-only access)\n │\n ├─ 5. Install signal handlers:\n │ SIGTERM → graceful shutdown (notify clients, kill children)\n │ SIGCHLD → reap children, update pane status\n │ SIGHUP → ignored (daemon must survive)\n │ SIGPIPE → ignored (client may disconnect)\n │\n ├─ 6. Create default session with one window and one pane\n │ (allocate PTY, fork child jsh into it)\n │\n └─ 7. Enter event loop:\n select/poll on: server socket + all client fds + all PTY master fds\n ├─ New client connection → accept, run auth handshake (if enabled), then ATTACH\n ├─ Client input → route to active pane's PTY master\n ├─ PTY output → broadcast to all clients viewing that pane\n ├─ Client disconnect → mark detached, keep session alive\n └─ PTY EOF → pane died, clean up, maybe close window\n```\n\n**Socket path convention:**\n```\n/run/user/$UID/jsh/ # XDG_RUNTIME_DIR based (mode 0700)\n├── default.sock # default server\n├── default.pid\n├── default.auth # password hash + salt (only if --password)\n├── work.sock # named server \"work\"\n├── work.pid\n├── work.auth # (only if --password)\n└── ...\n```\n\n**Files:**\n- New: `src/jsh/server.sls` — server event loop, client management\n- New: `src/jsh/mux-protocol.sls` — wire protocol encode/decode\n- Modified: `src/jsh/main.sls` — add `--server`, `--attach`, `--list-servers` arg parsing\n\n**Estimated complexity:** Medium-large. Core event loop + socket management.\n\n---\n\n### Phase 1.5: Authentication\n\n**Goal:** Optionally require a password to attach to a server. Prevent unauthorized\naccess when the socket is reachable (shared machines, forwarded sockets, etc.).\n\n#### Design Principles\n\n1. **Opt-in** — no password by default (Unix socket permissions are sufficient for\n single-user machines). Password adds defense-in-depth.\n2. **No plaintext on wire** — challenge-response protocol so the password never\n travels over the socket, even in cleartext Unix domain socket scenarios.\n3. **Encrypted channel** — after authentication, all traffic is encrypted with\n ChaCha20-Poly1305 using a session key derived from the auth handshake. Even\n without a password, clients and server can negotiate an encrypted channel.\n4. **Brute-force resistant** — Argon2id for password hashing, rate limiting on\n failed attempts, exponential backoff with lockout.\n5. **No stored plaintext** — server stores only the Argon2id hash + salt, never\n the password itself. Hash file is mode 0600.\n6. **Changeable at runtime** — password can be set, changed, or cleared while\n the server is running via meta-command from an authenticated session.\n\n#### Authentication Flow\n\n```\nClient Server\n │ │\n ├─── connect() ───────────────────────────────→ │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Server checks: auth enabled? │ │\n │ │ No → send AUTH_OK, skip to ATTACH │ │\n │ │ Yes → generate challenge, send it │ │\n │ └──────────────────────────────────────┘ │\n │ │\n │ ←── AUTH_REQUIRED {salt, challenge} ────────── │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Client prompts user for password │ │\n │ │ (canonical mode, no echo, on stderr) │ │\n │ │ │ │\n │ │ Compute: │ │\n │ │ key = Argon2id(password, salt) │ │\n │ │ proof = HMAC-SHA256(key, challenge) │ │\n │ └──────────────────────────────────────┘ │\n │ │\n ├─── AUTH_RESPONSE {proof} ─────────────────→ │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Server computes expected proof: │ │\n │ │ key = stored_hash (already Argon2id)│ │\n │ │ expected = HMAC-SHA256(key, challenge) │\n │ │ │ │\n │ │ constant-time compare proof vs expected│ │\n │ │ Match → AUTH_OK │ │\n │ │ Mismatch → AUTH_FAIL + rate limit │ │\n │ └──────────────────────────────────────┘ │\n │ │\n │ ←── AUTH_OK {server_nonce} ───────────────── │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Both sides derive session key: │ │\n │ │ session_key = HMAC-SHA256(key, │ │\n │ │ challenge ‖ server_nonce) │ │\n │ │ │ │\n │ │ All subsequent messages encrypted │ │\n │ │ with ChaCha20-Poly1305 using │ │\n │ │ session_key + per-message nonce │ │\n │ └──────────────────────────────────────┘ │\n │ │\n ├─── ATTACH {session, cols, rows} ──────────→ │\n │ (encrypted from here on) │\n```\n\n**No-password mode with encryption:**\n\nWhen the server has no password set, the channel can still be encrypted using\nan ephemeral key exchange:\n\n```\nClient Server\n │ │\n ├─── connect() ───────────────────────────────→ │\n │ │\n │ ←── AUTH_OK {server_nonce, server_pubkey} ─── │\n │ (no password required) │\n │ │\n ├─── KEY_EXCHANGE {client_pubkey} ────────────→ │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Both sides compute: │ │\n │ │ shared = X25519(my_priv, their_pub) │ │\n │ │ session_key = HMAC-SHA256(shared, │ │\n │ │ server_nonce ‖ client_pubkey │ │\n │ │ ‖ server_pubkey) │ │\n │ │ │ │\n │ │ Encrypted channel established │ │\n │ └──────────────────────────────────────┘ │\n │ │\n ├─── ATTACH {session, cols, rows} ──────────→ │\n │ (encrypted from here on) │\n```\n\nThis gives encryption even without authentication — protecting against passive\neavesdropping on the socket (e.g., root sniffing with `socat`, forwarded sockets).\nIt does **not** protect against active MITM (no identity verification without a\npassword). To disable encryption entirely (micro-optimization for trusted local\nsockets), use `JSH_MUX_ENCRYPT=off`.\n\n#### Challenge-Response Detail\n\nThe server never sends or receives the raw password. Instead:\n\n1. **Server stores:** `Argon2id(password, salt)` → 32-byte `key`, plus the 16-byte `salt`\n2. **On connect:** Server generates a random 32-byte `challenge` (one-time nonce)\n3. **Client computes:** `key = Argon2id(password, salt)` then `proof = HMAC-SHA256(key, challenge)`\n4. **Server computes:** `expected = HMAC-SHA256(stored_key, challenge)` and compares\n5. **Timing-safe comparison:** `ffi_constant_time_compare(proof, expected, 32)`\n\nThis ensures:\n- Password never crosses the wire (only the HMAC proof does)\n- Replay attacks fail (each challenge is a fresh nonce)\n- Offline brute-force requires cracking Argon2id\n\n#### Encrypted Channel (Post-Auth)\n\nOnce authentication succeeds (or key exchange completes in no-password mode),\nall subsequent wire protocol messages are wrapped in ChaCha20-Poly1305 AEAD:\n\n```\nPlaintext message (Type + Len + Payload):\n┌──────────┬──────────┬──────────────────────┐\n│ Type (1B)│ Len (4B) │ Payload (variable) │\n└──────────┴──────────┴──────────────────────┘\n\nEncrypted on wire:\n┌────────────┬──────────────────────────────────┬──────────┐\n│ Nonce (12B)│ Ciphertext (5B + payload) │ Tag (16B)│\n└────────────┴──────────────────────────────────┴──────────┘\n ↑ ChaCha20-Poly1305 encrypts ↑ authenticates\n Type+Len+Payload nonce+ciphertext\n```\n\n**Nonce management:**\n\nEach direction (client→server, server→client) maintains an independent 64-bit\nmessage counter starting at 0, encoded as a 12-byte little-endian nonce\n(4 zero bytes prefix + 8-byte counter). The counter increments after every\nmessage. This avoids nonce reuse without coordination.\n\n```scheme\n;; Per-direction encryption state\n(defstruct cipher-state\n key: bytevector ; 32-byte ChaCha20-Poly1305 key\n nonce-counter: fixnum) ; monotonically increasing, starts at 0\n\n;; Encrypt a message\n(define (encrypt-message state type payload)\n (let* ((nonce (counter->nonce (cipher-state-nonce-counter state)))\n (plaintext (encode-message type payload))\n (ciphertext (chacha20-poly1305-encrypt\n (cipher-state-key state) nonce plaintext)))\n (set! (cipher-state-nonce-counter state)\n (+ 1 (cipher-state-nonce-counter state)))\n (bytevector-append nonce ciphertext)))\n\n;; Decrypt a message\n(define (decrypt-message state encrypted)\n (let* ((nonce (bytevector-slice encrypted 0 12))\n (expected-nonce (counter->nonce (cipher-state-nonce-counter state)))\n (_ (unless (bytevector=? nonce expected-nonce)\n (error \"nonce mismatch — replay or reorder detected\")))\n (ciphertext (bytevector-slice encrypted 12))\n (plaintext (chacha20-poly1305-decrypt\n (cipher-state-key state) nonce ciphertext)))\n (set! (cipher-state-nonce-counter state)\n (+ 1 (cipher-state-nonce-counter state)))\n (decode-message plaintext)))\n```\n\n**Session key derivation (password mode):**\n\nBoth sides already share `key` (the Argon2id hash) and `challenge`. The server\ngenerates a fresh `server_nonce` and includes it in `AUTH_OK`. The session key\nis derived deterministically:\n\n```\nsession_key = HMAC-SHA256(key, challenge ‖ server_nonce ‖ \"jsh-mux-v1\")\n```\n\nTwo directional keys are derived from this:\n\n```\nclient_to_server_key = HMAC-SHA256(session_key, \"client-to-server\")\nserver_to_client_key = HMAC-SHA256(session_key, \"server-to-client\")\n```\n\nUsing separate keys per direction prevents reflection attacks.\n\n**Session key derivation (no-password mode):**\n\nX25519 key agreement produces a 32-byte shared secret. The session key is:\n\n```\nsession_key = HMAC-SHA256(shared_secret,\n server_nonce ‖ client_pubkey ‖ server_pubkey ‖ \"jsh-mux-v1\")\n```\n\nSame directional key split as above.\n\n**Performance:**\n\nChaCha20-Poly1305 is extremely fast (~3 GB/s on modern CPUs without hardware\nAES). For terminal I/O (typically <1 MB/s even under heavy `cat /dev/urandom`),\nthe overhead is negligible — under 1% CPU.\n\n#### Password Storage\n\n```\n/run/user/$UID/jsh/NAME.auth (mode 0600)\n```\n\nFormat (binary):\n```\n┌──────────┬────────────┬───────────────────────┐\n│ Salt(16B)│ Argon2id │ Hash (32B) │\n│ │ params(12B)│ │\n└──────────┴────────────┴───────────────────────┘\n```\n\nArgon2id parameters: `t=3, m=65536 (64MB), p=1` — tuned for interactive\nlatency (~0.5s on modern hardware). Stored alongside the hash so the client\ncan use matching parameters.\n\n#### Rate Limiting & Lockout\n\n```\nPer connecting UID (via SO_PEERCRED on Unix sockets):\n\n Attempt 1-3: immediate response\n Attempt 4: 1 second delay\n Attempt 5: AUTH_FAIL + lockout for 60 seconds\n After lockout: counter resets\n\nServer logs failed attempts to stderr (if --verbose) or syslog.\n```\n\nThe lockout state is tracked in `server-auth.failed-ips` (keyed by peer UID\nfrom `SO_PEERCRED`). This prevents a local user from brute-forcing the password\nbut doesn't penalize legitimate users on failed typos excessively.\n\n#### Server-Side Setup\n\n```\njsh --server --password\n │\n ├─ 1. Prompt: \"Set server password: \" (no echo, canonical mode)\n ├─ 2. Prompt: \"Confirm password: \" (no echo)\n ├─ 3. If mismatch → error, exit\n ├─ 4. Generate 16-byte random salt (getrandom/urandom)\n ├─ 5. Compute hash = Argon2id(password, salt, t=3, m=64MB, p=1)\n ├─ 6. Zero password from memory (explicit_bzero)\n ├─ 7. Write {salt, params, hash} to NAME.auth (mode 0600)\n └─ 8. Continue normal server startup\n\njsh --server --password-file PATH\n │\n ├─ 1. Read first line of PATH as password (trim newline)\n ├─ 2. Same steps 4-8 as above\n └─ 3. Zero file contents from memory\n```\n\n#### Client-Side Auth\n\n```\njsh --attach [--name NAME]\n │\n ├─ 1. Connect to socket\n ├─ 2. Receive first message:\n │ ├─ AUTH_OK → no password needed, proceed to attach\n │ └─ AUTH_REQUIRED {salt, challenge} → need password\n │\n ├─ 3. If AUTH_REQUIRED:\n │ ├─ If stdin is a TTY:\n │ │ Prompt: \"Password: \" (no echo, on stderr so pipes work)\n │ ├─ If stdin is not a TTY:\n │ │ Read from JSH_MUX_PASSWORD env var\n │ │ Or read from --password-file if given\n │ │ Or error: \"password required but no TTY\"\n │ │\n │ ├─ Compute key = Argon2id(password, salt)\n │ ├─ Compute proof = HMAC-SHA256(key, challenge)\n │ ├─ Zero password + key from memory\n │ ├─ Send AUTH_RESPONSE {proof}\n │ │\n │ ├─ Receive response:\n │ │ ├─ AUTH_OK → proceed to attach\n │ │ └─ AUTH_FAIL {reason, retry_after} →\n │ │ print error, wait retry_after, re-prompt (up to 3 tries)\n │ │\n │ └─ After 3 client-side failures → exit 1\n │\n └─ 4. Send ATTACH message (normal flow)\n```\n\n#### Runtime Password Management\n\nFrom an already-authenticated session:\n\n```\n,password # set or change password\n ├─ Prompt: \"New password: \" (no echo)\n ├─ Prompt: \"Confirm: \"\n ├─ Server re-hashes with new salt\n ├─ Existing authenticated clients remain connected\n └─ New connections must use new password\n\n,password --clear # remove password requirement\n ├─ Server disables auth\n ├─ Deletes NAME.auth file\n └─ New connections no longer prompted\n```\n\n#### Security Considerations\n\n| Threat | Mitigation |\n|--------|------------|\n| Eavesdropping on Unix socket | All post-auth traffic encrypted with ChaCha20-Poly1305. Even without password, X25519 key exchange encrypts the channel. |\n| Passive network sniffing (forwarded sockets) | Encrypted channel protects all INPUT/OUTPUT/CMD traffic. Attacker sees only ciphertext + message lengths. |\n| Active MITM (no password) | X25519 without authentication cannot prevent MITM. Use `--password` for full protection on untrusted paths. |\n| Brute force | Argon2id (slow hash) + rate limiting + lockout after 5 failures |\n| Replay attack on auth | Fresh 32-byte random challenge per connection attempt |\n| Replay/reorder attack on channel | Monotonic nonce counter per direction; out-of-order nonces rejected. Poly1305 tag rejects tampered ciphertext. |\n| Timing side-channel | `ffi_constant_time_compare` for proof verification |\n| Password in memory | `explicit_bzero` on password, key material after use (already in ffi-shim.c) |\n| Session key in memory | Keys zeroed on detach/disconnect. Keys are per-connection (not reused). |\n| Stolen .auth file | Attacker gets Argon2id hash, must still crack it. No plaintext. Does not reveal session keys. |\n| Privilege escalation via socket | `SO_PEERCRED` verifies connecting UID matches server UID (unless explicitly opened) |\n| Password file on disk | `--password-file` read once at startup, contents zeroed. File can be on tmpfs or removed after. |\n| Nonce exhaustion | 64-bit counter supports 2^64 messages per direction (~500 exabytes at max throughput). Practically inexhaustible. |\n\n#### New FFI Functions\n\n```c\n// Argon2id password hashing (using system libargon2, or embedded impl)\nint ffi_argon2id_hash(const char* password, int pwlen,\n const uint8_t* salt, int saltlen,\n int t_cost, int m_cost, int parallelism,\n uint8_t* out, int outlen);\n\n// HMAC-SHA256 for challenge-response proof\nint ffi_hmac_sha256(const uint8_t* key, int keylen,\n const uint8_t* msg, int msglen,\n uint8_t* out); // always 32 bytes\n\n// Constant-time comparison (prevents timing attacks)\nint ffi_constant_time_compare(const uint8_t* a, const uint8_t* b, int len);\n\n// Cryptographic random bytes\nint ffi_getrandom(uint8_t* buf, int len); // getrandom(2) or /dev/urandom\n\n// Get peer credentials from Unix socket\nint ffi_peercred_uid(int sockfd); // getsockopt SO_PEERCRED → uid\n\n// X25519 key exchange (for no-password encrypted channels)\nint ffi_x25519_keypair(uint8_t* pubkey, uint8_t* privkey); // generate ephemeral pair\nint ffi_x25519_shared(const uint8_t* my_priv, const uint8_t* their_pub,\n uint8_t* shared_out); // compute shared secret\n```\n\nNote: `explicit_bzero`, `ChaCha20-Poly1305`, and PBKDF2 primitives already exist in\n`ffi-shim.c` / `embed-crypto.c` — the encrypted channel reuses the existing\nChaCha20-Poly1305 implementation directly. HMAC-SHA256 can be built from the\nexisting SHA256, or pulled from libargon2's dependency on libcrypto. X25519 can\nuse TweetNaCl (~800 lines of C, public domain) or system libsodium.\n\n**Files:**\n- New: `src/jsh/mux-auth.sls` — auth protocol, hashing, challenge generation, encrypted channel\n- Modified: `ffi-shim.c` — add Argon2id, HMAC-SHA256, getrandom, peercred, X25519 FFI\n- Modified: `src/jsh/server.sls` — integrate auth check + encryption into accept flow\n- Modified: `src/jsh/client.sls` — integrate auth handshake + encryption into connect flow\n- Modified: `src/jsh/mux-protocol.sls` — encrypt/decrypt wrapper around message encode/decode\n\n**Estimated complexity:** Medium. ~400 lines Scheme + ~250 lines C (or ~800 lines C if embedding TweetNaCl for X25519).\n\n---\n\n### Phase 2: Client Attach Mode\n\n**Goal:** `jsh --attach` connects to a running server and relays terminal I/O.\n\n**Client lifecycle:**\n\n```\njsh --attach [--name NAME] [--session SESSION]\n │\n ├─ 1. Locate socket: /run/user/$UID/jsh/NAME.sock\n │ → not found: error \"no server named NAME\"\n │\n ├─ 2. Connect to Unix socket\n │\n ├─ 3. Authentication handshake (Phase 1.5)\n │ → AUTH_OK received (either no-auth or password accepted)\n │\n ├─ 4. Send ATTACH message with {session_name, cols, rows}\n │\n ├─ 5. Save terminal state (termios)\n │\n ├─ 6. Set terminal to raw mode\n │\n ├─ 7. Enter relay loop:\n │ select/poll on: stdin (fd 0) + server socket\n │ ├─ stdin data → wrap as INPUT message, send to server\n │ │ (intercept Ctrl-b prefix for local mux commands)\n │ ├─ server OUTPUT → write directly to stdout\n │ ├─ server STATUS → render status bar\n │ ├─ server ERROR → display error\n │ ├─ SIGWINCH → send RESIZE message to server\n │ └─ server disconnect → restore terminal, exit\n │\n └─ 7. On detach/disconnect:\n Restore terminal state\n Print \"detached from session NAME\"\n Exit 0\n```\n\n**Prefix key handling (client-side):**\n\n```\nInput byte → is prefix active?\n │\n ├─ No: is this Ctrl-b (0x02)?\n │ ├─ Yes: set prefix_active = true, start 1s timeout\n │ └─ No: forward byte to server as INPUT\n │\n └─ Yes (prefix active):\n ├─ 'd' → send DETACH, disconnect\n ├─ 'c' → send CMD{new-window}\n ├─ 'n' → send CMD{next-window}\n ├─ 'p' → send CMD{prev-window}\n ├─ '0'-'9' → send CMD{select-window, N}\n ├─ '%' → send CMD{split-vertical}\n ├─ '\"' → send CMD{split-horizontal}\n ├─ 'o' → send CMD{next-pane}\n ├─ 'x' → send CMD{kill-pane}\n ├─ 'z' → send CMD{zoom-pane}\n ├─ 'w' → send CMD{list-windows}\n ├─ 's' → send CMD{list-sessions}\n ├─ '?' → send CMD{show-help}\n ├─ ':' → enter local command-line mode\n ├─ Ctrl-b → forward literal Ctrl-b to server\n ├─ timeout → forward original Ctrl-b + this byte\n └─ unknown → beep, clear prefix\n```\n\n**Files:**\n- New: `src/jsh/client.sls` — client relay loop, prefix key handling\n- Modified: `src/jsh/main.sls` — dispatch to client mode\n\n**Estimated complexity:** Medium. Mostly I/O relay + keybinding dispatch.\n\n---\n\n### Phase 3: Session, Window, and Pane Management\n\n**Goal:** Full tmux-like session/window/pane lifecycle within the server.\n\n#### 3a: Sessions\n\n```\nServer maintains: *sessions* hashtable (id → session)\n *next-session-id* counter\n\nCommands:\n new-session [name] → create session + default window + pane\n kill-session [id|name] → kill all windows/panes, notify clients\n rename-session [name] → update session name\n list-sessions → return session list with metadata\n switch-session [id] → move client to different session\n```\n\n#### 3b: Windows\n\n```\nEach session has an ordered list of windows.\n\nCommands:\n new-window [name] → create window with one pane, switch to it\n kill-window [id] → kill all panes in window, remove from list\n next-window → cycle to next window\n prev-window → cycle to previous window\n select-window [N] → jump to window N\n rename-window [name] → set window title\n list-windows → interactive chooser overlay\n move-window [target] → reorder window in list\n last-window → toggle to previously active window\n```\n\n#### 3c: Panes\n\n```\nEach window has a layout tree of panes.\n\nCommands:\n split-vertical → split active pane left/right (new PTY)\n split-horizontal → split active pane top/bottom (new PTY)\n kill-pane → close pane, rebalance layout\n next-pane → cycle focus to next pane\n select-pane [dir] → move focus up/down/left/right\n resize-pane [dir] [N] → grow/shrink pane by N rows/cols\n zoom-pane → toggle fullscreen for active pane\n swap-pane [dir] → swap pane position with neighbor\n```\n\n#### 3d: Layout Engine\n\nThe layout engine recursively subdivides the window area:\n\n```\nWindow (80x24, minus 1 row for status bar = 80x23 usable)\n\nHorizontal split (50/50):\n┌─────────────────────┬─────────────────────┐\n│ Pane 0 (40x23) │ Pane 1 (39x23) │\n│ │ │\n└─────────────────────┴─────────────────────┘\n\nThen vertical split pane 1 (50/50):\n┌─────────────────────┬─────────────────────┐\n│ Pane 0 (40x23) │ Pane 1 (39x11) │\n│ ├─────────────────────┤\n│ │ Pane 2 (39x11) │\n└─────────────────────┴─────────────────────┘\n```\n\n**Layout recalculation triggers:**\n- Client RESIZE (terminal size changed)\n- Pane created or destroyed\n- Manual resize-pane command\n- Zoom/unzoom\n\n**Files:**\n- New: `src/jsh/session.sls` — session/window/pane CRUD\n- New: `src/jsh/layout.sls` — layout tree, split/merge, resize\n- Modified: `src/jsh/server.sls` — integrate session manager\n\n**Estimated complexity:** Large. Layout engine is the trickiest part.\n\n---\n\n### Phase 4: Screen Rendering\n\n**Goal:** Server composites all visible panes into a single screen buffer and sends\nit to attached clients.\n\n#### Virtual Terminal Emulator\n\nEach pane needs a virtual terminal (VT) that interprets the PTY output and maintains\na character grid — the same role that xterm/kitty/alacritty play, but in-process.\n\n```\nPTY master output bytes\n │\n ▼\n┌────────────────────────────────────────┐\n│ VT Parser (ANSI/xterm escape decoder) │\n│ - CSI sequences (cursor, color, etc.) │\n│ - OSC sequences (title, clipboard) │\n│ - SGR (text attributes) │\n│ - DEC private modes │\n└────────────────────────────────────────┘\n │\n ▼\n┌────────────────────────────────────────┐\n│ Cell Grid (cols × rows) │\n│ Each cell: {char, fg, bg, attrs} │\n│ + cursor position │\n│ + scrollback ring buffer │\n└────────────────────────────────────────┘\n```\n\n**VT state per pane:**\n\n```scheme\n(defstruct vt\n grid: vector ; vector of rows, each row = vector of cells\n cols: fixnum\n rows: fixnum\n cursor-x: fixnum\n cursor-y: fixnum\n saved-cursor: (cons fixnum fixnum) ; for DECSC/DECRC\n fg: fixnum ; current foreground color\n bg: fixnum ; current background color\n attrs: fixnum ; bold, underline, reverse, etc. bitmask\n scrollback: vector ; ring buffer of past rows\n scroll-top: fixnum ; scroll region top\n scroll-bot: fixnum ; scroll region bottom\n charset: symbol ; G0/G1 charset\n modes: fixnum ; DEC private mode bits\n title: string ; window title (from OSC 0/2)\n alt-grid: (or #f vector)) ; alternate screen buffer\n\n(defstruct cell\n char: char\n fg: fixnum ; 256-color or true-color index\n bg: fixnum\n attrs: fixnum) ; bold(1) underline(2) reverse(4) dim(8) italic(16) strikethrough(32)\n```\n\n#### Screen Compositor\n\n```\nFor each attached client:\n 1. Determine visible session → window → pane layout\n 2. For each visible pane:\n a. Read pane's VT grid\n b. Map pane cells to screen coordinates (pane.x, pane.y offset)\n 3. Draw pane borders (│, ─, ┼, etc.)\n 4. Draw status bar (bottom row)\n 5. Diff against last-sent screen buffer\n 6. Generate minimal ANSI escape sequence to update only changed cells\n 7. Send OUTPUT message to client\n```\n\n**Differential rendering:**\n\n```\nlast_screen[row][col] vs current_screen[row][col]\n │\n ├─ Same cell → skip\n └─ Different → emit: CSI row;col H (move cursor)\n SGR attrs (set colors/attrs)\n char (print character)\n```\n\nThis minimizes bandwidth and keeps the client responsive.\n\n**Files:**\n- New: `src/jsh/vt.sls` — VT100/xterm terminal emulator (escape parser + cell grid)\n- New: `src/jsh/screen.sls` — screen compositor, diff renderer, status bar\n- Modified: `src/jsh/server.sls` — integrate rendering into event loop\n\n**Estimated complexity:** Large. VT emulator is significant but well-specified.\n\n---\n\n### Phase 5: Copy Mode & Scrollback\n\n**Goal:** `Ctrl-b [` enters a mode where the user can scroll through pane history\nand copy text, like tmux copy mode.\n\n**Copy mode keybindings (vi-style, matching tmux):**\n\n| Key | Action |\n|-----|--------|\n| `q` / `Escape` | Exit copy mode |\n| `h/j/k/l` | Cursor movement |\n| `Ctrl-u/d` | Page up/down |\n| `g/G` | Top/bottom of scrollback |\n| `Space` | Start selection |\n| `Enter` | Copy selection, exit copy mode |\n| `/` | Search forward |\n| `?` | Search backward |\n| `n/N` | Next/prev search match |\n| `w/b` | Word forward/backward |\n| `0/$` | Line start/end |\n\n**Scrollback buffer:**\n\n```\nPer pane: ring buffer of N rows (default 2000, configurable)\n\n┌─────────────────────────────────┐\n│ scrollback[0] (oldest) │ ← scroll-start\n│ scrollback[1] │\n│ ... │\n│ scrollback[N-1] (most recent) │ ← scroll-end\n├─────────────────────────────────┤\n│ grid[0] (visible top) │ ← viewport top\n│ ... │\n│ grid[rows-1] (visible bot) │ ← viewport bottom\n└─────────────────────────────────┘\n```\n\nIn copy mode, the viewport shifts up into scrollback. The VT grid freezes\n(new output buffered, not displayed until copy mode exits).\n\n**Files:**\n- New: `src/jsh/copymode.sls` — copy mode input handling, selection, search\n- Modified: `src/jsh/vt.sls` — scrollback ring buffer integration\n- Modified: `src/jsh/screen.sls` — render scrollback viewport + selection highlight\n\n**Estimated complexity:** Medium.\n\n---\n\n### Phase 6: Status Bar & Chrome\n\n**Goal:** Render a tmux-style status bar and pane borders.\n\n**Status bar layout:**\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ [0:jsh] [1:vim]* [2:make] \"session-name\" 2026-03-19 14:30 │\n└─────────────────────────────────────────────────────────────────────┘\n ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^\n window list (* = active) session name date/time\n```\n\n**Pane borders:**\n\n```\nActive pane border: bright green (configurable)\nInactive pane border: dim gray\n\n│ ← vertical border\n─ ← horizontal border\n┌┐└┘┬┴├┤┼ ← corner/junction characters\n```\n\n**Configurable via shell variables:**\n\n```bash\nJSH_MUX_STATUS_LEFT='[#S] #W' # session name, window list\nJSH_MUX_STATUS_RIGHT='%Y-%m-%d %H:%M'\nJSH_MUX_STATUS_BG=colour235\nJSH_MUX_STATUS_FG=colour136\nJSH_MUX_PREFIX=C-b # prefix key (default Ctrl-b)\nJSH_MUX_SCROLLBACK=2000 # scrollback lines\nJSH_MUX_PANE_BORDER_ACTIVE=green\nJSH_MUX_PANE_BORDER_INACTIVE=colour240\n```\n\n**Files:**\n- New: `src/jsh/statusbar.sls` — status bar formatting and rendering\n- Modified: `src/jsh/screen.sls` — integrate borders and status bar\n\n**Estimated complexity:** Small-medium.\n\n---\n\n### Phase 7: Persistence & Resurrection\n\n**Goal:** Server state survives crashes. On restart, sessions can be partially recovered.\n\n**Server state file:** `/run/user/$UID/jsh/NAME.state` (or `~/.jsh/NAME.state`)\n\n```scheme\n;; Serialized on clean shutdown and periodically (every 30s)\n(server-state\n (sessions\n ((id 0) (name \"work\") (cwd \"/home/user/project\")\n (windows\n ((id 0) (name \"editor\") (panes ((cmd \"vim\" \"main.c\"))))\n ((id 1) (name \"build\") (panes ((cmd \"make\" \"watch\")))))))\n (env ((EDITOR . \"vim\") (PAGER . \"less\"))))\n```\n\n**On server restart after crash:**\n1. Read state file\n2. Recreate sessions/windows with same names\n3. Re-execute saved commands in new PTYs (best-effort)\n4. Notify re-attaching clients that session was resurrected\n\n**Files:**\n- New: `src/jsh/persist.sls` — state serialization/deserialization\n- Modified: `src/jsh/server.sls` — periodic state save, crash recovery\n\n**Estimated complexity:** Small-medium.\n\n---\n\n### Phase 8: Integration with Existing jsh Features\n\n**Goal:** Wire the multiplexer into jsh's existing infrastructure.\n\n#### 8a: Recording Integration\n\nEach pane automatically records if `*recording?*` is active in the server.\nThe recorder captures per-pane events with pane ID tags:\n\n```json\n[1.5, \"o\", \"output text\", {\"pane\": 2, \"window\": 1, \"session\": 0}]\n```\n\nPlayback can filter by pane/window/session.\n\n#### 8b: Meta-Commands\n\nAdd multiplexer meta-commands alongside existing `,record` / `,play`:\n\n| Command | Action |\n|---------|--------|\n| `,mux` | Show multiplexer status |\n| `,detach` | Detach from server |\n| `,new-window [name]` | Create window |\n| `,split [-v\\|-h]` | Split pane |\n| `,sessions` | List sessions |\n| `,attach [name]` | Attach to session |\n| `,rename [name]` | Rename current window/session |\n\n#### 8c: History Sharing\n\nAll panes in a session share command history (append-mode, like `HISTFILE`\nwith `shopt -s histappend`). Cross-pane history is merged on read.\n\n#### 8d: Environment Propagation\n\nNew panes inherit the environment of the pane they were split from.\nSession-level env vars (set via `,setenv`) propagate to new windows.\n\n**Files modified:**\n- `src/jsh/recorder.sls` — add pane-aware event emission\n- `src/jsh/main.sls` — register meta-commands\n- `src/jsh/history.sls` — shared history file locking\n\n**Estimated complexity:** Medium.\n\n---\n\n## Implementation Order & Dependencies\n\n```\nPhase 0: PTY FFI ──────────────────────────────┐\n │\nPhase 1: Server Daemon ────────────────────────┤\n │\nPhase 1.5: Authentication ─────────────────────┤\n │\nPhase 2: Client Attach ────────────────────────┤\n (minimal working system at this point) │\n │\nPhase 4: VT Emulator ──┐ │\n │ │\nPhase 3: Sessions ──────┤ (can develop │\n │ in parallel) │\nPhase 6: Status Bar ────┘ │\n │\nPhase 5: Copy Mode ────────────────────────────┤\n │\nPhase 7: Persistence ──────────────────────────┤\n │\nPhase 8: Integration ──────────────────────────┘\n```\n\n**Milestone 1 (MVP):** Phases 0-2 (incl. 1.5) — single session, single pane, attach/detach with optional auth.\n**Milestone 2 (Usable):** + Phases 3-4 — multiple windows/panes with proper rendering.\n**Milestone 3 (Complete):** + Phases 5-8 — copy mode, persistence, full integration.\n\n---\n\n## FFI Additions Summary\n\nAll new C functions needed in `ffi-shim.c`:\n\n```c\n// PTY management\nint ffi_pty_open(void);\nchar* ffi_pty_slave_name(int master_fd);\nint ffi_pty_open_slave(int master_fd);\nvoid ffi_pty_set_size(int master_fd, int cols, int rows);\n\n// Daemon support\nint ffi_daemonize(void);\nint ffi_write_pidfile(const char* path);\nint ffi_read_pidfile(const char* path);\nint ffi_process_alive(int pid); // kill(pid, 0)\nint ffi_unlink(const char* path); // already exists\n\n// Poll/select for event loop\nint ffi_poll(int* fds, int nfds, int timeout_ms, int* revents);\n// or: use existing ffi-byte-ready? in a loop with short timeout\n\n// Authentication & Encryption (Phase 1.5)\nint ffi_argon2id_hash(const char* pw, int pwlen, const uint8_t* salt,\n int saltlen, int t, int m, int p,\n uint8_t* out, int outlen);\nint ffi_hmac_sha256(const uint8_t* key, int keylen,\n const uint8_t* msg, int msglen, uint8_t* out);\nint ffi_constant_time_compare(const uint8_t* a, const uint8_t* b, int len);\nint ffi_getrandom(uint8_t* buf, int len);\nint ffi_peercred_uid(int sockfd);\nint ffi_x25519_keypair(uint8_t* pub, uint8_t* priv);\nint ffi_x25519_shared(const uint8_t* priv, const uint8_t* pub, uint8_t* out);\n// Note: explicit_bzero + ChaCha20-Poly1305 already exist in ffi-shim.c\n```\n\n---\n\n## New Module Summary\n\n| Module | Purpose | Lines (est.) |\n|--------|---------|-------------|\n| `src/jsh/server.sls` | Server daemon, event loop, client manager | ~800 |\n| `src/jsh/client.sls` | Client attach mode, prefix key handling | ~400 |\n| `src/jsh/mux-protocol.sls` | Wire protocol encode/decode | ~200 |\n| `src/jsh/mux-auth.sls` | Auth protocol, Argon2id hashing, challenge/proof, encrypted channel | ~400 |\n| `src/jsh/session.sls` | Session/window/pane lifecycle | ~500 |\n| `src/jsh/layout.sls` | Pane layout tree, split/merge/resize | ~400 |\n| `src/jsh/vt.sls` | VT100/xterm terminal emulator | ~1200 |\n| `src/jsh/screen.sls` | Screen compositor, diff renderer | ~600 |\n| `src/jsh/copymode.sls` | Copy mode, scrollback nav, selection | ~400 |\n| `src/jsh/statusbar.sls` | Status bar formatting | ~200 |\n| `src/jsh/persist.sls` | Server state serialization | ~200 |\n| **Total** | | **~5300** |\n\n---\n\n## Risks & Mitigations\n\n| Risk | Impact | Mitigation |\n|------|--------|------------|\n| VT emulator complexity | High — incomplete escape handling causes rendering glitches | Start with a minimal subset (CSI cursor/SGR/erase), add escapes incrementally. Use vttest for validation. |\n| PTY lifecycle bugs | Medium — leaked PTY fds, zombie children | Careful fd tracking in pane struct. SIGCHLD handler reaps all. Periodic audit of /proc/self/fd. |\n| Race conditions in event loop | Medium — concurrent client input + PTY output | Single-threaded event loop (no threading). All I/O via poll/select. |\n| Performance of screen diffing | Low-medium — large terminals with rapid output | Dirty-rectangle tracking. Rate-limit OUTPUT messages (max 60 fps). Batch PTY reads. |\n| Client crash leaves stale state | Low — orphaned socket connection | Server detects client EOF on socket read. Periodic PING/PONG with timeout. |\n| Chez Scheme threading model | Medium — foreign thread callbacks | Keep FFI calls non-blocking. Use poll with timeouts, never blocking reads on multiple fds from Scheme. |\n| Argon2id library dependency | Low — need libargon2 or embedded impl | Use system libargon2-dev if available, else embed reference C impl (~500 lines). Already have crypto primitives in embed-crypto.c. |\n| Auth bypass via socket steal | Low — attacker with same UID could connect | SO_PEERCRED validates UID. Socket dir is mode 0700. Password adds second factor beyond Unix permissions. |\n| DoS via auth flooding | Low — attacker spams connections | Rate limit per UID via SO_PEERCRED. Lockout after 5 failures. Server stays responsive for PTY I/O during auth delays. |\n\n---\n\n## Testing Strategy\n\n1. **Unit tests:** VT emulator escape parsing, layout tree operations, protocol encode/decode\n2. **Integration tests:** Start server, attach client, type commands, verify output\n3. **Stress tests:** Rapid window create/destroy, large scrollback, many concurrent clients\n4. **Compatibility tests:** Run `vttest` inside a pane, verify correct rendering\n5. **Crash recovery:** Kill server with SIGKILL, restart, verify state file resurrection\n6. **Binary tests:** Add mux-specific tests to `test-binary.sh` (start server, attach, detach, verify)\n7. **Auth tests:** Password set/verify round-trip, wrong password rejection, lockout after 5 failures, rate limit timing, password change while clients connected, `--password-file` mode, no-auth server skips handshake\n\n---\n\n## Configuration\n\nAll configuration via shell variables (no separate config file):\n\n```bash\n# In ~/.jshrc or ~/.profile\nexport JSH_MUX_PREFIX='C-b' # prefix key (default: Ctrl-b)\nexport JSH_MUX_SCROLLBACK=10000 # scrollback lines per pane\nexport JSH_MUX_MOUSE=on # mouse support (clicks select pane)\nexport JSH_MUX_STATUS=on # show status bar\nexport JSH_MUX_STATUS_POSITION=bottom\nexport JSH_MUX_BASE_INDEX=0 # window numbering starts at 0\nexport JSH_MUX_ESCAPE_TIME=500 # ms to wait after prefix key\nexport JSH_MUX_HISTORY_LIMIT=50000\nexport JSH_MUX_DEFAULT_SHELL=jsh # shell to spawn in new panes\nexport JSH_MUX_SOCKET_DIR=/run/user/$UID/jsh\nexport JSH_MUX_ENCRYPT=on # encrypt client↔server traffic (default: on)\n # \"off\" disables for trusted local sockets\nexport JSH_MUX_AUTH_MAX_ATTEMPTS=5 # failed attempts before lockout\nexport JSH_MUX_AUTH_LOCKOUT_SEC=60 # lockout duration\nexport JSH_MUX_PASSWORD= # for non-interactive attach (scripts/CI)\n```\n"} {"text":";; FILE: jerboa-shell/gen-embed.ss\n#!chezscheme\n;;; gen-embed.ss — Generate src/jsh/embed-data.sls from embed/ directory\n;;;\n;;; Scans the embed/ directory (or JSH_EMBED_DIR) recursively and generates\n;;; a Scheme library containing a hashtable mapping relative paths to bytevectors.\n;;;\n;;; When JSH_EMBED_ENCRYPT=1, prompts for a passphrase and encrypts each file with ChaCha20-Poly1305\n;;; using a key derived from the passphrase via PBKDF2-HMAC-SHA256.\n;;; The salt is generated per-build and stored in the library.\n;;;\n;;; Usage: LD_LIBRARY_PATH=. scheme -q < gen-embed.ss\n\n(import (chezscheme))\n\n;; Pick the right shared-library extension at runtime: macOS uses .dylib,\n;; everything else (Linux, FreeBSD, etc.) uses .so. Try .dylib first when it\n;; actually exists on disk so this works regardless of machine-type encoding.\n(define (resolve-shared-lib base-path)\n (cond\n [(file-exists? (string-append base-path \".dylib\"))\n (string-append base-path \".dylib\")]\n [(file-exists? (string-append base-path \".so\"))\n (string-append base-path \".so\")]\n [else (string-append base-path \".so\")])) ; let load-shared-object fail with a useful error\n\n(define embed-dir\n (or (getenv \"JSH_EMBED_DIR\") \"embed\"))\n\n(define output-file \"src/jsh/embed-data.sls\")\n\n;; Encryption: auto-enable when embedding from ~/.embed/ (override with JSH_EMBED_ENCRYPT=0 or =1)\n(define embed-encrypt?\n (let ([flag (getenv \"JSH_EMBED_ENCRYPT\")])\n (cond\n [(equal? flag \"1\") #t]\n [(equal? flag \"0\") #f]\n [else\n ;; Default: encrypt if embed-dir is under $HOME/.embed\n (let ([home (getenv \"HOME\")])\n (and home\n (let ([dot-embed (string-append home \"/.embed\")])\n (and (>= (string-length embed-dir) (string-length dot-embed))\n (string=? (substring embed-dir 0 (string-length dot-embed))\n dot-embed)))))])))\n\n(when embed-encrypt?\n (display (string-append \"*** Encryption enabled for \" embed-dir \"\\n\")\n (current-error-port)))\n\n;; Resolve the Rust native library (libjerboa_native.{so,dylib}). The\n;; same library now provides the embed_* crypto symbols (ring-backed)\n;; AND the X25519 symbols used below. Order: $JERBOA_NATIVE_LIB env,\n;; then the vendored sparse crate.\n(define (resolve-jerboa-native-lib)\n (let ([env (getenv \"JERBOA_NATIVE_LIB\")]\n [vendor \"vendor/jerboa-native-rs/target/release/libjerboa_native\"])\n (cond\n [(and env (file-exists? env)) env]\n [(file-exists? (string-append vendor \".dylib\"))\n (string-append vendor \".dylib\")]\n [(file-exists? (string-append vendor \".so\"))\n (string-append vendor \".so\")]\n [else #f])))\n\n;; Load libjerboa_native for embed encryption. Replaces the historical\n;; hand-rolled embed-crypto.c (L-1, W-1): every crypto primitive\n;; (PBKDF2, ChaCha20-Poly1305 AEAD, random_bytes) plus the tty\n;; passphrase reader is now served by ring + libc inside Rust.\n(define crypto-lib\n (and embed-encrypt?\n (guard (e [#t\n (display \"Error: Cannot load libjerboa_native.{so,dylib} for encryption\\n\"\n (current-error-port))\n (display \"Build it: (cd vendor/jerboa-native-rs && cargo build --release --no-default-features --features 'tls crypto')\\n\"\n (current-error-port))\n (exit 1)])\n (let ([p (resolve-jerboa-native-lib)])\n (unless p\n (display \"Error: libjerboa_native not found in vendor/jerboa-native-rs\\n\"\n (current-error-port))\n (exit 1))\n (load-shared-object p)))))\n\n;; Prompt for passphrase on /dev/tty (no echo), verify with second prompt\n(define embed-key\n (and embed-encrypt?\n (let ([buf (make-bytevector 256)]\n [buf2 (make-bytevector 256)]\n [read-pass (foreign-procedure \"embed_read_passphrase\"\n (string u8* int) int)])\n (let ([len (read-pass \"Embed passphrase: \" buf 256)])\n (if (<= len 0)\n (begin\n (display \"Error: No passphrase provided\\n\" (current-error-port))\n (exit 1))\n (let ([len2 (read-pass \"Confirm passphrase: \" buf2 256)])\n (let ([match? (and (= len len2)\n (let loop ([i 0])\n (or (= i len)\n (and (= (bytevector-u8-ref buf i)\n (bytevector-u8-ref buf2 i))\n (loop (+ i 1))))))])\n (unless match?\n (display \"Error: Passphrases do not match\\n\" (current-error-port))\n (exit 1))\n (let ([out (make-bytevector len)])\n (bytevector-copy! buf 0 out 0 len)\n (utf8->string out)))))))))\n\n;; FFI bindings for crypto (only used when encrypting)\n(define (crypto-random-bytes bv)\n (when embed-key\n (let ([rc ((foreign-procedure \"embed_random_bytes\" (u8* int) int)\n bv (bytevector-length bv))])\n (unless (= rc 0)\n (error 'gen-embed \"Failed to generate random bytes\")))))\n\n(define (crypto-pbkdf2 password salt iterations out)\n ((foreign-procedure \"embed_pbkdf2_sha256\"\n (u8* int u8* int unsigned-32 u8* int) void)\n password (bytevector-length password)\n salt (bytevector-length salt)\n iterations\n out (bytevector-length out)))\n\n(define (crypto-encrypt key nonce plaintext out)\n ((foreign-procedure \"embed_encrypt\"\n (u8* u8* u8* int u8*) int)\n key nonce plaintext (bytevector-length plaintext) out))\n\n;; Generate salt and derive key at build time\n(define build-salt #f)\n(define build-key #f)\n\n(when embed-key\n (set! build-salt (make-bytevector 32))\n (crypto-random-bytes build-salt)\n (set! build-key (make-bytevector 32))\n (let ([pass-bv (string->utf8 embed-key)])\n (crypto-pbkdf2 pass-bv build-salt 100000 build-key)\n (when (getenv \"JSH_EMBED_DEBUG\")\n (display \"[gen-embed-debug] pass len=\" (current-error-port))\n (display (bytevector-length pass-bv) (current-error-port))\n (display \" salt[0..15]=\" (current-error-port))\n (do ((i 0 (+ i 1))) ((= i 16))\n (fprintf (current-error-port) \"~2,'0x\" (bytevector-u8-ref build-salt i)))\n (display \" key[0..7]=\" (current-error-port))\n (do ((i 0 (+ i 1))) ((= i 8))\n (fprintf (current-error-port) \"~2,'0x\" (bytevector-u8-ref build-key i)))\n (newline (current-error-port))))\n (printf \" Encryption enabled (PBKDF2 100k iterations)~n\"))\n\n;; Encrypt a bytevector, returning nonce||tag||ciphertext\n(define (encrypt-bytevector bv)\n (let* ([nonce (make-bytevector 12)]\n [out (make-bytevector (+ 28 (bytevector-length bv)))])\n (crypto-random-bytes nonce)\n (crypto-encrypt build-key nonce bv out)\n out))\n\n;; Recursively collect all regular files under dir, returning relative paths\n(define (collect-files dir prefix)\n (if (and (file-exists? dir) (file-directory? dir))\n (let loop ([entries (directory-list dir)] [acc '()])\n (if (null? entries)\n acc\n (let* ([name (car entries)]\n [full (string-append dir \"/\" name)]\n [rel (if (string=? prefix \"\")\n name\n (string-append prefix \"/\" name))])\n (cond\n [(file-directory? full)\n (loop (cdr entries)\n (append (collect-files full rel) acc))]\n [(file-regular? full)\n (loop (cdr entries) (cons (cons rel full) acc))]\n [else (loop (cdr entries) acc)]))))\n '()))\n\n;; Reject embedding private key material in the clear. When encryption is off,\n;; any embedded file is baked into the binary verbatim and recoverable from the\n;; artifact, so fail closed on sensitive path patterns. (When encrypting, these\n;; are protected and intentionally allowed — e.g. record.key.)\n(define sensitive-embed-patterns\n '(\".ssh/\" \"id_rsa\" \"id_ed25519\" \"id_ecdsa\" \"id_dsa\"\n \"key.pem\" \"privkey\" \".key\" \"mullvad\" \"wireguard\" \"_account.txt\"))\n\n(define (substring-contains? hay needle)\n (let ([hn (string-length hay)] [nn (string-length needle)])\n (and (<= nn hn)\n (let loop ([i 0])\n (cond\n [(> (+ i nn) hn) #f]\n [(string=? (substring hay i (+ i nn)) needle) #t]\n [else (loop (+ i 1))])))))\n\n(define (path-sensitive? rel)\n (let ([low (string-downcase rel)])\n (let loop ([ps sensitive-embed-patterns])\n (cond\n [(null? ps) #f]\n [(substring-contains? low (car ps)) #t]\n [else (loop (cdr ps))]))))\n\n;; Read a file as a bytevector\n(define (read-file-bytes path)\n (let* ([port (open-file-input-port path)]\n [data (get-bytevector-all port)])\n (close-port port)\n (if (eof-object? data)\n (make-bytevector 0)\n data)))\n\n;; Format a bytevector as a Scheme literal\n(define (bytevector->scheme-literal bv)\n (let ([port (open-output-string)])\n (display \"#vu8(\" port)\n (let loop ([i 0])\n (when (< i (bytevector-length bv))\n (when (> i 0) (display \" \" port))\n (display (bytevector-u8-ref bv i) port)\n (loop (+ i 1))))\n (display \")\" port)\n (get-output-string port)))\n\n;; X25519 keypair for sealed-box session log encryption.\n;; The private key lives at <embed-dir>/record.key — generated once, persisted forever.\n;; Public key is derived from it and baked into the binary (always accessible).\n;; Private key is encrypted into the embed file table (requires ,unlock to read).\n(define record-pubkey #f)\n\n(when embed-encrypt?\n (let ([key-path (string-append embed-dir \"/record.key\")])\n ;; Load jerboa-native for X25519\n (guard (e [#t\n (display \"Warning: Cannot load libjerboa_native.{so,dylib} for X25519\\n\"\n (current-error-port))\n (display \" Session log encryption will not be available\\n\"\n (current-error-port))])\n (let ([native-path (resolve-jerboa-native-lib)])\n (when native-path\n (load-shared-object native-path)\n (if (file-exists? key-path)\n ;; Load existing private key, derive public key\n (let ([priv (read-file-bytes key-path)]\n [pub (make-bytevector 32)])\n (when (= (bytevector-length priv) 32)\n (let ([rc ((foreign-procedure \"jerboa_x25519_public_from_private\"\n (u8* int u8*) int)\n priv 32 pub)])\n (when (= rc 0)\n (set! record-pubkey pub)\n (printf \" Loaded record.key — session log encryption enabled~n\")))))\n ;; First time: generate keypair, save private key\n (let ([priv (make-bytevector 32)]\n [pub (make-bytevector 32)])\n (let ([rc ((foreign-procedure \"jerboa_x25519_generate_keypair\"\n (u8* u8*) int)\n priv pub)])\n (when (= rc 0)\n ;; Save private key to embed dir (will be encrypted with other files)\n (let ([port (open-file-output-port key-path\n (file-options no-fail) (buffer-mode block))])\n (put-bytevector port priv)\n (close-port port))\n (chmod key-path #o600)\n (set! record-pubkey pub)\n (printf \" Generated record.key — session log encryption enabled~n\")\n (printf \" Private key saved to ~a~n\" key-path))))))))))\n\n;; Main\n(let ([files (collect-files embed-dir \"\")])\n (printf \"=== Generating embed-data.sls ===~n\")\n (unless embed-key\n (let ([bad (let loop ([fs files] [acc '()])\n (cond\n [(null? fs) (reverse acc)]\n [(path-sensitive? (caar fs)) (loop (cdr fs) (cons (caar fs) acc))]\n [else (loop (cdr fs) acc)]))])\n (when (pair? bad)\n (display \"Error: refusing to embed private material WITHOUT encryption:\\n\"\n (current-error-port))\n (for-each (lambda (r) (fprintf (current-error-port) \" ~a~n\" r)) bad)\n (display \"Enable encryption (JSH_EMBED_ENCRYPT=1, or embed from ~/.embed) or remove these files.\\n\"\n (current-error-port))\n (exit 1))))\n (when embed-key\n (printf \" Encryption enabled — passphrase accepted~n\"))\n (call-with-output-file output-file\n (lambda (out)\n (display \"#!chezscheme\\n\" out)\n (display \";;; embed-data.sls — Auto-generated by gen-embed.ss. Do not edit.\\n\" out)\n (display \";;; Contains embedded file data compiled into the binary.\\n\\n\" out)\n (display \"(library (jsh embed-data)\\n\" out)\n (display \" (export %embed-file-table %embed-encrypted? %embed-salt %record-pubkey)\\n\" out)\n (display \" (import (chezscheme))\\n\\n\" out)\n\n ;; Emit encryption flag\n (fprintf out \" (define %embed-encrypted? ~a)~n~n\"\n (if embed-key \"#t\" \"#f\"))\n\n ;; Emit salt (empty bytevector if not encrypted)\n (fprintf out \" (define %embed-salt ~a)~n~n\"\n (if build-salt\n (bytevector->scheme-literal build-salt)\n \"#vu8()\"))\n\n ;; Emit record public key (always accessible, no unlock needed)\n (fprintf out \" (define %record-pubkey ~a)~n~n\"\n (if record-pubkey\n (bytevector->scheme-literal record-pubkey)\n \"#f\"))\n\n ;; Emit file table\n (display \" (define %embed-file-table\\n\" out)\n (display \" (let ((ht (make-hashtable string-hash string=?)))\\n\" out)\n (for-each\n (lambda (entry)\n (let* ([rel (car entry)]\n [full (cdr entry)]\n [data (read-file-bytes full)]\n [raw-size (bytevector-length data)]\n [stored (if embed-key (encrypt-bytevector data) data)]\n [stored-size (bytevector-length stored)])\n (if embed-key\n (printf \" ~a (~a bytes -> ~a bytes encrypted)~n\"\n rel raw-size stored-size)\n (printf \" ~a (~a bytes)~n\" rel raw-size))\n (fprintf out \" (hashtable-set! ht ~s ~a)~n\"\n rel (bytevector->scheme-literal stored))))\n (sort (lambda (a b) (string<? (car a) (car b))) files))\n ;; record.key is picked up from the embed dir by collect-files above\n (display \" ht))\\n\\n\" out)\n (display \" ) ;; end library\\n\" out))\n 'replace)\n (printf \" Generated ~a with ~a file~a~a~n\"\n output-file (length files)\n (if (= (length files) 1) \"\" \"s\")\n (if embed-key \" (encrypted)\" \"\")))\n"} -{"text":";; FILE: jerboa-shell/build-jsh-musl.ss\n#!chezscheme\n;;; build-jsh-musl.ss — Build a fully static jsh binary using musl libc\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib> < build-jsh-musl.ss\n;;;\n;;; This script:\n;;; 1. Compiles jsh modules (using stock scheme with glibc)\n;;; 2. Creates boot file + optimized program .so\n;;; 3. Generates C files with embedded boot data\n;;; 4. Compiles C with musl-gcc against musl-built Chez's scheme.h\n;;; 5. Links fully static binary with libkernel.a from musl-built Chez\n;;;\n;;; The resulting jsh-musl binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (jerboa build musl)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Validate musl setup ==========\n\n(let ([result (validate-musl-setup)])\n (unless (eq? (car result) 'ok)\n (printf \"Error: ~a~n\" (cdr result))\n (printf \"~nTo build Chez Scheme with musl:~n\")\n (printf \" cd ~/mine/ChezScheme~n\")\n (printf \" ./configure --threads --static CC=musl-gcc --installprefix=$HOME/chez-musl~n\")\n (printf \" make -j$(nproc) && make install~n\")\n (exit 1)))\n\n(printf \"musl Chez found: ~a~n~n\" (musl-chez-lib-dir))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/root\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-musl.sh so we know the repo root.\n(define vendor-dir\n (let ([script-dir (getenv \"SCRIPT_DIR\")])\n (if script-dir\n (format \"~a/vendor\" script-dir)\n (let ([cwd-vendor \"./vendor\"])\n (if (file-directory? cwd-vendor) cwd-vendor\n (format \"~a/mine/jerboa-shell/vendor\" home-dir))))))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. The container build sets HOME=/build and\n;; clones all deps into /build/mine/, so the ~/mine/ fallback covers that.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n(define ffi-shim-symbols (read-symbol-list \"ffi-shim-symbols.list\"))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n;; Base jerboa directory (parent of lib/) — for support/ files\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n;; jerboa-ssh library (SSH agent)\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n;; jsqlite library\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jsqlite/src\" home-dir)))\n\n;; jerboa-crypto library (AEAD, HMAC, scrypt for mux auth)\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n;; jerboa-coreutils: replaced by Rust uutils/coreutils (libjsh_coreutils.a)\n;; No coreutils-dir needed — Scheme coreutils modules are no longer compiled\n\n;; jerboa-awk library\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n;; jerboa-sed library\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n;; Rust coreutils static library (replaces jerboa-coreutils Scheme modules)\n(define rust-coreutils-lib-path\n (format \"~a/mine/jerboa-shell/rust-coreutils/target/x86_64-unknown-linux-musl/release/libjsh_coreutils.a\"\n home-dir))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — will try native target path~n\")\n (set! rust-coreutils-lib-path\n (format \"~a/mine/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\"\n home-dir))\n (set! has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n (unless has-rust-coreutils?\n (printf \" ERROR: No libjsh_coreutils.a found. Run: cd rust-coreutils && cargo build --release --target x86_64-unknown-linux-musl~n\")\n (exit 1)))\n\n;; jerboa-aws library\n;; HTTP/HTTPS is provided by (std net request) → (std net tls-rustls) (rustls).\n;; jerboa-ssl/jerboa-https are no longer used — they require dynamic OpenSSL via\n;; load-shared-object, which fails in static builds, and rustls is preferred\n;; over OpenSSL for security.\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; jerboa-fuse library (encrypted FUSE vault)\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n;; Rust native library — check once, reuse in C code generation and linking.\n;; Prefer the vendored Rust sources (vendor/jerboa/jerboa-native-rs) so that\n;; `git clean -xfd && make jsh-musl` works on a fresh checkout. Fall back to\n;; the developer's ~/mine/jerboa checkout for iterative local work.\n(define native-lib-path\n (let* ([relpath \"jerboa-native-rs/target/x86_64-unknown-linux-musl/release/libjerboa_native.a\"]\n [vendor-path (format \"~a/jerboa/~a\" vendor-dir relpath)]\n [mine-path (format \"~a/mine/jerboa/~a\" home-dir relpath)])\n (cond\n [(file-exists? vendor-path) vendor-path]\n [(file-exists? mine-path) mine-path]\n [else vendor-path]))) ; report the vendor path in the warning\n(define has-native-lib? (file-exists? native-lib-path))\n(when (and has-native-lib?\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\")\n (printf \" Looked in vendor/jerboa/... and ~~/mine/jerboa/...~n\"))\n\n;; ========== Step 0: Coreutils (Rust uutils) ==========\n;; Coreutils are now provided by Rust uutils/coreutils (libjsh_coreutils.a).\n;; No Scheme coreutils staging needed — the Rust library provides all builtins\n;; via FFI (jsh_ls, jsh_cat, etc.) called from (jsh coreutils).\n(printf \"[0/7] Coreutils: using Rust uutils (~a)~n\" rust-coreutils-lib-path)\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n;; jerboa-awk: pure Scheme, no patching needed — just copy and compile\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: needs pcre2 patched to use Rust regex instead of C PCRE2\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n;; Replace pcre2.sls with Rust-backed version\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if has-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n;; jerboa-aws: copy entire tree (optional — only if aws-dir contains jerboa-aws)\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when has-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n ;; Delete pre-compiled .so and .wpo files\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(unless has-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (printf \" jerboa-aws not found, skipping~n\"))\n\n;; Compile all jerboa-aws modules in dependency order\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons aws-stage aws-stage)\n (library-directories))])\n ;; jerboa-aws modules in dependency order (only if present)\n (when has-aws?\n (printf \" Compiling jerboa-aws...~n\")\n ;; Core modules first\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n ;; Services\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n ;; CLI\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0c: Stage jerboa-ssh for static build ==========\n(printf \"[0c/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls' 2>/dev/null || true\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null || true\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build (Linux sed -i, not BSD -i '')\n (system (format \"find '~a' -name '*.sls' -exec sed -i 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"sed -i 's/base64-encode/b64-encode/g' '~a'\" kh))\n (system (format \"sed -i 's/base64-decode/b64-decode/g' '~a'\" kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0d: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0d/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n;; vault-stage/ is checked into the repo with pre-patched .sls files:\n;; - crypto.sls uses Rust native (ring) instead of OpenSSL\n;; - mount.sls uses ffi-shim instead of load-shared-object\n;; - blockstore/fuse.sls have load-shared-object calls removed\n;; Just clean stale compiled artifacts and compile what's there.\n(define vault-stage (format \"~a/vault-stage\" (current-directory)))\n\n(when has-jerboa-fuse?\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(and (file-exists? so)\n (time>=? (file-modification-time so) (file-modification-time sls)))\n (void)] ;; up to date from jsh-compile — skip\n [else\n (printf \" Compiling ~a...~n\" sls)\n ;; Use with-exception-handler instead of guard so continuable warnings\n ;; (e.g. Chez compile-time format-string warnings raised via\n ;; raise-continuable) don't abort the compile and leave a 15-byte stub\n ;; .so file behind. guard converts continuable raises to non-continuable.\n (with-exception-handler\n (lambda (exn)\n (cond\n [(warning? exn)\n (fprintf (current-error-port)\n \" WARNING: ~a: ~a~n\" sls\n (if (message-condition? exn)\n (condition-message exn)\n exn))]\n [else\n (fprintf (current-error-port)\n \" ERROR: ~a raised: ~a~n\" sls\n (if (message-condition? exn)\n (condition-message exn)\n exn))\n ;; Delete any partial/stub .so left behind by the aborted compile\n (when (file-exists? so) (delete-file so))\n (raise exn)]))\n (lambda () (compile-library sls)))])))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (library-directories))])\n ;; Compat layer first\n (compile-jsh-module \"../compat/gambit\")\n ;; FFI module (must be compiled even for static builds — load-shared-object\n ;; calls happen at runtime, not compile time)\n (for-each compile-jsh-module '(\"ffi\"))\n ;; Embed (ffi -> embed-data -> embed)\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n ;; Tier 1: no deps\n (for-each compile-jsh-module '(\"ast\" \"registry\"))\n ;; Tier 2+\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n ;; Coreutils integration (calls Rust uutils via FFI — no Scheme coreutils modules)\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-musl \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n;; compile-imported-libraries is needed so compile-program can resolve\n;; imports from (jsh registry) etc. The .so files from step 1 already\n;; exist, so Chez will use them rather than recompiling.\n(parameterize ([compile-imported-libraries #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (append\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (library-directories))])\n (compile-program \"jsh-generated.ss\"))\n\n;; ========== Step 3: Skip WPO for musl builds ==========\n;; WPO requires all .wpo files with matching compilation instances,\n;; which is fragile. Use the direct jsh-generated.so from compile-program instead.\n(printf \"[3/7] Skipping WPO (using jsh-generated.so directly)...~n\")\n(define program-so \"jsh-generated.so\")\n\n;; ========== Step 3.5: Pre-compile all boot-file dependencies ==========\n;; In a clean build (e.g. container build), --compile-imported-libraries only compiles\n;; modules transitively imported by jsh. Some boot-file entries (std/stm,\n;; std/cli/getopt, etc.) are not imported by jsh modules but must exist as .so\n;; for make-boot-file. Compile them explicitly here.\n\n(let ([boot-jerboa-modules\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\")])\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 2]\n [generate-inspector-information #f])\n (for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [so (format \"~a/~a.so\" jerboa-dir m)])\n (when (and (file-exists? sls) (not (file-exists? so)))\n (printf \" Pre-compiling ~a~n\" sls)\n (guard (e [#t\n (printf \" !! compile-library ~a failed: ~a~n\"\n m (call-with-string-output-port\n (lambda (p) (display-condition e p))))])\n (compile-library sls))\n ;; If compile-library succeeded but didn't write .so (because\n ;; transitive imports failed), retry by loading the .sls — loading\n ;; with compile-imported-libraries forces dep-first compilation.\n (unless (file-exists? so)\n (printf \" .so missing after compile-library; loading ~a to force transitive compile~n\" sls)\n (guard (e [#t\n (printf \" !! load ~a failed: ~a~n\"\n m (call-with-string-output-port\n (lambda (p) (display-condition e p))))])\n (load sls))))))\n boot-jerboa-modules)))\n\n;; ========== Step 4: Create libs-only boot file ==========\n;; NOTE: The program is NOT in the boot file — it's loaded separately\n;; via Sscheme_script to preserve threading support.\n\n(printf \"[4/7] Creating libs-only boot file...~n\")\n(define (filter-existing-sos files)\n (let ([missing (filter (lambda (f) (not (file-exists? f))) files)])\n (unless (null? missing)\n (printf \" WARNING: ~a .so files missing from boot list, skipping:~n\" (length missing))\n (for-each (lambda (m) (printf \" ~a~n\" m)) missing))\n (filter file-exists? files)))\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (filter-existing-sos\n (append\n ;; Jerboa runtime + stdlib\n (map (lambda (m) (format \"~a/~a.so\" jerboa-dir m))\n '(\"jerboa/core\"\n \"jerboa/runtime\"\n \"std/error\"\n \"std/error/conditions\"\n \"std/format\"\n \"std/sort\"\n \"std/pregexp\"\n \"std/regex\"\n \"std/match2\"\n \"std/sugar\"\n \"std/misc/string\"\n \"std/misc/string-more\"\n \"std/misc/list\"\n \"std/misc/alist\"\n \"std/misc/thread\"\n \"std/stm\"\n \"std/foreign\"\n \"std/os/path\"\n \"std/os/path-caps\"\n \"std/os/platform\"\n \"std/os/posix\"\n \"std/os/limits\"\n \"std/os/supervise\"\n \"std/os/limits/sandbox\"\n \"std/os/tracefs\"\n \"std/net/allowlist\"\n \"std/net/address\"\n \"std/os/signal\"\n \"std/os/fdio\"\n \"std/transducer\"\n \"std/log\"\n \"std/capability\"\n \"std/capability/sandbox\"\n \"std/security/capsicum\"\n \"std/os/landlock\"\n \"std/os/sandbox\"\n \"std/security/landlock\"\n \"std/security/seatbelt\"\n \"std/security/cage\"\n \"std/security/seccomp\"\n ;; New stdlib modules (jerboa latest)\n \"std/misc/lru-cache\"\n \"std/misc/trie\"\n \"std/text/glob\"\n \"std/misc/process\"\n \"std/gambit-compat\"\n ;; Stdlib integrations\n \"std/misc/guardian-pool\"\n \"std/misc/diff\"\n \"std/misc/fmt\"\n \"std/misc/terminal\"\n \"std/misc/custodian\"\n \"std/misc/profile\"\n \"std/misc/memoize\"\n \"std/misc/config\"\n ;; Actor transport (for recording streaming)\n \"std/actor/mpsc\"\n \"std/actor/core\"\n \"std/net/tcp-raw\"\n \"std/crypto/native\"\n \"std/crypto/random\"\n \"std/crypto/native-rust\"\n \"std/actor/transport\"))\n ;; Local compat layer\n (list \"src/compat/gambit.so\")\n ;; Additional jerboa stdlib modules needed by coreutils\n (map (lambda (m) (format \"~a/~a.so\" jerboa-dir m))\n '(\"std/cli/getopt\"\n \"std/misc/ports\"\n \"std/crypto/digest\"\n \"std/srfi/srfi-13\"\n \"std/srfi/srfi-115\"\n \"std/text/base64\"\n ;; Networking: rustls TLS + HTTP/HTTPS client (used by jerboa-aws)\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; jerboa-crypto removed — mux-auth now uses (std crypto native-rust) backed by Rust\n ;; jerboa-ssh (SSH agent + sub-libraries)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" ssh-stage))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" ssh-stage m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" ssh-stage)))\n '())\n ;; jsqlite is compiled through normal library dependencies.\n ;; jerboa-coreutils: replaced by Rust uutils (no Scheme .so files needed)\n ;; jerboa-awk (AWK interpreter, pure Scheme)\n (map (lambda (m) (format \"~a/~a.so\" awk-stage m))\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed (sed stream editor, Rust-backed PCRE2)\n (map (lambda (m) (format \"~a/~a.so\" sed-stage m))\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-ssl + jerboa-https removed — jerboa-aws now uses (std net request) (rustls)\n ;; jerboa-aws (optional — only included when aws-dir has the module)\n (if has-aws?\n (map (lambda (m) (format \"~a/~a.so\" aws-stage m))\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\" \"jerboa-aws/time\"\n \"jerboa-aws/crypto\" \"jerboa-aws/creds\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/request\" \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jerboa-fuse / vault (encrypted FUSE vault)\n (if has-jerboa-fuse?\n (map (lambda (m) (format \"~a/~a.so\" vault-stage m))\n '(\"chez/vault/format\" \"chez/fuse/constants\" \"chez/fuse/types\"\n \"chez/fuse/mount\" \"chez/fuse/codec\" \"chez/fuse/secmem\"\n \"chez/fuse/access\" \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\"))\n '())\n ;; jsh modules\n (map (lambda (m) (format \"src/jsh/~a.so\" m))\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\" \"config\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"\n \"coreutils\")))))\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-musl-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define musl-lib-dir (musl-chez-lib-dir))\n(define gcc (musl-gcc-path))\n(define scheme-h-dir musl-lib-dir)\n;; Hardening: strip source paths from the binary to prevent information leakage\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" (or (getenv \"HOME\") \"/root\") \"=~\"))\n\n;; Get boot file paths from musl Chez installation\n(define musl-boots (musl-boot-files))\n(define petite-boot-path (cdr (assoc \"petite\" musl-boots)))\n(define scheme-boot-path (cdr (assoc \"scheme\" musl-boots)))\n\n;; Generate static_boot.c: embeds petite.boot + scheme.boot + jsh.boot\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n ;; Embed boot files\n (display (file->c-array petite-boot-path \"petite_boot\") out)\n (newline out)\n (display (file->c-array scheme-boot-path \"scheme_boot\") out)\n (newline out)\n (display (file->c-array \"jsh.boot\" \"jsh_boot\") out)\n (newline out)\n ;; static_boot_init for Chez's main.o\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", jsh_boot, jsh_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Generate jsh_program_embed.c: embeds the optimized program .so\n;; and provides a custom main that:\n;; - Saves args to env vars (bypass Chez arg parsing)\n;; - Calls static_boot_init + Sbuild_heap\n;; - Loads program via memfd + Sscheme_script (threading workaround)\n(define program-c (format \"~a/jsh_main_musl.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#define _GNU_SOURCE\\n\" out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/ioctl.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <netdb.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n ;; Conditionally define HAS_JERBOA_NATIVE when Rust library is available\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n ;; dlopen/dlsym stubs for static builds.\n ;; Coreutils modules call (load-shared-object #f) at init time, which\n ;; calls dlopen(NULL). In musl static builds, dlopen always fails.\n ;; These stubs make dlopen(NULL) succeed (returning a dummy handle)\n ;; while actual symbol lookup goes through Sforeign_symbol.\n (display \"/* dlopen stubs — override musl's failing stubs in static builds */\\n\" out)\n (display \"void *dlopen(const char *filename, int flags) {\\n\" out)\n (display \" (void)flags;\\n\" out)\n (display \" (void)filename;\\n\" out)\n (display \" return (void*)1; /* all symbols pre-registered via Sforeign_symbol */\\n\" out)\n (display \"}\\n\" out)\n (display \"void *dlsym(void *handle, const char *symbol) {\\n\" out)\n (display \" (void)handle; (void)symbol;\\n\" out)\n (display \" return NULL; /* symbols found via Sforeign_symbol */\\n\" out)\n (display \"}\\n\" out)\n (display \"int dlclose(void *handle) { (void)handle; return 0; }\\n\" out)\n (display \"char *dlerror(void) { return NULL; }\\n\\n\" out)\n ;; Embed program .so\n (display (file->c-array program-so \"jsh_program_data\") out)\n (newline out)\n ;; Declare static_boot_init (defined in static_boot.c)\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n ;; Declare all FFI functions from ffi-shim.c\n ;; List is auto-generated by tools/extract-ffi-symbols.sh from ffi-shim.c.\n (display \"/* FFI symbols from ffi-shim.c (auto-generated via ffi-shim-symbols.list) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n ;; Non-ffi_ helpers that are used alongside the whitelist. These are\n ;; defined in ffi-shim.c but do not carry the ffi_ prefix, so the\n ;; auto-extractor does not pick them up.\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\"))\n ;; Rust native library symbols — only declared when libjerboa_native.a exists\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n ;; TLS (rustls)\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n ;; TLS mTLS (mutual TLS)\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n ;; Hardening (extern void OK — only used via Sforeign_symbol)\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n ;; X509 certificate generation\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\" \"jerboa_x509_generate_signed_by_ca_mem\" \"jerboa_x509_cert_fingerprint\"\n ;; SOCKS5 proxy server\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n ;; These three are called directly in main() — need proper return type\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n (display \"#endif\\n\" out)\n ;; Weak stubs for mtls functions missing from older musl-target native libs\n (display \"/* Weak stubs: overridden if libjerboa_native.a provides them */\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n ;; jerboa-fuse vault FFI symbols (from ffi-shim.c)\n (when has-jerboa-fuse?\n (display \"/* FFI symbols from ffi-shim.c (jerboa-fuse vault) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n ;; Rust native crypto symbols used by vault/crypto.sls (replaced OpenSSL)\n (display \"/* Rust native crypto symbols for vault/crypto.sls */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jerboa_random_bytes\" \"jerboa_pbkdf2_derive\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\")))\n ;; jerboa-crypto removed — mux-auth uses Rust native crypto\n ;; Rust uutils/coreutils FFI symbols (from libjsh_coreutils.a)\n (display \"/* FFI symbols from libjsh_coreutils.a (Rust uutils) */\\n\" out)\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n '(\"jsh_ls\" \"jsh_dir\" \"jsh_vdir\" \"jsh_stat\" \"jsh_du\" \"jsh_df\"\n \"jsh_dircolors\" \"jsh_pathchk\"\n \"jsh_cat\" \"jsh_cp\" \"jsh_mv\" \"jsh_rm\" \"jsh_ln\"\n \"jsh_mkdir\" \"jsh_rmdir\" \"jsh_mktemp\" \"jsh_touch\"\n \"jsh_link\" \"jsh_unlink\" \"jsh_readlink\" \"jsh_cu_realpath\"\n \"jsh_install\" \"jsh_shred\" \"jsh_truncate\" \"jsh_mkfifo\" \"jsh_mknod\" \"jsh_dd\"\n \"jsh_chmod\" \"jsh_chown\" \"jsh_chgrp\"\n \"jsh_head\" \"jsh_tail\" \"jsh_tac\" \"jsh_tee\" \"jsh_wc\" \"jsh_nl\"\n \"jsh_fold\" \"jsh_expand\" \"jsh_unexpand\" \"jsh_fmt\"\n \"jsh_cut\" \"jsh_paste\" \"jsh_join\" \"jsh_comm\"\n \"jsh_sort\" \"jsh_uniq\" \"jsh_tr\" \"jsh_numfmt\"\n \"jsh_grep\"\n \"jsh_id\" \"jsh_whoami\" \"jsh_hostname\" \"jsh_uname\" \"jsh_uptime\"\n \"jsh_who\" \"jsh_groups\" \"jsh_users\" \"jsh_pinky\" \"jsh_logname\"\n \"jsh_arch\" \"jsh_nproc\" \"jsh_tty\" \"jsh_hostid\" \"jsh_date\"\n \"jsh_seq\" \"jsh_expr\" \"jsh_factor\"\n \"jsh_base64\" \"jsh_base32\" \"jsh_basenc\" \"jsh_od\"\n \"jsh_cksum\" \"jsh_md5sum\" \"jsh_sha1sum\" \"jsh_sha224sum\"\n \"jsh_sha256sum\" \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_b2sum\" \"jsh_sum\"\n \"jsh_env\" \"jsh_timeout\" \"jsh_nice\" \"jsh_nohup\" \"jsh_chroot\"\n \"jsh_kill\"\n \"jsh_echo\" \"jsh_printf\" \"jsh_sleep\" \"jsh_yes\" \"jsh_printenv\"\n \"jsh_pwd\" \"jsh_sync\" \"jsh_test\" \"jsh_shuf\" \"jsh_split\" \"jsh_csplit\"\n \"jsh_tsort\" \"jsh_stty\" \"jsh_pr\" \"jsh_ptx\"\n \"jsh_basename\" \"jsh_dirname\"))\n ;; jerboa-ssh FFI symbols\n (display \"/* FFI symbols from jerboa_ssh_shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n ;; jerboa-ssl FFI symbols — TLS replaced by jerboa_tls_* (rustls)\n ;; jerboa_ssl_shim.c is no longer compiled; emit weak stubs so the linker\n ;; doesn't fail if any old foreign-procedure reference remains in a .so.\n (display \"/* jerboa-ssl stub symbols — TLS replaced by jerboa_tls_* (rustls) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"__attribute__((weak)) void ~a(void) { }\\n\" name))\n '(\"jerboa_ssl_init\" \"jerboa_ssl_cleanup\"\n \"jerboa_ssl_connect\" \"jerboa_ssl_write\" \"jerboa_ssl_read\"\n \"jerboa_ssl_read_all\" \"jerboa_ssl_free_buf\" \"jerboa_ssl_close\"\n \"jerboa_ssl_memcpy\"\n \"jerboa_tcp_listen\" \"jerboa_tcp_accept\"\n \"jerboa_tcp_connect\" \"jerboa_tcp_close\"\n \"jerboa_tcp_read\" \"jerboa_tcp_write\" \"jerboa_tcp_read_all\"\n \"jerboa_tcp_set_timeout\"\n \"jerboa_ssl_server_ctx\" \"jerboa_ssl_server_accept\" \"jerboa_ssl_server_ctx_free\"\n \"jerboa_tcp_conn_wrap\" \"jerboa_conn_write\" \"jerboa_conn_read\"))\n ;; jsqlite is pure Jerboa; no native sqlite symbols are registered.\n (newline out)\n ;; Wrapper functions for variadic/macro POSIX functions\n ;; (must appear before register_ffi_symbols which takes their address)\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n ;; Register all FFI symbols so Chez foreign-procedure can find them\n ;; (load-shared-object is disabled in static builds)\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n ;; ffi-shim.c functions — list is auto-generated from ffi-shim.c via\n ;; tools/extract-ffi-symbols.sh, so adding a new ffi_* to the C file\n ;; automatically picks it up at the next build.\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Non-ffi_ helpers that live in ffi-shim.c (Cage/Landlock wrappers and\n ;; Rust-native shims) — kept as an explicit list because the prefix\n ;; filter only picks up ffi_* names.\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\"))\n ;; Rust native library symbols — only registered when libjerboa_native.a exists\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n ;; TLS (rustls)\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n ;; TLS mTLS (mutual TLS)\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n ;; Hardening: antidebug, seccomp, integrity\n \"jerboa_antidebug_ptrace\" \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\" \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n ;; X509 certificate generation\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\" \"jerboa_x509_generate_signed_by_ca_mem\" \"jerboa_x509_cert_fingerprint\"\n ;; SOCKS5 proxy server\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n (display \"#endif\\n\" out))\n ;; jerboa-crypto removed — mux-auth uses Rust native crypto\n ;; POSIX functions used via foreign-procedure in ffi.sls\n ;; These are real C functions (not macros) from musl libc\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\" \"execve\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"pipe\" \"sigemptyset\" \"sigfillset\" \"sigaddset\" \"sigdelset\"\n \"sigismember\" \"sigprocmask\" \"sigwait\"\n \"tcgetattr\" \"tcsetattr\" \"ioctl\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"setuid\" \"setgid\"\n \"setenv\" \"unsetenv\"\n ;; POSIX functions used by awk/sed and other Scheme modules\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"stat\" \"fstat\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"setpriority\" \"getrlimit\" \"setrlimit\" \"strftime\" \"localtime\"\n \"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"getsockopt\"\n \"getaddrinfo\" \"freeaddrinfo\"\n \"htons\" \"ntohs\" \"inet_pton\" \"inet_ntop\" \"inet_addr\"\n \"listen\" \"accept\" \"connect\"\n \"strerror\"))\n ;; mkdir needs a wrapper (may be a macro on some platforms)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\"))\n ;; Variadic/macro POSIX functions need wrappers (defined above)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; Rust uutils/coreutils functions\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jsh_ls\" \"jsh_dir\" \"jsh_vdir\" \"jsh_stat\" \"jsh_du\" \"jsh_df\"\n \"jsh_dircolors\" \"jsh_pathchk\"\n \"jsh_cat\" \"jsh_cp\" \"jsh_mv\" \"jsh_rm\" \"jsh_ln\"\n \"jsh_mkdir\" \"jsh_rmdir\" \"jsh_mktemp\" \"jsh_touch\"\n \"jsh_link\" \"jsh_unlink\" \"jsh_readlink\" \"jsh_cu_realpath\"\n \"jsh_install\" \"jsh_shred\" \"jsh_truncate\" \"jsh_mkfifo\" \"jsh_mknod\" \"jsh_dd\"\n \"jsh_chmod\" \"jsh_chown\" \"jsh_chgrp\"\n \"jsh_head\" \"jsh_tail\" \"jsh_tac\" \"jsh_tee\" \"jsh_wc\" \"jsh_nl\"\n \"jsh_fold\" \"jsh_expand\" \"jsh_unexpand\" \"jsh_fmt\"\n \"jsh_cut\" \"jsh_paste\" \"jsh_join\" \"jsh_comm\"\n \"jsh_sort\" \"jsh_uniq\" \"jsh_tr\" \"jsh_numfmt\"\n \"jsh_grep\"\n \"jsh_id\" \"jsh_whoami\" \"jsh_hostname\" \"jsh_uname\" \"jsh_uptime\"\n \"jsh_who\" \"jsh_groups\" \"jsh_users\" \"jsh_pinky\" \"jsh_logname\"\n \"jsh_arch\" \"jsh_nproc\" \"jsh_tty\" \"jsh_hostid\" \"jsh_date\"\n \"jsh_seq\" \"jsh_expr\" \"jsh_factor\"\n \"jsh_base64\" \"jsh_base32\" \"jsh_basenc\" \"jsh_od\"\n \"jsh_cksum\" \"jsh_md5sum\" \"jsh_sha1sum\" \"jsh_sha224sum\"\n \"jsh_sha256sum\" \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_b2sum\" \"jsh_sum\"\n \"jsh_env\" \"jsh_timeout\" \"jsh_nice\" \"jsh_nohup\" \"jsh_chroot\"\n \"jsh_kill\"\n \"jsh_echo\" \"jsh_printf\" \"jsh_sleep\" \"jsh_yes\" \"jsh_printenv\"\n \"jsh_pwd\" \"jsh_sync\" \"jsh_test\" \"jsh_shuf\" \"jsh_split\" \"jsh_csplit\"\n \"jsh_tsort\" \"jsh_stty\" \"jsh_pr\" \"jsh_ptx\"\n \"jsh_basename\" \"jsh_dirname\"))\n ;; jerboa-ssh functions\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n ;; jerboa-ssl functions\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_ssl_init\" \"jerboa_ssl_cleanup\"\n \"jerboa_ssl_connect\" \"jerboa_ssl_write\" \"jerboa_ssl_read\"\n \"jerboa_ssl_read_all\" \"jerboa_ssl_free_buf\" \"jerboa_ssl_close\"\n \"jerboa_ssl_memcpy\"\n \"jerboa_tcp_listen\" \"jerboa_tcp_accept\"\n \"jerboa_tcp_connect\" \"jerboa_tcp_close\"\n \"jerboa_tcp_read\" \"jerboa_tcp_write\" \"jerboa_tcp_read_all\"\n \"jerboa_tcp_set_timeout\"\n \"jerboa_ssl_server_ctx\" \"jerboa_ssl_server_accept\" \"jerboa_ssl_server_ctx_free\"\n \"jerboa_tcp_conn_wrap\" \"jerboa_conn_write\" \"jerboa_conn_read\"))\n ;; POSIX socket functions used by (std net tcp-raw) for actor transport\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\"\n \"htons\" \"inet_pton\" \"__errno_location\"))\n ;; listen/accept/connect need special handling — listen conflicts with C\n ;; We register them with distinct names and wrap in tcp-raw\n ;; Actually these are plain C functions, no conflict:\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"listen\" \"accept\" \"connect\"))\n ;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n (when has-jerboa-fuse?\n (display \" /* jerboa-fuse vault FFI symbols */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n ;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n ;; jerboa_aead_seal, jerboa_aead_open — all exported by libjerboa_native.a (ring)\n ;; POSIX symbols NOT already registered by Chez (open/read/write/close ARE)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\")))\n ;; jsqlite is pure Jerboa; no sqlite shim is linked.\n (display \"}\\n\\n\" out)\n ;; Custom main\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" /* Initialize Rust stdlib args for musl (glibc .init_array won't fire) */\\n\" out)\n (display \" jsh_coreutils_init(argc, argv);\\n\\n\" out)\n (display \" /* Ensure fds 0/1/2 point to /dev/null when closed (e.g. fork-exec from ,server).\\n\" out)\n (display \" * Without this, memfd_create reuses fd 0, Chez boot opens ports on wrong fds,\\n\" out)\n (display \" * and the server process crashes silently. */\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n (display \" /* Save args in env vars (bypass Chez arg parsing) */\\n\" out)\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n (display \" /* Resolve real exe path for $SHELL */\\n\" out)\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" ssize_t len = readlink(\\\"/proc/self/exe\\\", exe_buf, sizeof(exe_buf) - 1);\\n\" out)\n (display \" if (len > 0) { exe_buf[len] = '\\\\0'; setenv(\\\"JSH_EXE\\\", exe_buf, 1); }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening — runs before Chez init for strongest protection.\n ;; Guarded by HAS_JERBOA_NATIVE and !JSH_DEV env var.\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" /* Phase 0: C-level hardening — before Chez init */\\n\" out)\n (display \" /* NOTE: ptrace(TRACEME) is NOT used here — it makes the parent\\n\" out)\n (display \" the tracer, causing SIGSTOP on signal delivery. Fatal for a\\n\" out)\n (display \" shell that relies on SIGCHLD/SIGWINCH. Instead we detect\\n\" out)\n (display \" existing tracers and use seccomp to block future attachment. */\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n (display \" /* Initialize Chez + register embedded boot files */\\n\" out)\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\\n\" out)\n (display \" /* Register FFI symbols after heap is built */\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n (display \" /* Load program via memfd (threading workaround) */\\n\" out)\n (display \" int fd = memfd_create(\\\"jsh-program\\\", 1 /* MFD_CLOEXEC */);\\n\" out)\n (display \" if (fd < 0) { perror(\\\"memfd_create\\\"); return 1; }\\n\" out)\n (display \" if (write(fd, jsh_program_data, jsh_program_data_len) != (ssize_t)jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"write\\\"); close(fd); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" char prog_path[64];\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"/proc/self/fd/%d\\\", fd);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" close(fd);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C with musl-gcc ==========\n\n(printf \"[6/7] Compiling C with musl-gcc...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-musl \"Command failed\" cmd)))\n\n;; Compile static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir\n build-dir static-boot-c))\n\n;; Compile jsh_main_musl.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_musl.o' '~a'\"\n gcc harden-cflags scheme-h-dir\n build-dir program-c))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; Compile ffi-shim.c with musl-gcc\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; Compile landlock-shim.c from jerboa\n(run-cmd (format \"~a -c -O2 ~a -o '~a/landlock-shim.o' '~a/support/landlock-shim.c' -Wall\"\n gcc harden-cflags build-dir jerboa-dir-base))\n\n;; Coreutils FFI shim no longer needed — replaced by Rust uutils (libjsh_coreutils.a)\n;; Generate empty .o to satisfy link step (will be removed once link step is updated)\n(system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))\n\n;; Compile jerboa-ssh shim (mirrors build-jsh-macos.ss / build-jsh-freebsd.ss)\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Compile jerboa-ssh shim WITHOUT OpenSSL — ed25519/etc. come from Rust\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf — compile if upstream jerboa-ssh ships it, else empty stub\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c — no longer compiled (OpenSSL removed for musl);\n ;; SSH transport crypto goes through Rust ring via libjerboa_native.\n ;; Generate empty .o placeholder for the linker.\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir)))\n (begin\n (printf \" Warning: jerboa-ssh shim not found, building without SSH agent~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-ssl shim — no longer compiled; TLS now via jerboa_tls_* (rustls)\n(printf \" [skip] jerboa-ssl shim — replaced by jerboa_tls_* (rustls)~n\")\n\n;; jerboa-crypto shim: no longer compiled — vault crypto now uses jerboa_native (ring)\n;; glibc-compat shim: no longer needed — libcrypto.a removed\n(printf \" [skip] jerboa-crypto shim and glibc-compat — vault crypto now via jerboa_native (ring)~n\")\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking static jsh-musl binary...~n\")\n\n(let ([link-cmd (apply musl-link-command\n \"jsh-musl\"\n (list (format \"~a/jsh_main_musl.o\" build-dir)\n (format \"~a/static_boot.o\" build-dir)\n (format \"~a/ffi-shim.o\" build-dir)\n (format \"~a/embed-crypto.o\" build-dir)\n (format \"~a/coreutils-ffi.o\" build-dir)\n (format \"~a/jerboa-ssh-shim.o\" build-dir)\n (format \"~a/jerboa-ssh-crypto.o\" build-dir)\n (format \"~a/ed25519-standalone.o\" build-dir)\n (format \"~a/bcrypt_pbkdf.o\" build-dir)\n (format \"~a/landlock-shim.o\" build-dir)\n ;; jerboa-crypto-shim.o removed — vault crypto now via jerboa_native (ring)\n ;; jsqlite is pure Jerboa; no sqlite shim object needed\n ;; jerboa-ssl-shim.o removed — TLS now via jerboa_tls_* (rustls)\n ;; glibc-compat.o removed — was only needed for libcrypto.a\n )\n ;; Rust native library (crypto, regex, landlock, etc.)\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n (append\n ;; Rust native library — provides crypto, regex, landlock, etc.\n (if has-native-lib?\n (list native-lib-path)\n '())\n ;; Rust uutils/coreutils library\n (list rust-coreutils-lib-path))\n ;; Use plain -static (not -static-pie):\n ;; GCC 13+ libgcc_eh.a references _dl_find_object (glibc 2.35+)\n ;; which doesn't exist in musl, breaking -static-pie builds\n '(no-harden: #t))])\n ;; C landlock-shim and Rust lib both define jerboa_landlock_abi_version\n ;; (Rust LTO packs all symbols into one .o, causing duplicate when linked)\n ;; Android: use custom CRT that skips musl self-relocation (linker64 does it).\n ;; -nostartfiles: don't link rcrt1.o (self-relocating CRT)\n ;; android-crt.o: custom _start → __libc_start_main (no self-relocation)\n ;; crti.o/crtn.o: .init/.fini section support\n ;; -z noseparate-code: first PT_LOAD at offset 0 for linker64 PHDR lookup\n (run-cmd (string-append link-cmd\n \" -Wl,--allow-multiple-definition\"\n (if (getenv \"JSH_ANDROID\")\n (string-append\n \" -nostartfiles\"\n \" /tmp/android-crt.o\"\n \" /usr/lib/aarch64-linux-musl/crti.o\"\n \" /usr/lib/aarch64-linux-musl/crtn.o\"\n \" -Wl,-z,noseparate-code\")\n \"\"))))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? \"jsh-musl\")\n (if (getenv \"JSH_NO_STRIP\")\n (printf \"~n[harden] Skipping strip (JSH_NO_STRIP set)~n\")\n (begin\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-musl\"))])\n ;; Strip all symbol tables and debug info\n (run-cmd \"strip --strip-all jsh-musl\")\n ;; Remove section headers (prevents section-based disassembly)\n ;; objcopy --strip-section-headers requires binutils >= 2.40\n ;; Skip on Android: linker requires section headers for static-pie binaries\n (if (getenv \"JSH_KEEP_SECTION_HEADERS\")\n (printf \" Section headers preserved (JSH_KEEP_SECTION_HEADERS set)~n\")\n (when (= 0 (system \"objcopy --strip-section-headers jsh-musl 2>/dev/null\"))\n (printf \" Section headers removed~n\")))\n (let ([post-size (file-length (open-file-input-port \"jsh-musl\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))))\n\n ;; Compute SHA-256 integrity hash for runtime verification.\n ;; The binary checks for jsh-musl.sha256 at startup (see harden.sls).\n ;; Write raw 32-byte hash using pure Scheme (no xxd dependency).\n (printf \"[harden] Computing integrity hash...~n\")\n (system \"sha256sum jsh-musl | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\")\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-musl.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-musl.sha256 (32 bytes)~n\")))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n;; coreutils-stage no longer needed — Rust uutils replaces Scheme coreutils\n;; ssl-stage removed — jerboa-ssl/jerboa-https no longer used (rustls replaces them)\n(system (format \"rm -rf '~a'\" aws-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Static binary created: jsh-musl~n~n\")\n(system \"ls -lh jsh-musl\")\n(printf \"~n\")\n(system \"file jsh-musl\")\n(printf \"~nTest: ./jsh-musl -c 'echo Hello from static jsh'~n\")\n"} +{"text":";; FILE: jerboa-shell/build-jsh-musl.ss\n#!chezscheme\n;;; build-jsh-musl.ss — Build a fully static jsh binary using musl libc\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib> < build-jsh-musl.ss\n;;;\n;;; This script:\n;;; 1. Compiles jsh modules (using stock scheme with glibc)\n;;; 2. Creates boot file + optimized program .so\n;;; 3. Generates C files with embedded boot data\n;;; 4. Compiles C with musl-gcc against musl-built Chez's scheme.h\n;;; 5. Links fully static binary with libkernel.a from musl-built Chez\n;;;\n;;; The resulting jsh-musl binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (jerboa build musl)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Validate musl setup ==========\n\n(let ([result (validate-musl-setup)])\n (unless (eq? (car result) 'ok)\n (printf \"Error: ~a~n\" (cdr result))\n (printf \"~nTo build Chez Scheme with musl:~n\")\n (printf \" cd ~/mine/ChezScheme~n\")\n (printf \" ./configure --threads --static CC=musl-gcc --installprefix=$HOME/chez-musl~n\")\n (printf \" make -j$(nproc) && make install~n\")\n (exit 1)))\n\n(printf \"musl Chez found: ~a~n~n\" (musl-chez-lib-dir))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/root\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-musl.sh so we know the repo root.\n(define vendor-dir\n (let ([script-dir (getenv \"SCRIPT_DIR\")])\n (if script-dir\n (format \"~a/vendor\" script-dir)\n (let ([cwd-vendor \"./vendor\"])\n (if (file-directory? cwd-vendor) cwd-vendor\n (format \"~a/mine/jerboa-shell/vendor\" home-dir))))))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. The container build sets HOME=/build and\n;; clones all deps into /build/mine/, so the ~/mine/ fallback covers that.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n(define ffi-shim-symbols (read-symbol-list \"ffi-shim-symbols.list\"))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n;; Base jerboa directory (parent of lib/) — for support/ files\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n;; jerboa-ssh library (SSH agent)\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n;; jsqlite library\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jerboa-sqlite/src\" home-dir)))\n\n;; jerboa-crypto library (AEAD, HMAC, scrypt for mux auth)\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n;; jerboa-coreutils: replaced by Rust uutils/coreutils (libjsh_coreutils.a)\n;; No coreutils-dir needed — Scheme coreutils modules are no longer compiled\n\n;; jerboa-awk library\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n;; jerboa-sed library\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n;; Rust coreutils static library (replaces jerboa-coreutils Scheme modules)\n(define rust-coreutils-lib-path\n (format \"~a/mine/jerboa-shell/rust-coreutils/target/x86_64-unknown-linux-musl/release/libjsh_coreutils.a\"\n home-dir))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — will try native target path~n\")\n (set! rust-coreutils-lib-path\n (format \"~a/mine/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\"\n home-dir))\n (set! has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n (unless has-rust-coreutils?\n (printf \" ERROR: No libjsh_coreutils.a found. Run: cd rust-coreutils && cargo build --release --target x86_64-unknown-linux-musl~n\")\n (exit 1)))\n\n;; jerboa-aws library\n;; HTTP/HTTPS is provided by (std net request) → (std net tls-rustls) (rustls).\n;; jerboa-ssl/jerboa-https are no longer used — they require dynamic OpenSSL via\n;; load-shared-object, which fails in static builds, and rustls is preferred\n;; over OpenSSL for security.\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; jerboa-fuse library (encrypted FUSE vault)\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n;; Rust native library — check once, reuse in C code generation and linking.\n;; Prefer the vendored Rust sources (vendor/jerboa/jerboa-native-rs) so that\n;; `git clean -xfd && make jsh-musl` works on a fresh checkout. Fall back to\n;; the developer's ~/mine/jerboa checkout for iterative local work.\n(define native-lib-path\n (let* ([relpath \"jerboa-native-rs/target/x86_64-unknown-linux-musl/release/libjerboa_native.a\"]\n [vendor-path (format \"~a/jerboa/~a\" vendor-dir relpath)]\n [mine-path (format \"~a/mine/jerboa/~a\" home-dir relpath)])\n (cond\n [(file-exists? vendor-path) vendor-path]\n [(file-exists? mine-path) mine-path]\n [else vendor-path]))) ; report the vendor path in the warning\n(define has-native-lib? (file-exists? native-lib-path))\n(when (and has-native-lib?\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\")\n (printf \" Looked in vendor/jerboa/... and ~~/mine/jerboa/...~n\"))\n\n;; ========== Step 0: Coreutils (Rust uutils) ==========\n;; Coreutils are now provided by Rust uutils/coreutils (libjsh_coreutils.a).\n;; No Scheme coreutils staging needed — the Rust library provides all builtins\n;; via FFI (jsh_ls, jsh_cat, etc.) called from (jsh coreutils).\n(printf \"[0/7] Coreutils: using Rust uutils (~a)~n\" rust-coreutils-lib-path)\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n;; jerboa-awk: pure Scheme, no patching needed — just copy and compile\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: needs pcre2 patched to use Rust regex instead of C PCRE2\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n;; Replace pcre2.sls with Rust-backed version\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if has-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n;; jerboa-aws: copy entire tree (optional — only if aws-dir contains jerboa-aws)\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when has-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n ;; Delete pre-compiled .so and .wpo files\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(unless has-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (printf \" jerboa-aws not found, skipping~n\"))\n\n;; Compile all jerboa-aws modules in dependency order\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons aws-stage aws-stage)\n (library-directories))])\n ;; jerboa-aws modules in dependency order (only if present)\n (when has-aws?\n (printf \" Compiling jerboa-aws...~n\")\n ;; Core modules first\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n ;; Services\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n ;; CLI\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0c: Stage jerboa-ssh for static build ==========\n(printf \"[0c/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls' 2>/dev/null || true\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null || true\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build (Linux sed -i, not BSD -i '')\n (system (format \"find '~a' -name '*.sls' -exec sed -i 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"sed -i 's/base64-encode/b64-encode/g' '~a'\" kh))\n (system (format \"sed -i 's/base64-decode/b64-decode/g' '~a'\" kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0d: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0d/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n;; vault-stage/ is checked into the repo with pre-patched .sls files:\n;; - crypto.sls uses Rust native (ring) instead of OpenSSL\n;; - mount.sls uses ffi-shim instead of load-shared-object\n;; - blockstore/fuse.sls have load-shared-object calls removed\n;; Just clean stale compiled artifacts and compile what's there.\n(define vault-stage (format \"~a/vault-stage\" (current-directory)))\n\n(when has-jerboa-fuse?\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(and (file-exists? so)\n (time>=? (file-modification-time so) (file-modification-time sls)))\n (void)] ;; up to date from jsh-compile — skip\n [else\n (printf \" Compiling ~a...~n\" sls)\n ;; Use with-exception-handler instead of guard so continuable warnings\n ;; (e.g. Chez compile-time format-string warnings raised via\n ;; raise-continuable) don't abort the compile and leave a 15-byte stub\n ;; .so file behind. guard converts continuable raises to non-continuable.\n (with-exception-handler\n (lambda (exn)\n (cond\n [(warning? exn)\n (fprintf (current-error-port)\n \" WARNING: ~a: ~a~n\" sls\n (if (message-condition? exn)\n (condition-message exn)\n exn))]\n [else\n (fprintf (current-error-port)\n \" ERROR: ~a raised: ~a~n\" sls\n (if (message-condition? exn)\n (condition-message exn)\n exn))\n ;; Delete any partial/stub .so left behind by the aborted compile\n (when (file-exists? so) (delete-file so))\n (raise exn)]))\n (lambda () (compile-library sls)))])))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (library-directories))])\n ;; Compat layer first\n (compile-jsh-module \"../compat/gambit\")\n ;; FFI module (must be compiled even for static builds — load-shared-object\n ;; calls happen at runtime, not compile time)\n (for-each compile-jsh-module '(\"ffi\"))\n ;; Embed (ffi -> embed-data -> embed)\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n ;; Tier 1: no deps\n (for-each compile-jsh-module '(\"ast\" \"registry\"))\n ;; Tier 2+\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n ;; Coreutils integration (calls Rust uutils via FFI — no Scheme coreutils modules)\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-musl \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n;; compile-imported-libraries is needed so compile-program can resolve\n;; imports from (jsh registry) etc. The .so files from step 1 already\n;; exist, so Chez will use them rather than recompiling.\n(parameterize ([compile-imported-libraries #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (append\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (library-directories))])\n (compile-program \"jsh-generated.ss\"))\n\n;; ========== Step 3: Skip WPO for musl builds ==========\n;; WPO requires all .wpo files with matching compilation instances,\n;; which is fragile. Use the direct jsh-generated.so from compile-program instead.\n(printf \"[3/7] Skipping WPO (using jsh-generated.so directly)...~n\")\n(define program-so \"jsh-generated.so\")\n\n;; ========== Step 3.5: Pre-compile all boot-file dependencies ==========\n;; In a clean build (e.g. container build), --compile-imported-libraries only compiles\n;; modules transitively imported by jsh. Some boot-file entries (std/stm,\n;; std/cli/getopt, etc.) are not imported by jsh modules but must exist as .so\n;; for make-boot-file. Compile them explicitly here.\n\n(let ([boot-jerboa-modules\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\")])\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 2]\n [generate-inspector-information #f])\n (for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [so (format \"~a/~a.so\" jerboa-dir m)])\n (when (and (file-exists? sls) (not (file-exists? so)))\n (printf \" Pre-compiling ~a~n\" sls)\n (guard (e [#t\n (printf \" !! compile-library ~a failed: ~a~n\"\n m (call-with-string-output-port\n (lambda (p) (display-condition e p))))])\n (compile-library sls))\n ;; If compile-library succeeded but didn't write .so (because\n ;; transitive imports failed), retry by loading the .sls — loading\n ;; with compile-imported-libraries forces dep-first compilation.\n (unless (file-exists? so)\n (printf \" .so missing after compile-library; loading ~a to force transitive compile~n\" sls)\n (guard (e [#t\n (printf \" !! load ~a failed: ~a~n\"\n m (call-with-string-output-port\n (lambda (p) (display-condition e p))))])\n (load sls))))))\n boot-jerboa-modules)))\n\n;; ========== Step 4: Create libs-only boot file ==========\n;; NOTE: The program is NOT in the boot file — it's loaded separately\n;; via Sscheme_script to preserve threading support.\n\n(printf \"[4/7] Creating libs-only boot file...~n\")\n(define (filter-existing-sos files)\n (let ([missing (filter (lambda (f) (not (file-exists? f))) files)])\n (unless (null? missing)\n (printf \" WARNING: ~a .so files missing from boot list, skipping:~n\" (length missing))\n (for-each (lambda (m) (printf \" ~a~n\" m)) missing))\n (filter file-exists? files)))\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (filter-existing-sos\n (append\n ;; Jerboa runtime + stdlib\n (map (lambda (m) (format \"~a/~a.so\" jerboa-dir m))\n '(\"jerboa/core\"\n \"jerboa/runtime\"\n \"std/error\"\n \"std/error/conditions\"\n \"std/format\"\n \"std/sort\"\n \"std/pregexp\"\n \"std/regex\"\n \"std/match2\"\n \"std/sugar\"\n \"std/misc/string\"\n \"std/misc/string-more\"\n \"std/misc/list\"\n \"std/misc/alist\"\n \"std/misc/thread\"\n \"std/stm\"\n \"std/foreign\"\n \"std/os/path\"\n \"std/os/path-caps\"\n \"std/os/platform\"\n \"std/os/posix\"\n \"std/os/limits\"\n \"std/os/supervise\"\n \"std/os/limits/sandbox\"\n \"std/os/tracefs\"\n \"std/net/allowlist\"\n \"std/net/address\"\n \"std/os/signal\"\n \"std/os/fdio\"\n \"std/transducer\"\n \"std/log\"\n \"std/capability\"\n \"std/capability/sandbox\"\n \"std/security/capsicum\"\n \"std/os/landlock\"\n \"std/os/sandbox\"\n \"std/security/landlock\"\n \"std/security/seatbelt\"\n \"std/security/cage\"\n \"std/security/seccomp\"\n ;; New stdlib modules (jerboa latest)\n \"std/misc/lru-cache\"\n \"std/misc/trie\"\n \"std/text/glob\"\n \"std/misc/process\"\n \"std/gambit-compat\"\n ;; Stdlib integrations\n \"std/misc/guardian-pool\"\n \"std/misc/diff\"\n \"std/misc/fmt\"\n \"std/misc/terminal\"\n \"std/misc/custodian\"\n \"std/misc/profile\"\n \"std/misc/memoize\"\n \"std/misc/config\"\n ;; Actor transport (for recording streaming)\n \"std/actor/mpsc\"\n \"std/actor/core\"\n \"std/net/tcp-raw\"\n \"std/crypto/native\"\n \"std/crypto/random\"\n \"std/crypto/native-rust\"\n \"std/actor/transport\"))\n ;; Local compat layer\n (list \"src/compat/gambit.so\")\n ;; Additional jerboa stdlib modules needed by coreutils\n (map (lambda (m) (format \"~a/~a.so\" jerboa-dir m))\n '(\"std/cli/getopt\"\n \"std/misc/ports\"\n \"std/crypto/digest\"\n \"std/srfi/srfi-13\"\n \"std/srfi/srfi-115\"\n \"std/text/base64\"\n ;; Networking: rustls TLS + HTTP/HTTPS client (used by jerboa-aws)\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; jerboa-crypto removed — mux-auth now uses (std crypto native-rust) backed by Rust\n ;; jerboa-ssh (SSH agent + sub-libraries)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" ssh-stage))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" ssh-stage m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" ssh-stage)))\n '())\n ;; jsqlite is compiled through normal library dependencies.\n ;; jerboa-coreutils: replaced by Rust uutils (no Scheme .so files needed)\n ;; jerboa-awk (AWK interpreter, pure Scheme)\n (map (lambda (m) (format \"~a/~a.so\" awk-stage m))\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed (sed stream editor, Rust-backed PCRE2)\n (map (lambda (m) (format \"~a/~a.so\" sed-stage m))\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-ssl + jerboa-https removed — jerboa-aws now uses (std net request) (rustls)\n ;; jerboa-aws (optional — only included when aws-dir has the module)\n (if has-aws?\n (map (lambda (m) (format \"~a/~a.so\" aws-stage m))\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\" \"jerboa-aws/time\"\n \"jerboa-aws/crypto\" \"jerboa-aws/creds\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/request\" \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jerboa-fuse / vault (encrypted FUSE vault)\n (if has-jerboa-fuse?\n (map (lambda (m) (format \"~a/~a.so\" vault-stage m))\n '(\"chez/vault/format\" \"chez/fuse/constants\" \"chez/fuse/types\"\n \"chez/fuse/mount\" \"chez/fuse/codec\" \"chez/fuse/secmem\"\n \"chez/fuse/access\" \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\"))\n '())\n ;; jsh modules\n (map (lambda (m) (format \"src/jsh/~a.so\" m))\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\" \"config\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"\n \"coreutils\")))))\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-musl-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define musl-lib-dir (musl-chez-lib-dir))\n(define gcc (musl-gcc-path))\n(define scheme-h-dir musl-lib-dir)\n;; Hardening: strip source paths from the binary to prevent information leakage\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" (or (getenv \"HOME\") \"/root\") \"=~\"))\n\n;; Get boot file paths from musl Chez installation\n(define musl-boots (musl-boot-files))\n(define petite-boot-path (cdr (assoc \"petite\" musl-boots)))\n(define scheme-boot-path (cdr (assoc \"scheme\" musl-boots)))\n\n;; Generate static_boot.c: embeds petite.boot + scheme.boot + jsh.boot\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n ;; Embed boot files\n (display (file->c-array petite-boot-path \"petite_boot\") out)\n (newline out)\n (display (file->c-array scheme-boot-path \"scheme_boot\") out)\n (newline out)\n (display (file->c-array \"jsh.boot\" \"jsh_boot\") out)\n (newline out)\n ;; static_boot_init for Chez's main.o\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", jsh_boot, jsh_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Generate jsh_program_embed.c: embeds the optimized program .so\n;; and provides a custom main that:\n;; - Saves args to env vars (bypass Chez arg parsing)\n;; - Calls static_boot_init + Sbuild_heap\n;; - Loads program via memfd + Sscheme_script (threading workaround)\n(define program-c (format \"~a/jsh_main_musl.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#define _GNU_SOURCE\\n\" out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/ioctl.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <netdb.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n ;; Conditionally define HAS_JERBOA_NATIVE when Rust library is available\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n ;; dlopen/dlsym stubs for static builds.\n ;; Coreutils modules call (load-shared-object #f) at init time, which\n ;; calls dlopen(NULL). In musl static builds, dlopen always fails.\n ;; These stubs make dlopen(NULL) succeed (returning a dummy handle)\n ;; while actual symbol lookup goes through Sforeign_symbol.\n (display \"/* dlopen stubs — override musl's failing stubs in static builds */\\n\" out)\n (display \"void *dlopen(const char *filename, int flags) {\\n\" out)\n (display \" (void)flags;\\n\" out)\n (display \" (void)filename;\\n\" out)\n (display \" return (void*)1; /* all symbols pre-registered via Sforeign_symbol */\\n\" out)\n (display \"}\\n\" out)\n (display \"void *dlsym(void *handle, const char *symbol) {\\n\" out)\n (display \" (void)handle; (void)symbol;\\n\" out)\n (display \" return NULL; /* symbols found via Sforeign_symbol */\\n\" out)\n (display \"}\\n\" out)\n (display \"int dlclose(void *handle) { (void)handle; return 0; }\\n\" out)\n (display \"char *dlerror(void) { return NULL; }\\n\\n\" out)\n ;; Embed program .so\n (display (file->c-array program-so \"jsh_program_data\") out)\n (newline out)\n ;; Declare static_boot_init (defined in static_boot.c)\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n ;; Declare all FFI functions from ffi-shim.c\n ;; List is auto-generated by tools/extract-ffi-symbols.sh from ffi-shim.c.\n (display \"/* FFI symbols from ffi-shim.c (auto-generated via ffi-shim-symbols.list) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n ;; Non-ffi_ helpers that are used alongside the whitelist. These are\n ;; defined in ffi-shim.c but do not carry the ffi_ prefix, so the\n ;; auto-extractor does not pick them up.\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\"))\n ;; Rust native library symbols — only declared when libjerboa_native.a exists\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n ;; TLS (rustls)\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n ;; TLS mTLS (mutual TLS)\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n ;; Hardening (extern void OK — only used via Sforeign_symbol)\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n ;; X509 certificate generation\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\" \"jerboa_x509_generate_signed_by_ca_mem\" \"jerboa_x509_cert_fingerprint\"\n ;; SOCKS5 proxy server\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n ;; These three are called directly in main() — need proper return type\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n (display \"#endif\\n\" out)\n ;; Weak stubs for mtls functions missing from older musl-target native libs\n (display \"/* Weak stubs: overridden if libjerboa_native.a provides them */\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n ;; jerboa-fuse vault FFI symbols (from ffi-shim.c)\n (when has-jerboa-fuse?\n (display \"/* FFI symbols from ffi-shim.c (jerboa-fuse vault) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n ;; Rust native crypto symbols used by vault/crypto.sls (replaced OpenSSL)\n (display \"/* Rust native crypto symbols for vault/crypto.sls */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jerboa_random_bytes\" \"jerboa_pbkdf2_derive\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\")))\n ;; jerboa-crypto removed — mux-auth uses Rust native crypto\n ;; Rust uutils/coreutils FFI symbols (from libjsh_coreutils.a)\n (display \"/* FFI symbols from libjsh_coreutils.a (Rust uutils) */\\n\" out)\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n '(\"jsh_ls\" \"jsh_dir\" \"jsh_vdir\" \"jsh_stat\" \"jsh_du\" \"jsh_df\"\n \"jsh_dircolors\" \"jsh_pathchk\"\n \"jsh_cat\" \"jsh_cp\" \"jsh_mv\" \"jsh_rm\" \"jsh_ln\"\n \"jsh_mkdir\" \"jsh_rmdir\" \"jsh_mktemp\" \"jsh_touch\"\n \"jsh_link\" \"jsh_unlink\" \"jsh_readlink\" \"jsh_cu_realpath\"\n \"jsh_install\" \"jsh_shred\" \"jsh_truncate\" \"jsh_mkfifo\" \"jsh_mknod\" \"jsh_dd\"\n \"jsh_chmod\" \"jsh_chown\" \"jsh_chgrp\"\n \"jsh_head\" \"jsh_tail\" \"jsh_tac\" \"jsh_tee\" \"jsh_wc\" \"jsh_nl\"\n \"jsh_fold\" \"jsh_expand\" \"jsh_unexpand\" \"jsh_fmt\"\n \"jsh_cut\" \"jsh_paste\" \"jsh_join\" \"jsh_comm\"\n \"jsh_sort\" \"jsh_uniq\" \"jsh_tr\" \"jsh_numfmt\"\n \"jsh_grep\"\n \"jsh_id\" \"jsh_whoami\" \"jsh_hostname\" \"jsh_uname\" \"jsh_uptime\"\n \"jsh_who\" \"jsh_groups\" \"jsh_users\" \"jsh_pinky\" \"jsh_logname\"\n \"jsh_arch\" \"jsh_nproc\" \"jsh_tty\" \"jsh_hostid\" \"jsh_date\"\n \"jsh_seq\" \"jsh_expr\" \"jsh_factor\"\n \"jsh_base64\" \"jsh_base32\" \"jsh_basenc\" \"jsh_od\"\n \"jsh_cksum\" \"jsh_md5sum\" \"jsh_sha1sum\" \"jsh_sha224sum\"\n \"jsh_sha256sum\" \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_b2sum\" \"jsh_sum\"\n \"jsh_env\" \"jsh_timeout\" \"jsh_nice\" \"jsh_nohup\" \"jsh_chroot\"\n \"jsh_kill\"\n \"jsh_echo\" \"jsh_printf\" \"jsh_sleep\" \"jsh_yes\" \"jsh_printenv\"\n \"jsh_pwd\" \"jsh_sync\" \"jsh_test\" \"jsh_shuf\" \"jsh_split\" \"jsh_csplit\"\n \"jsh_tsort\" \"jsh_stty\" \"jsh_pr\" \"jsh_ptx\"\n \"jsh_basename\" \"jsh_dirname\"))\n ;; jerboa-ssh FFI symbols\n (display \"/* FFI symbols from jerboa_ssh_shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n ;; jerboa-ssl FFI symbols — TLS replaced by jerboa_tls_* (rustls)\n ;; jerboa_ssl_shim.c is no longer compiled; emit weak stubs so the linker\n ;; doesn't fail if any old foreign-procedure reference remains in a .so.\n (display \"/* jerboa-ssl stub symbols — TLS replaced by jerboa_tls_* (rustls) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"__attribute__((weak)) void ~a(void) { }\\n\" name))\n '(\"jerboa_ssl_init\" \"jerboa_ssl_cleanup\"\n \"jerboa_ssl_connect\" \"jerboa_ssl_write\" \"jerboa_ssl_read\"\n \"jerboa_ssl_read_all\" \"jerboa_ssl_free_buf\" \"jerboa_ssl_close\"\n \"jerboa_ssl_memcpy\"\n \"jerboa_tcp_listen\" \"jerboa_tcp_accept\"\n \"jerboa_tcp_connect\" \"jerboa_tcp_close\"\n \"jerboa_tcp_read\" \"jerboa_tcp_write\" \"jerboa_tcp_read_all\"\n \"jerboa_tcp_set_timeout\"\n \"jerboa_ssl_server_ctx\" \"jerboa_ssl_server_accept\" \"jerboa_ssl_server_ctx_free\"\n \"jerboa_tcp_conn_wrap\" \"jerboa_conn_write\" \"jerboa_conn_read\"))\n ;; jsqlite is pure Jerboa; no native sqlite symbols are registered.\n (newline out)\n ;; Wrapper functions for variadic/macro POSIX functions\n ;; (must appear before register_ffi_symbols which takes their address)\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n ;; Register all FFI symbols so Chez foreign-procedure can find them\n ;; (load-shared-object is disabled in static builds)\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n ;; ffi-shim.c functions — list is auto-generated from ffi-shim.c via\n ;; tools/extract-ffi-symbols.sh, so adding a new ffi_* to the C file\n ;; automatically picks it up at the next build.\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Non-ffi_ helpers that live in ffi-shim.c (Cage/Landlock wrappers and\n ;; Rust-native shims) — kept as an explicit list because the prefix\n ;; filter only picks up ffi_* names.\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\"))\n ;; Rust native library symbols — only registered when libjerboa_native.a exists\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n ;; TLS (rustls)\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n ;; TLS mTLS (mutual TLS)\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n ;; Hardening: antidebug, seccomp, integrity\n \"jerboa_antidebug_ptrace\" \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\" \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n ;; X509 certificate generation\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\" \"jerboa_x509_generate_signed_by_ca_mem\" \"jerboa_x509_cert_fingerprint\"\n ;; SOCKS5 proxy server\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n (display \"#endif\\n\" out))\n ;; jerboa-crypto removed — mux-auth uses Rust native crypto\n ;; POSIX functions used via foreign-procedure in ffi.sls\n ;; These are real C functions (not macros) from musl libc\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\" \"execve\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"pipe\" \"sigemptyset\" \"sigfillset\" \"sigaddset\" \"sigdelset\"\n \"sigismember\" \"sigprocmask\" \"sigwait\"\n \"tcgetattr\" \"tcsetattr\" \"ioctl\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"setuid\" \"setgid\"\n \"setenv\" \"unsetenv\"\n ;; POSIX functions used by awk/sed and other Scheme modules\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"stat\" \"fstat\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"setpriority\" \"getrlimit\" \"setrlimit\" \"strftime\" \"localtime\"\n \"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"getsockopt\"\n \"getaddrinfo\" \"freeaddrinfo\"\n \"htons\" \"ntohs\" \"inet_pton\" \"inet_ntop\" \"inet_addr\"\n \"listen\" \"accept\" \"connect\"\n \"strerror\"))\n ;; mkdir needs a wrapper (may be a macro on some platforms)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\"))\n ;; Variadic/macro POSIX functions need wrappers (defined above)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; Rust uutils/coreutils functions\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jsh_ls\" \"jsh_dir\" \"jsh_vdir\" \"jsh_stat\" \"jsh_du\" \"jsh_df\"\n \"jsh_dircolors\" \"jsh_pathchk\"\n \"jsh_cat\" \"jsh_cp\" \"jsh_mv\" \"jsh_rm\" \"jsh_ln\"\n \"jsh_mkdir\" \"jsh_rmdir\" \"jsh_mktemp\" \"jsh_touch\"\n \"jsh_link\" \"jsh_unlink\" \"jsh_readlink\" \"jsh_cu_realpath\"\n \"jsh_install\" \"jsh_shred\" \"jsh_truncate\" \"jsh_mkfifo\" \"jsh_mknod\" \"jsh_dd\"\n \"jsh_chmod\" \"jsh_chown\" \"jsh_chgrp\"\n \"jsh_head\" \"jsh_tail\" \"jsh_tac\" \"jsh_tee\" \"jsh_wc\" \"jsh_nl\"\n \"jsh_fold\" \"jsh_expand\" \"jsh_unexpand\" \"jsh_fmt\"\n \"jsh_cut\" \"jsh_paste\" \"jsh_join\" \"jsh_comm\"\n \"jsh_sort\" \"jsh_uniq\" \"jsh_tr\" \"jsh_numfmt\"\n \"jsh_grep\"\n \"jsh_id\" \"jsh_whoami\" \"jsh_hostname\" \"jsh_uname\" \"jsh_uptime\"\n \"jsh_who\" \"jsh_groups\" \"jsh_users\" \"jsh_pinky\" \"jsh_logname\"\n \"jsh_arch\" \"jsh_nproc\" \"jsh_tty\" \"jsh_hostid\" \"jsh_date\"\n \"jsh_seq\" \"jsh_expr\" \"jsh_factor\"\n \"jsh_base64\" \"jsh_base32\" \"jsh_basenc\" \"jsh_od\"\n \"jsh_cksum\" \"jsh_md5sum\" \"jsh_sha1sum\" \"jsh_sha224sum\"\n \"jsh_sha256sum\" \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_b2sum\" \"jsh_sum\"\n \"jsh_env\" \"jsh_timeout\" \"jsh_nice\" \"jsh_nohup\" \"jsh_chroot\"\n \"jsh_kill\"\n \"jsh_echo\" \"jsh_printf\" \"jsh_sleep\" \"jsh_yes\" \"jsh_printenv\"\n \"jsh_pwd\" \"jsh_sync\" \"jsh_test\" \"jsh_shuf\" \"jsh_split\" \"jsh_csplit\"\n \"jsh_tsort\" \"jsh_stty\" \"jsh_pr\" \"jsh_ptx\"\n \"jsh_basename\" \"jsh_dirname\"))\n ;; jerboa-ssh functions\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n ;; jerboa-ssl functions\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_ssl_init\" \"jerboa_ssl_cleanup\"\n \"jerboa_ssl_connect\" \"jerboa_ssl_write\" \"jerboa_ssl_read\"\n \"jerboa_ssl_read_all\" \"jerboa_ssl_free_buf\" \"jerboa_ssl_close\"\n \"jerboa_ssl_memcpy\"\n \"jerboa_tcp_listen\" \"jerboa_tcp_accept\"\n \"jerboa_tcp_connect\" \"jerboa_tcp_close\"\n \"jerboa_tcp_read\" \"jerboa_tcp_write\" \"jerboa_tcp_read_all\"\n \"jerboa_tcp_set_timeout\"\n \"jerboa_ssl_server_ctx\" \"jerboa_ssl_server_accept\" \"jerboa_ssl_server_ctx_free\"\n \"jerboa_tcp_conn_wrap\" \"jerboa_conn_write\" \"jerboa_conn_read\"))\n ;; POSIX socket functions used by (std net tcp-raw) for actor transport\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\"\n \"htons\" \"inet_pton\" \"__errno_location\"))\n ;; listen/accept/connect need special handling — listen conflicts with C\n ;; We register them with distinct names and wrap in tcp-raw\n ;; Actually these are plain C functions, no conflict:\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"listen\" \"accept\" \"connect\"))\n ;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n (when has-jerboa-fuse?\n (display \" /* jerboa-fuse vault FFI symbols */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n ;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n ;; jerboa_aead_seal, jerboa_aead_open — all exported by libjerboa_native.a (ring)\n ;; POSIX symbols NOT already registered by Chez (open/read/write/close ARE)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\")))\n ;; jsqlite is pure Jerboa; no sqlite shim is linked.\n (display \"}\\n\\n\" out)\n ;; Custom main\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" /* Initialize Rust stdlib args for musl (glibc .init_array won't fire) */\\n\" out)\n (display \" jsh_coreutils_init(argc, argv);\\n\\n\" out)\n (display \" /* Ensure fds 0/1/2 point to /dev/null when closed (e.g. fork-exec from ,server).\\n\" out)\n (display \" * Without this, memfd_create reuses fd 0, Chez boot opens ports on wrong fds,\\n\" out)\n (display \" * and the server process crashes silently. */\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n (display \" /* Save args in env vars (bypass Chez arg parsing) */\\n\" out)\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n (display \" /* Resolve real exe path for $SHELL */\\n\" out)\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" ssize_t len = readlink(\\\"/proc/self/exe\\\", exe_buf, sizeof(exe_buf) - 1);\\n\" out)\n (display \" if (len > 0) { exe_buf[len] = '\\\\0'; setenv(\\\"JSH_EXE\\\", exe_buf, 1); }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening — runs before Chez init for strongest protection.\n ;; Guarded by HAS_JERBOA_NATIVE and !JSH_DEV env var.\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" /* Phase 0: C-level hardening — before Chez init */\\n\" out)\n (display \" /* NOTE: ptrace(TRACEME) is NOT used here — it makes the parent\\n\" out)\n (display \" the tracer, causing SIGSTOP on signal delivery. Fatal for a\\n\" out)\n (display \" shell that relies on SIGCHLD/SIGWINCH. Instead we detect\\n\" out)\n (display \" existing tracers and use seccomp to block future attachment. */\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n (display \" /* Initialize Chez + register embedded boot files */\\n\" out)\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\\n\" out)\n (display \" /* Register FFI symbols after heap is built */\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n (display \" /* Load program via memfd (threading workaround) */\\n\" out)\n (display \" int fd = memfd_create(\\\"jsh-program\\\", 1 /* MFD_CLOEXEC */);\\n\" out)\n (display \" if (fd < 0) { perror(\\\"memfd_create\\\"); return 1; }\\n\" out)\n (display \" if (write(fd, jsh_program_data, jsh_program_data_len) != (ssize_t)jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"write\\\"); close(fd); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" char prog_path[64];\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"/proc/self/fd/%d\\\", fd);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" close(fd);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C with musl-gcc ==========\n\n(printf \"[6/7] Compiling C with musl-gcc...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-musl \"Command failed\" cmd)))\n\n;; Compile static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir\n build-dir static-boot-c))\n\n;; Compile jsh_main_musl.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_musl.o' '~a'\"\n gcc harden-cflags scheme-h-dir\n build-dir program-c))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; Compile ffi-shim.c with musl-gcc\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; Compile landlock-shim.c from jerboa\n(run-cmd (format \"~a -c -O2 ~a -o '~a/landlock-shim.o' '~a/support/landlock-shim.c' -Wall\"\n gcc harden-cflags build-dir jerboa-dir-base))\n\n;; Coreutils FFI shim no longer needed — replaced by Rust uutils (libjsh_coreutils.a)\n;; Generate empty .o to satisfy link step (will be removed once link step is updated)\n(system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))\n\n;; Compile jerboa-ssh shim (mirrors build-jsh-macos.ss / build-jsh-freebsd.ss)\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Compile jerboa-ssh shim WITHOUT OpenSSL — ed25519/etc. come from Rust\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf — compile if upstream jerboa-ssh ships it, else empty stub\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c — no longer compiled (OpenSSL removed for musl);\n ;; SSH transport crypto goes through Rust ring via libjerboa_native.\n ;; Generate empty .o placeholder for the linker.\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir)))\n (begin\n (printf \" Warning: jerboa-ssh shim not found, building without SSH agent~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-ssl shim — no longer compiled; TLS now via jerboa_tls_* (rustls)\n(printf \" [skip] jerboa-ssl shim — replaced by jerboa_tls_* (rustls)~n\")\n\n;; jerboa-crypto shim: no longer compiled — vault crypto now uses jerboa_native (ring)\n;; glibc-compat shim: no longer needed — libcrypto.a removed\n(printf \" [skip] jerboa-crypto shim and glibc-compat — vault crypto now via jerboa_native (ring)~n\")\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking static jsh-musl binary...~n\")\n\n(let ([link-cmd (apply musl-link-command\n \"jsh-musl\"\n (list (format \"~a/jsh_main_musl.o\" build-dir)\n (format \"~a/static_boot.o\" build-dir)\n (format \"~a/ffi-shim.o\" build-dir)\n (format \"~a/embed-crypto.o\" build-dir)\n (format \"~a/coreutils-ffi.o\" build-dir)\n (format \"~a/jerboa-ssh-shim.o\" build-dir)\n (format \"~a/jerboa-ssh-crypto.o\" build-dir)\n (format \"~a/ed25519-standalone.o\" build-dir)\n (format \"~a/bcrypt_pbkdf.o\" build-dir)\n (format \"~a/landlock-shim.o\" build-dir)\n ;; jerboa-crypto-shim.o removed — vault crypto now via jerboa_native (ring)\n ;; jsqlite is pure Jerboa; no sqlite shim object needed\n ;; jerboa-ssl-shim.o removed — TLS now via jerboa_tls_* (rustls)\n ;; glibc-compat.o removed — was only needed for libcrypto.a\n )\n ;; Rust native library (crypto, regex, landlock, etc.)\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n (append\n ;; Rust native library — provides crypto, regex, landlock, etc.\n (if has-native-lib?\n (list native-lib-path)\n '())\n ;; Rust uutils/coreutils library\n (list rust-coreutils-lib-path))\n ;; Use plain -static (not -static-pie):\n ;; GCC 13+ libgcc_eh.a references _dl_find_object (glibc 2.35+)\n ;; which doesn't exist in musl, breaking -static-pie builds\n '(no-harden: #t))])\n ;; C landlock-shim and Rust lib both define jerboa_landlock_abi_version\n ;; (Rust LTO packs all symbols into one .o, causing duplicate when linked)\n ;; Android: use custom CRT that skips musl self-relocation (linker64 does it).\n ;; -nostartfiles: don't link rcrt1.o (self-relocating CRT)\n ;; android-crt.o: custom _start → __libc_start_main (no self-relocation)\n ;; crti.o/crtn.o: .init/.fini section support\n ;; -z noseparate-code: first PT_LOAD at offset 0 for linker64 PHDR lookup\n (run-cmd (string-append link-cmd\n \" -Wl,--allow-multiple-definition\"\n (if (getenv \"JSH_ANDROID\")\n (string-append\n \" -nostartfiles\"\n \" /tmp/android-crt.o\"\n \" /usr/lib/aarch64-linux-musl/crti.o\"\n \" /usr/lib/aarch64-linux-musl/crtn.o\"\n \" -Wl,-z,noseparate-code\")\n \"\"))))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? \"jsh-musl\")\n (if (getenv \"JSH_NO_STRIP\")\n (printf \"~n[harden] Skipping strip (JSH_NO_STRIP set)~n\")\n (begin\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-musl\"))])\n ;; Strip all symbol tables and debug info\n (run-cmd \"strip --strip-all jsh-musl\")\n ;; Remove section headers (prevents section-based disassembly)\n ;; objcopy --strip-section-headers requires binutils >= 2.40\n ;; Skip on Android: linker requires section headers for static-pie binaries\n (if (getenv \"JSH_KEEP_SECTION_HEADERS\")\n (printf \" Section headers preserved (JSH_KEEP_SECTION_HEADERS set)~n\")\n (when (= 0 (system \"objcopy --strip-section-headers jsh-musl 2>/dev/null\"))\n (printf \" Section headers removed~n\")))\n (let ([post-size (file-length (open-file-input-port \"jsh-musl\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))))\n\n ;; Compute SHA-256 integrity hash for runtime verification.\n ;; The binary checks for jsh-musl.sha256 at startup (see harden.sls).\n ;; Write raw 32-byte hash using pure Scheme (no xxd dependency).\n (printf \"[harden] Computing integrity hash...~n\")\n (system \"sha256sum jsh-musl | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\")\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-musl.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-musl.sha256 (32 bytes)~n\")))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n;; coreutils-stage no longer needed — Rust uutils replaces Scheme coreutils\n;; ssl-stage removed — jerboa-ssl/jerboa-https no longer used (rustls replaces them)\n(system (format \"rm -rf '~a'\" aws-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Static binary created: jsh-musl~n~n\")\n(system \"ls -lh jsh-musl\")\n(printf \"~n\")\n(system \"file jsh-musl\")\n(printf \"~nTest: ./jsh-musl -c 'echo Hello from static jsh'~n\")\n"} {"text":";; FILE: jerboa-shell/tools/jsh-learn-policy/README.md\n# jsh learn-policy\n\nSynthesize a `jsh` wrapper from one observed run of a command. Linux only\nfor now (uses `strace` + an `LD_PRELOAD` `getaddrinfo` hook).\n\n## Usage\n\n```\ntools/jsh-learn-policy/learn-policy -- codex --version > codex-wrap\nchmod +x codex-wrap\n```\n\nThe emitted wrapper goes to stdout. Status messages go to stderr. The\nhook library `dns-hook.so` is built on first run; rerun `make -C\ntools/jsh-learn-policy clean` to rebuild it.\n\n## What it captures\n\n- **Reads / writes** — every successful `openat()` is bucketed by flag\n (`O_RDONLY` → read; `O_WRONLY` / `O_RDWR` / `O_CREAT` / `O_TRUNC` /\n `O_APPEND` → write). Paths under `$HOME` become `~/...`, paths under\n `$PWD` become `@project`. Paths are collapsed: system paths\n (`/usr`, `/etc`, …) to their top directory; `~/.config/*`,\n `~/.cache/*`, `~/.local/{share,state}/*` to depth 2; everything else\n to one component under `~`.\n- **Execs** — each `execve()` becomes `,exec <dir>` plus `,exec\n <basename>`.\n- **Network** — `getaddrinfo` / `gethostbyname` lookups become\n `,net <host>:443` (HTTPS assumed). Connects to IPs that the hook\n did not see are emitted as `# observed IP-literal connect:` comments\n for manual review.\n- **Standard tail** — `,home real`, `,cache ephemeral`, `,limit pids\n 256`, `,limit fsize 2g`, `,run-policy -- <name> \"$@\"`.\n\n## What it does *not* capture\n\n- Programs that bypass libc (raw syscall, Go) — no `getaddrinfo` hook\n fires; you only get IPs.\n- Environment variables and secrets — `,env` / `,secret` lines must be\n added by hand.\n- Ports other than 443 — review the network rules.\n\n## Promote later\n\nThe cross-platform plan is a `,learn-policy` jsh subcommand that\nditches strace in favour of an audit-mode pass through jsh's own\npolicy engine. This Python wrapper is the v1 placeholder.\n"} {"text":";; FILE: jerboa-shell/test/test-jsh.ss\n#!chezscheme\n;;; test-jsh.ss — Unit tests for jsh (Jerboa Shell) modules\n;;;\n;;; Run: scheme --libdirs src:<jerboa>/lib --script test/test-jsh.ss\n\n(import\n (except (chezscheme) box box? unbox set-box! andmap ormap iota\n last-pair find 1+ 1- fx/ fx1+ fx1- error? raise\n with-exception-handler identifier? hash-table? make-hash-table\n sort sort! path-extension printf fprintf void for-all)\n (except (jerboa runtime) bind-method! call-method ~ void cons* make-list)\n (std format) (std sort) (std transducer)\n (only (std log) make-logger logger? logger-level logger-fields log-level?\n log-info log-debug current-logger))\n\n(define pass-count 0)\n(define fail-count 0)\n\n(define-syntax check\n (syntax-rules (=>)\n [(_ expr => expected)\n (let ([got expr] [exp expected])\n (if (equal? got exp)\n (set! pass-count (+ 1 pass-count))\n (begin\n (set! fail-count (+ 1 fail-count))\n (printf \"FAIL: ~s~n expected: ~s~n got: ~s~n\"\n 'expr exp got))))]))\n\n(define-syntax check-true\n (syntax-rules ()\n [(_ expr)\n (if expr\n (set! pass-count (+ 1 pass-count))\n (begin (set! fail-count (+ 1 fail-count))\n (printf \"FAIL: expected true: ~s~n\" 'expr)))]))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 1. Transducer integration (used by history search pipeline)\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Transducer tests ---~n\")\n\n;; Basic: filter + consecutive-dedup + take via sequence\n(check\n (sequence\n (compose-transducers (filtering even?) (deduplicate) (taking 3))\n '(1 2 2 4 4 6 8))\n => '(2 4 6))\n\n;; Prefix-filter pipeline (mirrors history-search)\n;; Note: (deduplicate) removes consecutive dups — for full dedup we track seen set\n(define (prefix-search prefix entries)\n (let ([plen (string-length prefix)]\n [seen (make-hashtable string-hash string=?)])\n (sequence\n (compose-transducers\n (filtering (lambda (cmd)\n (and (>= (string-length cmd) plen)\n (string=? (substring cmd 0 plen) prefix))))\n (filtering (lambda (cmd)\n (if (hashtable-ref seen cmd #f)\n #f\n (begin (hashtable-set! seen cmd #t) #t))))\n (taking 100))\n entries)))\n\n(check (prefix-search \"git\" '(\"git status\" \"git commit\" \"git status\" \"ls\" \"git push\"))\n => '(\"git status\" \"git commit\" \"git push\"))\n(check (prefix-search \"ls\" '(\"ls -la\" \"ls\" \"cat\" \"ls\")) => '(\"ls -la\" \"ls\"))\n(check (prefix-search \"no\" '(\"ls\" \"cat\" \"echo\")) => '())\n(check (prefix-search \"\" '(\"a\" \"b\" \"a\" \"c\")) => '(\"a\" \"b\" \"c\"))\n\n;; Windowing transducer\n(check\n (sequence (windowing 3) '(1 2 3 4 5))\n => '((1 2 3) (2 3 4) (3 4 5)))\n\n;; Flat-mapping\n(check\n (sequence (flat-mapping (lambda (x) (list x (* x x)))) '(1 2 3))\n => '(1 1 2 4 3 9))\n\n;; Mapping\n(check\n (sequence (mapping (lambda (x) (* x 2))) '(1 2 3 4 5))\n => '(2 4 6 8 10))\n\n;; dropping\n(check (sequence (dropping 2) '(1 2 3 4 5)) => '(3 4 5))\n\n;; into with list destination\n(check (into '() (filtering odd?) '(1 2 3 4 5)) => '(1 3 5))\n\n;; Sum via transduce + rf-sum factory\n(let ([sum-rf (rf-sum)])\n (check\n (transduce (mapping (lambda (x) (* x x))) sum-rf (sum-rf) '(1 2 3 4))\n => 30))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 2. Structured logging (Jerboa Phase 4 enhancement)\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Structured logging tests ---~n\")\n\n;; make-logger takes a level ('debug, 'info, 'warn, 'error, 'fatal)\n;; make-logger takes a level and optional key-value fields\n(let ([logger (make-logger 'debug)])\n (check-true (logger? logger))\n (check (logger-level logger) => 'debug))\n\n(let ([logger (make-logger 'info 'component 'jsh)])\n (check-true (logger? logger))\n (check (logger-level logger) => 'info))\n\n;; log-level? validates known levels\n(check-true (log-level? 'debug))\n(check-true (log-level? 'info))\n(check-true (log-level? 'warn))\n(check-true (log-level? 'error))\n(check-true (log-level? 'fatal))\n(check-true (not (log-level? 'trace)))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 3. History module tests (uses transducer-based search)\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- History module tests ---~n\")\n\n(import (only (jsh history)\n *history* history-init! history-add-raw! history-count\n history-list history-search history-unique-commands))\n\n(history-init! \"/dev/null\" 500)\n(check-true *history*)\n\n;; history-add-raw! takes (timestamp cwd command)\n(history-add-raw! 0 \"\" \"echo hello\")\n(history-add-raw! 0 \"\" \"ls -la\")\n(history-add-raw! 0 \"\" \"git status\")\n(history-add-raw! 0 \"\" \"echo world\")\n(history-add-raw! 0 \"\" \"git commit -m 'test'\")\n\n(check (history-count) => 5)\n(check-true (= (length (history-list)) 5))\n\n;; Transducer-based prefix search\n(check (history-search \"echo\") => '(\"echo hello\" \"echo world\"))\n(check (history-search \"git\") => '(\"git status\" \"git commit -m 'test'\"))\n(check (history-search \"xyz\") => '())\n\n;; Add duplicate entry; history-search returns all matches (no dedup)\n(history-add-raw! 0 \"\" \"echo hello\")\n(let ([results (history-search \"echo\")])\n (check-true (member \"echo hello\" results))\n (check-true (member \"echo world\" results))\n (check-true (>= (length results) 3)))\n\n;; unique commands\n(check-true (list? (history-unique-commands)))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 4. AST module tests\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- AST module tests ---~n\")\n\n(import (only (jsh ast)\n make-simple-command simple-command?\n simple-command-words simple-command-assignments simple-command-redirections\n make-token token? token-type token-value))\n\n;; Token construction (make-token takes type, value, pos)\n(let ([tok (make-token 'word \"echo\" 0)])\n (check-true (token? tok))\n (check (token-type tok) => 'word)\n (check (token-value tok) => \"echo\"))\n\n;; Simple command construction: (make-simple-command assignments words redirections)\n(let ([cmd (make-simple-command '() (list (make-token 'word \"echo\" 0) (make-token 'word \"hello\" 5)) '())])\n (check-true (simple-command? cmd))\n (check-true (list? (simple-command-words cmd)))\n (check (length (simple-command-words cmd)) => 2))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 5. Lexer tests\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Lexer tests ---~n\")\n\n(import (jsh lexer))\n\n(check-true (list? (tokenize \"echo hello world\")))\n(check-true (>= (length (tokenize \"echo hello world\")) 3))\n(check-true (null? (tokenize \"\")))\n(check-true (list? (tokenize \"'hello world'\")))\n(check-true (list? (tokenize \"a|b\")))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 6. Environment tests\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Environment tests ---~n\")\n\n(import (only (jsh environment)\n make-shell-environment shell-environment?\n env-set! env-get\n shell-environment-last-status env-set-last-status!\n canonicalize-path-with-shims jsh-user-shim-dir))\n\n(let ([env (make-shell-environment)])\n (check-true (shell-environment? env))\n (env-set! env \"TESTVAR\" \"hello\")\n (check (env-get env \"TESTVAR\") => \"hello\")\n (check (env-get env \"NOTSET\") => #f)\n (check (shell-environment-last-status env) => 0)\n (env-set-last-status! env 42)\n (check (shell-environment-last-status env) => 42))\n\n;; ~/.jsh/shims is always first in PATH. The HOME the test inherits is\n;; whatever launched the test runner; we just confirm the canonicalizer\n;; prepends the resolved shim dir when missing, leaves it alone when\n;; already first, and folds empty PATH down to just the shim dir.\n(let ([dir (jsh-user-shim-dir)])\n (when dir\n (check (canonicalize-path-with-shims \"/usr/bin:/bin\")\n => (string-append dir \":/usr/bin:/bin\"))\n (check (canonicalize-path-with-shims (string-append dir \":/usr/bin\"))\n => (string-append dir \":/usr/bin\"))\n (check (canonicalize-path-with-shims dir) => dir)\n (check (canonicalize-path-with-shims \"\") => dir))\n ;; A user PATH assignment goes through env-set!, which routes through\n ;; canonicalize-path-with-shims — verify the resulting env-get sees the\n ;; shim dir back in front even after the user overwrote PATH.\n (when dir\n (let ([env (make-shell-environment)])\n (env-set! env \"PATH\" \"/usr/bin:/bin\")\n (check (env-get env \"PATH\")\n => (string-append dir \":/usr/bin:/bin\")))))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 7. Arithmetic tests\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Arithmetic tests ---~n\")\n\n(import (only (jsh arithmetic) arith-eval))\n(import (only (jsh environment) arith-env-getter arith-env-setter))\n\n(let* ([env (make-shell-environment)]\n [get-fn (arith-env-getter env)]\n [set-fn (arith-env-setter env)])\n (check (arith-eval \"1+1\" get-fn set-fn) => 2)\n (check (arith-eval \"10-3\" get-fn set-fn) => 7)\n (check (arith-eval \"3*4\" get-fn set-fn) => 12)\n (check (arith-eval \"10/2\" get-fn set-fn) => 5)\n (check (arith-eval \"2**8\" get-fn set-fn) => 256)\n (check (arith-eval \"7%3\" get-fn set-fn) => 1)\n (check (arith-eval \"5&3\" get-fn set-fn) => 1)\n (check (arith-eval \"5|2\" get-fn set-fn) => 7)\n (check (arith-eval \"5^3\" get-fn set-fn) => 6)\n (check (arith-eval \"3>2\" get-fn set-fn) => 1)\n (check (arith-eval \"2>3\" get-fn set-fn) => 0)\n (check (arith-eval \"3==3\" get-fn set-fn) => 1))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 8. Glob tests\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Glob tests ---~n\")\n\n(import (only (jsh glob) glob-expand glob-pattern? glob-match?))\n\n(check-true (list? (glob-expand \"/tmp\")))\n(check-true (list? (glob-expand \"/tmp/*\")))\n(check-true (list? (glob-expand \"/tmp/this-does-not-exist-xyz-*\")))\n(check-true (glob-match? \"a\\\\*b\" \"a*b\" #f #f))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 8b. Parameter expansion literal fast paths\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Parameter expansion string fast-path tests ---~n\")\n\n(import (only (jsh expander)\n remove-prefix remove-suffix pattern-substitute-first pattern-substitute-all))\n\n(check (remove-prefix \"abcabc\" \"abc\" #f #f) => \"abc\")\n(check (remove-prefix \"a*bc\" \"a\\\\*\" #f #f) => \"bc\")\n(check (remove-prefix \"abc:def:ghi\" \"*:\" #f #f) => \"def:ghi\")\n(check (remove-prefix \"abc:def:ghi\" \"*:\" #t #f) => \"ghi\")\n(check (remove-suffix \"abcabc\" \"abc\" #f #f) => \"abc\")\n(check (remove-suffix \"ab*c\" \"\\\\*c\" #f #f) => \"ab\")\n(check (remove-suffix \"abc:def:ghi\" \":*\" #f #f) => \"abc:def\")\n(check (remove-suffix \"abc:def:ghi\" \":*\" #t #f) => \"abc\")\n(check (pattern-substitute-first \"abcabc\" \"ab\" \"XY\" #f) => \"XYcabc\")\n(check (pattern-substitute-all \"abcabc\" \"ab\" \"XY\" #f) => \"XYcXYc\")\n(check (pattern-substitute-first \"abcabc\" \"#ab\" \"XY\" #f) => \"XYcabc\")\n(check (pattern-substitute-first \"abcabc\" \"%bc\" \"XY\" #f) => \"abcaXY\")\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 9. Fuzzy match tests\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Fuzzy tests ---~n\")\n\n(import (only (jsh fuzzy) fuzzy-match-score fuzzy-match?))\n\n(check-true (number? (fuzzy-match-score \"git\" \"git\")))\n(check-true (> (fuzzy-match-score \"git\" \"git\") 0))\n(check-true (> (fuzzy-match-score \"gi\" \"git\") 0))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 10. Registry tests\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Registry tests ---~n\")\n\n(import (only (jsh registry)\n *jsh-tier* builtin-lookup builtin?\n meta-register! meta-complete-register!))\n(import (only (jsh builtins) special-builtin?))\n(import (only (jsh completion) complete-word))\n(import (only (jsh worm) worm-handler worm-feature-enable!))\n\n(check-true (string? (*jsh-tier*)))\n(check-true (special-builtin? \":\"))\n(check-true (builtin? \"print\"))\n\n(let ([status #f])\n (let ([out (call-with-string-output-port\n (lambda (p)\n (parameterize ([current-output-port p])\n (set! status ((builtin-lookup \"print\") '(\"test\") #f)))))])\n (check status => 0)\n (check out => \"test\\n\")))\n\n(meta-register! \"zzz-test\" (lambda (args) (cons \"\" 0)) \"test meta command\")\n(meta-complete-register! \"zzz-test\"\n (lambda (current-word words word-index line cursor env)\n (if (string=? current-word \"al\") '(\"alpha\" \"alpine\") '())))\n(let ([env (make-shell-environment)])\n (check-true (member \",zzz-test\" (complete-word \",zzz\" 4 env)))\n (check (complete-word \",zzz-test al\" 13 env) => '(\"alpha\" \"alpine\")))\n\n(define (registry-test-contains? hay needle)\n (let ([hlen (string-length hay)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring hay i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))\n\n(let ([disabled (worm-handler \"--help\")])\n (check (cdr disabled) => 1)\n (check-true (registry-test-contains? (car disabled) \"not available\")))\n\n(worm-feature-enable!)\n(let ([status #f])\n (let ([out (call-with-string-output-port\n (lambda (p)\n (parameterize ([current-output-port p])\n (set! status (worm-handler \"--help\")))))])\n (check (cdr status) => 0)\n (check-true (registry-test-contains? out \"wormhole send\"))\n (check-true (registry-test-contains? out \"wormhole receive CODE\"))))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 10b. Launch-policy (jsh limits) — parser and launcher helpers\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy tests ---~n\")\n\n(define (search-substring? hay needle)\n (let ([hl (string-length hay)] [nl (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nl) hl) #f]\n [(string=? (substring hay i (+ i nl)) needle) #t]\n [else (loop (+ i 1))]))))\n\n(import\n (only (jsh limits)\n launch-policy-reset! launch-policy->alist\n launch-policy-add-profile! launch-policy-add-read!\n launch-policy-add-write! launch-policy-add-exec!\n launch-policy-add-net! launch-policy-add-deny!\n launch-policy-add-env! launch-policy-add-secret!\n launch-policy-set-home! launch-policy-set-cache!\n launch-policy-add-limit! launch-policy-add-flag-bin!\n launch-policy-set-tracefs! launch-policy-set-audit-out!\n launch-policy-set-workspace!\n launch-policy-add! launch-policy-explain\n current-launch-policy\n audit-jsonl-line audit-emit!\n audit-emit-policy-parsed! audit-emit-paths-resolved!\n audit-emit-command-launched! audit-emit-exit-status!\n parse-host-port parse-limit-value parse-home-cache-mode\n parse-tracefs-mode parse-workspace-mode))\n\n;; Fresh policy is fully empty.\n(launch-policy-reset!)\n(check (cdr (assq 'profiles (launch-policy->alist))) => '())\n(check (cdr (assq 'read (launch-policy->alist))) => '())\n(check (cdr (assq 'home (launch-policy->alist))) => #f)\n(check (cdr (assq 'limit (launch-policy->alist))) => '())\n(check (cdr (assq 'tracefs (launch-policy->alist))) => #f)\n(check (cdr (assq 'workspace (launch-policy->alist))) => #f)\n\n;; Adders preserve insertion order via final reverse in ->alist.\n(launch-policy-reset!)\n(launch-policy-add-profile! \"codex\")\n(launch-policy-add-read! \"@project\")\n(launch-policy-add-read! \"@cache:npm\")\n(launch-policy-add-write! \"@scratch\")\n(launch-policy-add-exec! \"/usr/bin/node\")\n(launch-policy-add-net! \"api.openai.com:443\")\n(launch-policy-add-deny! \"home\")\n(launch-policy-add-deny! \"localnet\")\n(launch-policy-add-env! \"PATH\")\n(launch-policy-add-secret! \"OPENAI_API_KEY\")\n(launch-policy-set-home! \"ephemeral\")\n(launch-policy-set-cache! \"/tmp/jsh-cache\")\n(launch-policy-add-limit! \"mem\" \"2g\")\n(launch-policy-add-limit! \"pids\" \"64\")\n(launch-policy-add-flag-bin! \"node\" \"untrusted\")\n(launch-policy-set-tracefs! \"summary\")\n(launch-policy-set-audit-out! \"/tmp/audit.jsonl\")\n(launch-policy-set-workspace! \"transaction\")\n\n(let ([a (launch-policy->alist)])\n (check (cdr (assq 'profiles a)) => '(\"codex\"))\n (check (cdr (assq 'read a)) => '(\"@project\" \"@cache:npm\"))\n (check (cdr (assq 'write a)) => '(\"@scratch\"))\n (check (cdr (assq 'exec a)) => '(\"/usr/bin/node\"))\n (check (cdr (assq 'net a)) => '(\"api.openai.com:443\"))\n (check (cdr (assq 'deny a)) => '(\"home\" \"localnet\"))\n (check (cdr (assq 'env a)) => '(\"PATH\"))\n (check (cdr (assq 'secret a)) => '(\"OPENAI_API_KEY\"))\n (check (cdr (assq 'home a)) => \"ephemeral\")\n (check (cdr (assq 'cache a)) => \"/tmp/jsh-cache\")\n (check (cdr (assq 'limit a))\n => '((\"mem\" . \"2g\") (\"pids\" . \"64\")))\n (check (cdr (assq 'flag-bin a)) => '((\"node\" . \"untrusted\")))\n (check (cdr (assq 'tracefs a)) => \"summary\")\n (check (cdr (assq 'audit-out a)) => \"/tmp/audit.jsonl\")\n (check (cdr (assq 'workspace a)) => \"transaction\"))\n\n;; Generic dispatcher mirrors typed adders.\n(launch-policy-reset!)\n(check-true (launch-policy-add! 'profile \"x\"))\n(check-true (launch-policy-add! 'read \"/etc\"))\n(check-true (launch-policy-add! 'limit \"cpu\" \"2\"))\n(check (launch-policy-add! 'unknown \"z\") => #f)\n(let ([a (launch-policy->alist)])\n (check (cdr (assq 'profiles a)) => '(\"x\"))\n (check (cdr (assq 'read a)) => '(\"/etc\"))\n (check (cdr (assq 'limit a)) => '((\"cpu\" . \"2\"))))\n\n;; Reset really resets.\n(launch-policy-reset!)\n(check (cdr (assq 'profiles (launch-policy->alist))) => '())\n(check (cdr (assq 'home (launch-policy->alist))) => #f)\n\n;; Argument parsers\n(check (parse-host-port \"api.openai.com:443\") => '(\"api.openai.com\" . \"443\"))\n(check (parse-host-port \"[::1]:80\") => '(\"::1\" . \"80\"))\n(check-true (not (parse-host-port \"noport\")))\n(check-true (not (parse-host-port \"host:\")))\n(check-true (not (parse-host-port \":443\")))\n(check-true (not (parse-host-port \"host:abc\")))\n\n(check (parse-limit-value \"mem\" \"2g\") => \"2g\")\n(check (parse-limit-value \"time\" \"30m\") => \"30m\")\n(check-true (not (parse-limit-value \"bogus\" \"1\")))\n(check-true (not (parse-limit-value \"mem\" \"\")))\n\n(check (parse-home-cache-mode \"ephemeral\") => \"ephemeral\")\n(check (parse-home-cache-mode \"/tmp/x\") => \"/tmp/x\")\n(check (parse-home-cache-mode \"@scratch\") => \"@scratch\")\n(check (parse-home-cache-mode \"real\") => \"real\")\n(check (parse-home-cache-mode \"inherit\") => \"real\")\n(check-true (not (parse-home-cache-mode \"\")))\n(check-true (not (parse-home-cache-mode \"relative/path\")))\n\n(check (parse-tracefs-mode \"summary\") => \"summary\")\n(check (parse-tracefs-mode \"full\") => \"full\")\n(check-true (not (parse-tracefs-mode \"bogus\")))\n\n(check (parse-workspace-mode \"transaction\") => \"transaction\")\n(check-true (not (parse-workspace-mode \"weird\")))\n\n;; launch-policy-explain writes the labeled lines plus per-axis enforcement\n;; status. We don't fully snapshot the output (format may evolve), but the key\n;; markers should be present.\n(launch-policy-reset!)\n(launch-policy-add-profile! \"codex\")\n(launch-policy-add-read! \"@project\")\n(launch-policy-set-home! \"ephemeral\")\n(launch-policy-add-limit! \"mem\" \"2g\")\n(let ([out (call-with-string-output-port\n (lambda (p) (launch-policy-explain p)))])\n (check-true (string? out))\n (check-true (positive? (string-length out)))\n (check-true (search-substring? out \"jsh launch policy:\"))\n (check-true (search-substring? out \"profiles: codex\"))\n (check-true (search-substring? out \"read: @project\"))\n (check-true (search-substring? out \"home: ephemeral\"))\n (check-true (search-substring? out \"limits: mem=2g\"))\n (check-true (search-substring? out \"enforcement:\"))\n (check-true (search-substring? out \"jsh enforcement status:\")))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 10c. Launch-policy Phase 2 — env builder + temp dirs\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy Phase 2 tests ---~n\")\n\n(import\n (only (jsh limits)\n current-launch-policy\n policy-build-child-env policy-redact-env-for-audit\n policy-resolve-home policy-resolve-cache\n policy-make-temp-dir policy-cleanup-temp-dirs!))\n\n;; Helper: does env-list contain \"KEY=VAL\"?\n(define (env-has? lst key val)\n (let ([want (string-append key \"=\" val)])\n (let loop ([xs lst])\n (cond [(null? xs) #f]\n [(string=? (car xs) want) #t]\n [else (loop (cdr xs))]))))\n\n;; Helper: env-list lookup → value or #f\n(define (env-get lst key)\n (let ([prefix (string-append key \"=\")]\n [n (+ (string-length key) 1)])\n (let loop ([xs lst])\n (cond\n [(null? xs) #f]\n [(and (>= (string-length (car xs)) n)\n (string=? (substring (car xs) 0 n) prefix))\n (substring (car xs) n (string-length (car xs)))]\n [else (loop (cdr xs))]))))\n\n;; Default-forwarded names are passed through from parent env.\n(launch-policy-reset!)\n(let* ([parent '((\"PATH\" . \"/usr/bin:/bin\")\n (\"HOME\" . \"/home/user\")\n (\"USER\" . \"user\")\n (\"FOO_SECRET\" . \"shhh\")\n (\"RANDOM_OTHER\" . \"xyz\"))]\n [env (policy-build-child-env parent\n (current-launch-policy) #f #f)])\n ;; PATH/USER forwarded by default\n (check-true (env-has? env \"PATH\" \"/usr/bin:/bin\"))\n (check-true (env-has? env \"USER\" \"user\"))\n ;; Non-allowlisted vars dropped (allowlist policy)\n (check-true (not (env-get env \"RANDOM_OTHER\")))\n (check-true (not (env-get env \"FOO_SECRET\")))\n ;; No home override means HOME inherits (it's in *default-forwarded*)\n (check (env-get env \"HOME\") => \"/home/user\"))\n\n;; Explicit ,env adds the var to the allowlist; ,secret similarly.\n(launch-policy-reset!)\n(launch-policy-add-env! \"MYVAR\")\n(launch-policy-add-secret! \"MY_API_KEY\")\n(let* ([parent '((\"MYVAR\" . \"vv\")\n (\"MY_API_KEY\" . \"k-1234\")\n (\"LEAK\" . \"no\"))]\n [env (policy-build-child-env parent\n (current-launch-policy) #f #f)])\n (check (env-get env \"MYVAR\") => \"vv\")\n (check (env-get env \"MY_API_KEY\") => \"k-1234\")\n (check-true (not (env-get env \"LEAK\"))))\n\n;; HOME override applied even when HOME isn't explicitly in allowlist.\n(launch-policy-reset!)\n(let* ([parent '((\"HOME\" . \"/orig\"))]\n [env (policy-build-child-env parent\n (current-launch-policy) \"/tmp/fake\" #f)])\n (check (env-get env \"HOME\") => \"/tmp/fake\"))\n\n;; cache override sets XDG_CACHE_HOME, NPM_CONFIG_CACHE, etc.\n(launch-policy-reset!)\n(let* ([parent '((\"HOME\" . \"/orig\"))]\n [env (policy-build-child-env parent\n (current-launch-policy) #f \"/scratch/cache\")])\n (check (env-get env \"XDG_CACHE_HOME\") => \"/scratch/cache\")\n (check (env-get env \"TMPDIR\") => \"/scratch/cache\")\n (check (env-get env \"TMP\") => \"/scratch/cache\")\n (check (env-get env \"TEMP\") => \"/scratch/cache\")\n (check (env-get env \"NPM_CONFIG_CACHE\") => \"/scratch/cache\")\n (check (env-get env \"CARGO_HOME\") => \"/scratch/cache\"))\n\n;; Redaction: secret values appear as [redacted] in audit output, but\n;; non-secret vars pass through unchanged.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"MY_API_KEY\")\n(let* ([env '(\"PATH=/bin\" \"MY_API_KEY=hunter2\" \"FOO=bar\")]\n [red (policy-redact-env-for-audit env (current-launch-policy))])\n (check (env-get red \"PATH\") => \"/bin\")\n (check (env-get red \"FOO\") => \"bar\")\n (check (env-get red \"MY_API_KEY\") => \"[redacted]\"))\n\n;; HOME / cache resolution\n(launch-policy-reset!)\n(launch-policy-set-home! \"/abs/path\")\n(check (policy-resolve-home (current-launch-policy)) => \"/abs/path\")\n\n(launch-policy-reset!)\n(launch-policy-set-cache! \"@cap\")\n(check (policy-resolve-cache (current-launch-policy)) => #f) ;; Phase-7 capability\n\n;; ,run-policy defaults to fake (ephemeral) HOME — no policy field set\n;; produces a per-run temp dir, not #f. ,home real opts back in to parent.\n(launch-policy-reset!)\n(let ([p (policy-resolve-home (current-launch-policy))])\n (check-true (string? p))\n (check-true (file-exists? p))\n (check-true (search-substring? p \"jsh-policy-home-\"))\n (policy-cleanup-temp-dirs!))\n\n(launch-policy-reset!)\n(launch-policy-set-home! \"real\")\n(check (policy-resolve-home (current-launch-policy)) => #f)\n\n(launch-policy-reset!)\n(launch-policy-set-home! \"inherit\")\n(check (policy-resolve-home (current-launch-policy)) => #f)\n\n;; Cache stays opt-in (so existing npm cache flows keep working).\n(launch-policy-reset!)\n(check (policy-resolve-cache (current-launch-policy)) => #f)\n\n;; ephemeral creates a real directory and tracks it for cleanup\n(launch-policy-reset!)\n(launch-policy-set-home! \"ephemeral\")\n(let ([p (policy-resolve-home (current-launch-policy))])\n (check-true (string? p))\n (check-true (file-exists? p))\n (policy-cleanup-temp-dirs!)\n (check-true (not (file-exists? p))))\n\n;; policy-make-temp-dir works standalone with a tag suffix.\n(let ([d (policy-make-temp-dir \"test\")])\n (check-true (string? d))\n (check-true (search-substring? d \"jsh-policy-test-\"))\n (check-true (file-exists? d))\n (policy-cleanup-temp-dirs!)\n (check-true (not (file-exists? d))))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 10d. Launch-policy Phase 3 — shim-first PATH\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy Phase 3 tests ---~n\")\n\n(import\n (only (jsh limits)\n *default-shim-tools*\n policy-make-shim-dir policy-prepare-shim-dir!\n policy-shim-target-for policy-explicit-exec-match))\n\n;; Read a file's first N bytes as a string (test helper).\n(define (slurp-file path)\n (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([c (read-char p)])\n (cond\n [(eof-object? c) (list->string (reverse acc))]\n [else (loop (cons c acc))]))))))\n\n;; Default table has both pass-mode and deny-mode tools.\n(check-true (pair? (assoc \"node\" *default-shim-tools*)))\n(check-true (pair? (assoc \"npm\" *default-shim-tools*)))\n(check-true (pair? (assoc \"npx\" *default-shim-tools*)))\n(check (cadr (assoc \"node\" *default-shim-tools*)) => 'pass)\n(check (cadr (assoc \"npx\" *default-shim-tools*)) => 'deny)\n(check (cadr (assoc \"npm\" *default-shim-tools*)) => 'pass)\n(check (caddr (assoc \"npm\" *default-shim-tools*)) => \"--ignore-scripts\")\n\n;; ,exec /opt/foo/npx — basename match wins over deny default.\n(launch-policy-reset!)\n(launch-policy-add-exec! \"/opt/foo/npx\")\n(check (policy-explicit-exec-match (current-launch-policy) \"npx\")\n => \"/opt/foo/npx\")\n(check (policy-explicit-exec-match (current-launch-policy) \"node\")\n => #f)\n(check (policy-shim-target-for (current-launch-policy) \"npx\")\n => \"/opt/foo/npx\")\n\n;; ,exec <basename> must resolve via PATH so the shim writes an absolute\n;; target — otherwise `exec NAME` in the shim re-resolves to the shim dir\n;; (first on PATH) and infinite-loops. Use /bin/sh as a basename that\n;; exists on every supported platform.\n(launch-policy-reset!)\n(launch-policy-add-exec! \"sh\")\n(let ([match (policy-explicit-exec-match (current-launch-policy) \"sh\")])\n (check-true (and (string? match)\n (> (string-length match) 0)\n (char=? (string-ref match 0) #\\/))))\n;; Bare basename that does not exist on PATH resolves to #f (no shim\n;; written, child gets \"command not found\" — better than a loop).\n(launch-policy-reset!)\n(launch-policy-add-exec! \"definitely-not-a-real-binary-xyz123\")\n(check (policy-explicit-exec-match (current-launch-policy)\n \"definitely-not-a-real-binary-xyz123\")\n => #f)\n\n;; policy-make-shim-dir creates outer + shims/ and tracks outer for cleanup.\n(launch-policy-reset!)\n(let ([dir (policy-make-shim-dir)])\n (check-true (string? dir))\n (check-true (search-substring? dir \"jsh-run-\"))\n (check-true (search-substring? dir \"/shims\"))\n (check-true (file-exists? dir))\n ;; cleanup wipes the OUTER dir (and recurses into shims/)\n (policy-cleanup-temp-dirs!)\n (check-true (not (file-exists? dir))))\n\n;; policy-prepare-shim-dir! writes both pass and deny shims.\n(launch-policy-reset!)\n(let ([dir (policy-prepare-shim-dir! (current-launch-policy))])\n (check-true (string? dir))\n (check-true (file-exists? dir))\n ;; npx shim exists, has deny content, executable bit set (mode 0500)\n (let ([npx-path (string-append dir \"/npx\")])\n (check-true (file-exists? npx-path))\n (let ([body (slurp-file npx-path)])\n (check-true (search-substring? body \"exit 126\"))\n (check-true (search-substring? body \"denied by current launch policy\"))))\n ;; node shim exists when /usr/bin/node or similar is on PATH; if not\n ;; resolvable, the shim is skipped (we don't fabricate a target).\n ;; npm shim, when written, must reference --ignore-scripts.\n (when (file-exists? (string-append dir \"/npm\"))\n (let ([body (slurp-file (string-append dir \"/npm\"))])\n (check-true (search-substring? body \"--ignore-scripts\"))\n (check-true (search-substring? body \"exec \"))))\n (policy-cleanup-temp-dirs!)\n (check-true (not (file-exists? dir))))\n\n;; ,exec override flips npx from deny → pass-through using the override target.\n;; npx is in *default-shim-tools* with mode 'deny by default; ,exec swaps in\n;; a real target and the shim becomes a pass-through.\n(launch-policy-reset!)\n(launch-policy-add-exec! \"/usr/bin/true\") ;; basename \"true\" — not a shim, ignored\n(launch-policy-add-exec! \"/opt/local/bin/npx\") ;; basename \"npx\" — overrides deny\n(let ([dir (policy-prepare-shim-dir! (current-launch-policy))])\n (check-true (string? dir))\n (let ([body (slurp-file (string-append dir \"/npx\"))])\n ;; deny content gone; pass-through to the explicit target\n (check-true (not (search-substring? body \"exit 126\")))\n (check-true (search-substring? body \"exec '/opt/local/bin/npx'\")))\n (policy-cleanup-temp-dirs!))\n\n;; policy-build-child-env with shim-prefix prepends it to PATH.\n(launch-policy-reset!)\n(let* ([parent '((\"PATH\" . \"/usr/bin:/bin\")\n (\"HOME\" . \"/h\"))]\n [env (policy-build-child-env parent\n (current-launch-policy) #f #f \"/tmp/shims-x\")])\n (check (env-get env \"PATH\") => \"/tmp/shims-x:/usr/bin:/bin\"))\n\n;; shim-prefix without a parent PATH still sets PATH to the shim dir alone.\n(launch-policy-reset!)\n(let* ([parent '((\"HOME\" . \"/h\"))]\n [env (policy-build-child-env parent\n (current-launch-policy) #f #f \"/tmp/shims-y\")])\n (check (env-get env \"PATH\") => \"/tmp/shims-y\"))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 10e. Launch-policy Phase 4 — sandbox/rl bridging + degraded warnings\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy Phase 4 tests ---~n\")\n\n(import\n (only (jsh limits)\n parse-time-to-ms parse-mem-to-fraction\n policy-sandbox-opts policy-memlimit-fraction\n policy-degraded-warnings\n policy-path-sandbox-available?))\n\n;; ---- parse-time-to-ms ----\n(check (parse-time-to-ms \"200\") => 200)\n(check (parse-time-to-ms \"30s\") => 30000)\n(check (parse-time-to-ms \"5m\") => 300000)\n(check (parse-time-to-ms \"1h\") => 3600000)\n(check (parse-time-to-ms \"\") => #f)\n(check (parse-time-to-ms \"30x\") => #f)\n(check (parse-time-to-ms \"abc\") => #f)\n\n;; ---- parse-mem-to-fraction ----\n(check (parse-mem-to-fraction \"30%\") => 0.30)\n(check (parse-mem-to-fraction \"100%\") => 1.0)\n(check (parse-mem-to-fraction \"0.5\") => 0.5)\n(check (parse-mem-to-fraction \"0%\") => #f) ;; must be > 0\n(check (parse-mem-to-fraction \"150%\") => #f) ;; > 100 rejected\n(check (parse-mem-to-fraction \"2g\") => #f) ;; byte form not supported\n(check (parse-mem-to-fraction \"1\") => 1.0)\n(check (parse-mem-to-fraction \"\") => #f)\n\n;; ---- policy-sandbox-opts ----\n;; Empty policy → empty alist.\n(launch-policy-reset!)\n(check (policy-sandbox-opts (current-launch-policy)) => '())\n\n;; reads/writes/execs/nets/time populate matching keys.\n(launch-policy-reset!)\n(launch-policy-add-read! \"/etc\")\n(launch-policy-add-write! \"/tmp\")\n(launch-policy-add-exec! \"/usr/bin/git\")\n(launch-policy-add-net! \"api.example.com:443\")\n(launch-policy-add-limit! \"time\" \"5m\")\n(let ([opts (policy-sandbox-opts (current-launch-policy))])\n (check (cdr (assoc \"allow-read\" opts)) => '(\"/etc\"))\n (check (cdr (assoc \"allow-write\" opts)) => '(\"/tmp\"))\n ;; ,exec is enforced directly by the launcher now; it is not forwarded to\n ;; the degraded sandbox wrapper where it would create a false warning.\n (check (assoc \"allow-exec\" opts) => #f)\n (check (cdr (assoc \"allow-net\" opts)) => #t)\n (check (cdr (assoc \"timeout-ms\" opts)) => 300000))\n\n;; non-time limits should NOT appear in sandbox opts.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"mem\" \"30%\")\n(launch-policy-add-limit! \"cpu\" \"4\")\n(let ([opts (policy-sandbox-opts (current-launch-policy))])\n (check (assoc \"timeout-ms\" opts) => #f))\n\n;; ---- policy-memlimit-fraction ----\n(launch-policy-reset!)\n(check (policy-memlimit-fraction (current-launch-policy)) => #f)\n\n(launch-policy-reset!)\n(launch-policy-add-limit! \"mem\" \"30%\")\n(check (policy-memlimit-fraction (current-launch-policy)) => 0.30)\n\n(launch-policy-reset!)\n(launch-policy-add-limit! \"mem\" \"2g\") ;; byte form → unsupported\n(check (policy-memlimit-fraction (current-launch-policy)) => #f)\n\n;; ---- policy-degraded-warnings ----\n(define (test-platform-linux?)\n (let ([mt (symbol->string (machine-type))])\n (and (>= (string-length mt) 2)\n (string=? (substring mt (- (string-length mt) 2) (string-length mt))\n \"le\"))))\n\n(define (test-platform-macos?)\n (let ([mt (symbol->string (machine-type))])\n (and (>= (string-length mt) 3)\n (string=? (substring mt (- (string-length mt) 3) (string-length mt))\n \"osx\"))))\n\n(define (test-path-sandbox-available?)\n (policy-path-sandbox-available?))\n\n;; Empty policy → no warnings.\n(launch-policy-reset!)\n(check (policy-degraded-warnings (current-launch-policy)) => '())\n\n;; ,deny-localnet → warning.\n(launch-policy-reset!)\n(launch-policy-add-deny! \"localnet\")\n(let ([ws (policy-degraded-warnings (current-launch-policy))])\n (check (length ws) => 1)\n (check-true (search-substring? (car ws) \",deny localnet\")))\n\n;; ,limit warnings now come from the per-platform limit-plan. cpu is safe in\n;; target pre-exec, time/out are parent-supervised, and\n;; pids uses parent-side process-tree supervision on Linux and a target\n;; pre-exec rlimit elsewhere.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"cpu\" \"4\")\n(launch-policy-add-limit! \"time\" \"1s\")\n(launch-policy-add-limit! \"pids\" \"100\")\n(launch-policy-add-limit! \"fsize\" \"1g\")\n(launch-policy-add-limit! \"out\" \"10m\")\n(let ([ws (policy-degraded-warnings (current-launch-policy))])\n (check (length ws) => 0))\n\n;; ,limit mem is target-preexec on non-macOS. Darwin's usable memory rlimits do\n;; not provide the contract we need, so it warns/fails closed there.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"mem\" \"2g\")\n(let ([ws (policy-degraded-warnings (current-launch-policy))])\n (check (length ws) => (if (test-platform-macos?) 1 0)))\n\n(launch-policy-reset!)\n(launch-policy-add-limit! \"mem\" \"30%\")\n(let ([ws (policy-degraded-warnings (current-launch-policy))])\n (check (length ws) => (if (test-platform-macos?) 1 0)))\n\n;; ,net presence → diagnostic before fail-closed launch refusal.\n(launch-policy-reset!)\n(launch-policy-add-net! \"api.example.com:443\")\n(let ([ws (policy-degraded-warnings (current-launch-policy))])\n (check (length ws) => (if (test-path-sandbox-available?) 0 1))\n (when (pair? ws)\n (check-true (search-substring? (car ws) \"launch will fail closed\"))))\n\n;; ,tracefs is unavailable on macOS (→ diagnostic/refusal); on Linux it's\n;; enforced iff strace is installed (→ depends). ,workspace overlay also\n;; diagnoses/refuses until an overlay backend exists. ,audit-out is implemented.\n(launch-policy-reset!)\n(launch-policy-set-tracefs! \"summary\")\n(launch-policy-set-audit-out! \"/tmp/audit.jsonl\")\n(launch-policy-set-workspace! \"overlay\")\n(let ([ws (policy-degraded-warnings (current-launch-policy))])\n ;; overlay always warns; tracefs warns iff platform unavailable.\n (check-true (or (= (length ws) 1) (= (length ws) 2))))\n\n;;; ──────────────────────────────────────────────────────────────\n;;; 10f. Launch-policy Phase 6 — JSONL audit output\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy Phase 6 tests ---~n\")\n\n;; audit-jsonl-line returns a string with required type + ts + given fields.\n(launch-policy-reset!)\n(let ([line (audit-jsonl-line \"test_record\" '((\"k\" . \"v\")))])\n (check-true (string? line))\n (check-true (search-substring? line \"\\\"type\\\":\\\"test_record\\\"\"))\n (check-true (search-substring? line \"\\\"ts\\\":\"))\n (check-true (search-substring? line \"\\\"k\\\":\\\"v\\\"\")))\n\n;; audit-emit! is a no-op when audit-out is unset.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-audit-noop-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (audit-emit! (current-launch-policy) \"noop\" '((\"x\" . 1)))\n (check-true (not (file-exists? tmp))))\n\n;; audit-emit! writes a JSONL line to the configured path.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-audit-write-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (launch-policy-set-audit-out! tmp)\n (audit-emit! (current-launch-policy) \"first\" '((\"a\" . 1)))\n (audit-emit! (current-launch-policy) \"second\" '((\"b\" . 2)))\n (let* ([port (open-input-file tmp)]\n [l1 (get-line port)]\n [l2 (get-line port)]\n [eof (get-line port)])\n (close-port port)\n (check-true (search-substring? l1 \"\\\"type\\\":\\\"first\\\"\"))\n (check-true (search-substring? l1 \"\\\"a\\\":1\"))\n (check-true (search-substring? l2 \"\\\"type\\\":\\\"second\\\"\"))\n (check-true (search-substring? l2 \"\\\"b\\\":2\"))\n (check-true (eof-object? eof)))\n (delete-file tmp))\n\n;; JSON string escaping handles quotes, backslashes, and control chars.\n(let ([line (audit-jsonl-line \"esc\" `((\"s\" . \"ab\\\"c\\\\d\\ne\")))])\n (check-true (search-substring? line \"\\\"s\\\":\\\"ab\\\\\\\"c\\\\\\\\d\\\\ne\\\"\")))\n\n;; Secret NAMES appear in policy_parsed; VALUES never appear in any record.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-audit-secret-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (launch-policy-set-audit-out! tmp)\n (launch-policy-add-secret! \"API_TOKEN\")\n (audit-emit-policy-parsed! (current-launch-policy))\n (audit-emit-paths-resolved! (current-launch-policy) #f #f #f)\n (audit-emit-command-launched! (current-launch-policy) \"/bin/true\" '(\"/bin/true\") 42)\n (audit-emit-exit-status! (current-launch-policy) 0)\n (let* ([port (open-input-file tmp)]\n [content (let loop ([acc \"\"])\n (let ([l (get-line port)])\n (cond [(eof-object? l) acc]\n [else (loop (string-append acc l \"\\n\"))])))])\n (close-port port)\n ;; NAME present in two records (policy_parsed.fields.secret + secrets_injected.names).\n (check-true (search-substring? content \"API_TOKEN\"))\n ;; No literal value (we never set one, but also confirm placeholder\n ;; literal \"secret-value\" never sneaks in).\n (check-true (not (search-substring? content \"secret-value\")))\n (check-true (search-substring? content \"\\\"type\\\":\\\"policy_parsed\\\"\"))\n (check-true (search-substring? content \"\\\"type\\\":\\\"secrets_injected\\\"\"))\n (check-true (search-substring? content \"\\\"type\\\":\\\"command_launched\\\"\"))\n (check-true (search-substring? content \"\\\"type\\\":\\\"exit_status\\\"\")))\n (delete-file tmp))\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10g. Launch-policy Phase 7a — executable identity for ,flag-bin\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy Phase 7a tests ---~n\")\n\n(import (only (jsh limits)\n policy-resolve-flag-bin policy-resolved-flag-bins\n audit-emit-flag-bins-resolved!))\n\n;; Absolute-path flag-bin entries resolve directly; realpath==path for\n;; non-symlinks like /bin/sh on most systems.\n(let ([r (policy-resolve-flag-bin (cons \"/bin/sh\" \"shell\"))])\n (check (cdr (assoc \"name\" r)) => \"/bin/sh\")\n (check (cdr (assoc \"tag\" r)) => \"shell\")\n (check (cdr (assoc \"path\" r)) => \"/bin/sh\")\n (check-true (string? (cdr (assoc \"realpath\" r))))\n (check-true (> (string-length (cdr (assoc \"realpath\" r))) 0)))\n\n;; PATH lookup: pick a name that's always on PATH (\"ls\" or \"sh\").\n(let ([r (policy-resolve-flag-bin (cons \"sh\" \"shell\"))])\n (check (cdr (assoc \"name\" r)) => \"sh\")\n ;; \"path\" should be absolute (begins with /) when which() succeeds.\n (let ([p (cdr (assoc \"path\" r))])\n (check-true (and (string? p)\n (> (string-length p) 0)\n (char=? (string-ref p 0) #\\/)))))\n\n;; Unknown name: path stays empty, realpath stays empty, no crash.\n(let ([r (policy-resolve-flag-bin\n (cons \"absolutely-not-a-real-binary-xyz-7a\" \"tag\"))])\n (check (cdr (assoc \"path\" r)) => \"\")\n (check (cdr (assoc \"realpath\" r)) => \"\"))\n\n;; policy-resolved-flag-bins maps all entries in insertion order.\n(launch-policy-reset!)\n(launch-policy-add-flag-bin! \"/bin/sh\" \"shell\")\n(launch-policy-add-flag-bin! \"/bin/echo\" \"coreutil\")\n(let ([rs (policy-resolved-flag-bins (current-launch-policy))])\n (check (length rs) => 2)\n (check (cdr (assoc \"name\" (car rs))) => \"/bin/sh\")\n (check (cdr (assoc \"name\" (cadr rs))) => \"/bin/echo\"))\n\n;; audit-emit-flag-bins-resolved! writes one record per entry.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-audit-fb-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (launch-policy-set-audit-out! tmp)\n (launch-policy-add-flag-bin! \"/bin/sh\" \"shell\")\n (launch-policy-add-flag-bin! \"/bin/echo\" \"coreutil\")\n (audit-emit-flag-bins-resolved! (current-launch-policy))\n (let* ([port (open-input-file tmp)]\n [l1 (get-line port)]\n [l2 (get-line port)]\n [eof (get-line port)])\n (close-port port)\n (check-true (search-substring? l1 \"\\\"type\\\":\\\"flag_bin_resolved\\\"\"))\n (check-true (search-substring? l1 \"\\\"name\\\":\\\"/bin/sh\\\"\"))\n (check-true (search-substring? l1 \"\\\"tag\\\":\\\"shell\\\"\"))\n (check-true (search-substring? l2 \"\\\"name\\\":\\\"/bin/echo\\\"\"))\n (check-true (search-substring? l2 \"\\\"tag\\\":\\\"coreutil\\\"\"))\n (check-true (eof-object? eof)))\n (delete-file tmp))\n\n;; ,flag-bin no longer triggers a degraded warning (Phase 7a wires it).\n(launch-policy-reset!)\n(launch-policy-add-flag-bin! \"node\" \"untrusted\")\n(check (policy-degraded-warnings (current-launch-policy)) => '())\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10g+. Launch-policy — @capability resolution\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy capability tests ---~n\")\n\n(import (only (jsh limits)\n policy-resolve-capability\n policy-capability?\n policy-resolve-paths\n audit-emit-capabilities-resolved!))\n\n;; Predicate\n(check-true (policy-capability? \"@project\"))\n(check-true (policy-capability? \"@cache:npm\"))\n(check-true (not (policy-capability? \"\")))\n(check-true (not (policy-capability? \"/abs/path\")))\n(check-true (not (policy-capability? \"relative\")))\n\n;; @scratch -> per-run temp dir (caller cleans up).\n(launch-policy-reset!)\n(let ([p (policy-resolve-capability \"@scratch\")])\n (check-true (string? p))\n (check-true (file-exists? p))\n (check-true (search-substring? p \"jsh-policy-scratch-\")))\n(policy-cleanup-temp-dirs!)\n\n;; @cache:npm -> per-name temp dir.\n(launch-policy-reset!)\n(let ([p (policy-resolve-capability \"@cache:npm\")])\n (check-true (string? p))\n (check-true (search-substring? p \"jsh-policy-cache-npm-\")))\n(policy-cleanup-temp-dirs!)\n\n;; @config:tool -> per-tool temp dir.\n(launch-policy-reset!)\n(let ([p (policy-resolve-capability \"@config:codex\")])\n (check-true (string? p))\n (check-true (search-substring? p \"jsh-policy-config-codex-\")))\n(policy-cleanup-temp-dirs!)\n\n;; @home -> parent's real HOME.\n(launch-policy-reset!)\n(let ([p (policy-resolve-capability \"@home\")])\n (check (or p \"\") => (or (getenv \"HOME\") \"\")))\n(policy-cleanup-temp-dirs!)\n\n;; @transaction-workspace resolves only when a workspace is active.\n(launch-policy-reset!)\n(check (policy-resolve-capability \"@transaction-workspace\") => #f)\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"transaction\")\n(let ([p (policy-resolve-capability \"@transaction-workspace\")])\n (check-true (string? p))\n (check-true (file-exists? p))\n (check-true (search-substring? p \"jsh-policy-workspace-tx-\")))\n(policy-cleanup-temp-dirs!)\n\n;; Repeated resolution of @scratch returns the SAME path (memoized\n;; within one run; cleared by policy-cleanup-temp-dirs!).\n(launch-policy-reset!)\n(let ([a (policy-resolve-capability \"@scratch\")]\n [b (policy-resolve-capability \"@scratch\")])\n (check a => b))\n(policy-cleanup-temp-dirs!)\n\n;; Unknown capability resolves to #f, not a crash.\n(launch-policy-reset!)\n(check (policy-resolve-capability \"@nope\") => #f)\n(check (policy-resolve-capability \"@cache:\") => #f)\n(policy-cleanup-temp-dirs!)\n\n;; policy-resolve-paths handles a mix of @cap, ~, and literal absolutes.\n(launch-policy-reset!)\n(let-values ([(resolved unresolved)\n (policy-resolve-paths\n '(\"@scratch\" \"/etc/hosts\" \"~/file\" \"@nope\"))])\n (check (length resolved) => 3) ;; scratch + /etc/hosts + tilde-expansion\n (check (length unresolved) => 1) ;; @nope is unresolvable\n (check (caar unresolved) => \"@nope\"))\n(policy-cleanup-temp-dirs!)\n\n;; ,home @scratch resolves to a temp dir, not the literal \"@scratch\".\n(launch-policy-reset!)\n(launch-policy-set-home! \"@scratch\")\n(let ([h (policy-resolve-home (current-launch-policy))])\n (check-true (string? h))\n (check-true (search-substring? h \"jsh-policy-scratch-\"))\n (policy-cleanup-temp-dirs!))\n\n;; Audit emit writes a record per distinct @cap referenced.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-audit-caps-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (launch-policy-set-audit-out! tmp)\n (launch-policy-add-read! \"@project\")\n (launch-policy-add-write! \"@scratch\")\n (launch-policy-add-read! \"/etc/hosts\") ;; not a cap, skipped\n (audit-emit-capabilities-resolved! (current-launch-policy))\n (let* ([port (open-input-file tmp)]\n [lines (let loop ([acc '()])\n (let ([l (get-line port)])\n (cond [(eof-object? l) (reverse acc)]\n [else (loop (cons l acc))])))])\n (close-port port)\n (check (length lines) => 2)\n (for-each\n (lambda (l)\n (check-true (search-substring? l \"\\\"type\\\":\\\"capability_resolved\\\"\")))\n lines))\n (policy-cleanup-temp-dirs!)\n (delete-file tmp))\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10g++. Launch-policy — exec allowlist enforcement\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy exec allowlist tests ---~n\")\n\n(import (only (jsh limits)\n policy-realpath-of\n policy-exec-allowlist-realpaths\n policy-check-exec-allowed?\n policy-launch-refusal-reason\n policy-path-sandbox-available?\n policy-net-direct-sandbox-available?\n tracefs-capabilities\n audit-emit-exec-denied!))\n\n;; realpath of a known-existing absolute file resolves to a non-empty string.\n(let ([r (policy-realpath-of \"/bin/sh\")])\n (check-true (string? r))\n (check-true (> (string-length r) 0)))\n\n;; Unknown path is returned as-is (best-effort), never crash.\n(let ([r (policy-realpath-of \"/nonexistent/zzz\")])\n (check-true (or (eq? r #f) (string? r))))\n\n;; Empty ,exec allowlist refuses any target under strict fail-closed policy.\n(launch-policy-reset!)\n(check-true (not (policy-check-exec-allowed? (current-launch-policy) \"/bin/sh\")))\n(check-true (not (policy-check-exec-allowed? (current-launch-policy) \"/bin/echo\")))\n\n;; Listed absolute target is allowed; non-listed sibling is denied.\n(launch-policy-reset!)\n(launch-policy-add-exec! \"/bin/sh\")\n(check-true (policy-check-exec-allowed? (current-launch-policy) \"/bin/sh\"))\n(check-true (not (policy-check-exec-allowed? (current-launch-policy)\n \"/bin/echo\")))\n\n;; ,exec entries that don't exist are dropped from the realpath allowlist.\n(launch-policy-reset!)\n(launch-policy-add-exec! \"/nonexistent/zzz-xyz\")\n(check (policy-exec-allowlist-realpaths (current-launch-policy)) => '())\n\n;; PATH-style bare-name entries resolve through which() and then realpath().\n(launch-policy-reset!)\n(launch-policy-add-exec! \"sh\")\n(let ([allow (policy-exec-allowlist-realpaths (current-launch-policy))])\n (check (length allow) => 1)\n (check-true (and (string? (car allow))\n (> (string-length (car allow)) 0)\n (char=? (string-ref (car allow) 0) #\\/))))\n\n;; Symlink case: /bin/sh on macOS is a symlink. policy-check-exec-allowed?\n;; matches by realpath, so the listing should normalize to the same target.\n;; (Skipped if /bin/sh is not a symlink on the current platform — assertion\n;; is just that whatever realpath returns equals itself.)\n(launch-policy-reset!)\n(launch-policy-add-exec! \"/bin/sh\")\n(let* ([target \"/bin/sh\"]\n [rp (policy-realpath-of target)]\n [allowed (policy-check-exec-allowed? (current-launch-policy) rp)])\n (check-true allowed))\n\n;; exec_denied audit record carries target, realpath, and allowlist.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-audit-exec-deny-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (launch-policy-set-audit-out! tmp)\n (launch-policy-add-exec! \"/bin/sh\")\n (audit-emit-exec-denied! (current-launch-policy)\n \"/bin/echo\"\n (policy-realpath-of \"/bin/echo\")\n (policy-exec-allowlist-realpaths\n (current-launch-policy)))\n (let* ([port (open-input-file tmp)] [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"exec_denied\\\"\"))\n (check-true (search-substring? line \"\\\"target\\\":\\\"/bin/echo\\\"\"))\n (check-true (search-substring? line \"\\\"target_realpath\\\"\")))\n\t (delete-file tmp))\n\n;; Refusal reason covers unsupported launch-policy axes before fork/exec.\n(launch-policy-reset!)\n(launch-policy-add-deny! \"home\")\n(if (policy-path-sandbox-available?)\n (check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n (check-true (search-substring?\n (policy-launch-refusal-reason (current-launch-policy))\n \",read/,write/,deny-home require\")))\n\t(launch-policy-reset!)\n\t(launch-policy-add-net! \"example.com:443\")\n\t(if (policy-net-direct-sandbox-available?)\n\t (check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t (check-true (search-substring?\n\t (policy-launch-refusal-reason (current-launch-policy))\n\t \",net requires\")))\n\t(launch-policy-reset!)\n\t(launch-policy-add-read! \"/etc\")\n\t(if (policy-path-sandbox-available?)\n\t (check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t (check-true (search-substring?\n\t (policy-launch-refusal-reason (current-launch-policy))\n\t \",read/,write/,deny-home require\")))\n\t(launch-policy-reset!)\n\t(launch-policy-add-write! \"/tmp/jsh-test\")\n\t(if (policy-path-sandbox-available?)\n\t (check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t (check-true (search-substring?\n\t (policy-launch-refusal-reason (current-launch-policy))\n\t \",read/,write/,deny-home require\")))\n\t(launch-policy-reset!)\n\t(launch-policy-add-deny! \"home\")\n\t(launch-policy-add-read! \"@home\")\n\t(if (policy-path-sandbox-available?)\n\t (check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t (check-true (search-substring?\n\t (policy-launch-refusal-reason (current-launch-policy))\n\t \",read/,write/,deny-home require\")))\n\t(launch-policy-reset!)\n\t(launch-policy-add-limit! \"cpu\" \"1\")\n\t(check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t(launch-policy-reset!)\n\t(launch-policy-add-limit! \"time\" \"1s\")\n\t(check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t(launch-policy-reset!)\n\t(launch-policy-add-limit! \"out\" \"1\")\n\t(check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t(launch-policy-reset!)\n\t(launch-policy-add-limit! \"fsize\" \"1m\")\n\t(check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t(launch-policy-reset!)\n\t(launch-policy-add-limit! \"out\" \"1\")\n\t(launch-policy-add-limit! \"pids\" \"1\")\n\t(check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t(launch-policy-reset!)\n\t(launch-policy-set-tracefs! \"summary\")\n\t(if (string=? (cdr (assoc \"status\" (tracefs-capabilities))) \"installed\")\n\t (check (policy-launch-refusal-reason (current-launch-policy)) => #f)\n\t (check-true (search-substring?\n\t (policy-launch-refusal-reason (current-launch-policy))\n\t \",tracefs requested\")))\n\t(launch-policy-reset!)\n\t(launch-policy-add-read! \"@nope\")\n\t(check-true (search-substring?\n\t (policy-launch-refusal-reason (current-launch-policy))\n \"unresolved capability\"))\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10h. Launch-policy — argv secret leak prevention + audit redaction\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy argv-secret tests ---~n\")\n\n(import (only (jsh limits)\n policy-secret-values\n policy-redact-argv-for-audit\n policy-scan-argv-for-secrets\n policy-check-argv-secrets\n audit-emit-policy-violation!))\n\n;; policy-secret-values: NAME=VAL tags surface VAL; bare names read env.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"API_TOKEN=hunter2\")\n(check (member \"hunter2\" (policy-secret-values (current-launch-policy)))\n => '(\"hunter2\"))\n\n;; Bare name resolves via getenv at call time.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"JSH_TEST_SECRET_XYZ\")\n(check (policy-secret-values (current-launch-policy)) => '())\n;; (setenv may not exist on Chez — we test the present-value path via\n;; NAME=VAL form instead.)\n\n;; Empty secret list -> argv passes through unchanged.\n(launch-policy-reset!)\n(check (policy-redact-argv-for-audit '(\"cmd\" \"--flag\" \"val\")\n (current-launch-policy))\n => '(\"cmd\" \"--flag\" \"val\"))\n\n;; Exact-match argv element gets fully replaced with [redacted].\n(launch-policy-reset!)\n(launch-policy-add-secret! \"API_TOKEN=hunter2\")\n(check (policy-redact-argv-for-audit '(\"curl\" \"-H\" \"hunter2\")\n (current-launch-policy))\n => '(\"curl\" \"-H\" \"[redacted]\"))\n\n;; Substring within a \"--flag=value\" argv element gets redacted in place.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"API_TOKEN=hunter2\")\n(check (policy-redact-argv-for-audit '(\"myprog\" \"--token=hunter2\")\n (current-launch-policy))\n => '(\"myprog\" \"--token=[redacted]\"))\n\n;; Multiple secret values redact independently.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"A=alpha\")\n(launch-policy-add-secret! \"B=beta\")\n(check (policy-redact-argv-for-audit '(\"cmd\" \"alpha\" \"beta\" \"gamma\")\n (current-launch-policy))\n => '(\"cmd\" \"[redacted]\" \"[redacted]\" \"gamma\"))\n\n;; Scan returns #f when no secrets in argv.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"API_TOKEN=hunter2\")\n(check (policy-scan-argv-for-secrets '(\"cmd\" \"--safe\" \"value\")\n (current-launch-policy))\n => #f)\n\n;; Scan returns 1-based positions of offending argv entries.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"API_TOKEN=hunter2\")\n(check (policy-scan-argv-for-secrets '(\"cmd\" \"--token=hunter2\")\n (current-launch-policy))\n => '(1))\n(check (policy-scan-argv-for-secrets '(\"cmd\" \"ok\" \"hunter2\" \"ok\")\n (current-launch-policy))\n => '(2))\n\n;; Argv[0] is never scanned (executable name shouldn't be a secret).\n(launch-policy-reset!)\n(launch-policy-add-secret! \"API_TOKEN=hunter2\")\n(check (policy-scan-argv-for-secrets '(\"hunter2\")\n (current-launch-policy))\n => #f)\n\n;; policy-check-argv-secrets returns #t on safe argv.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"API_TOKEN=hunter2\")\n(let ([err-port (open-output-string)])\n (check (policy-check-argv-secrets (current-launch-policy)\n '(\"cmd\" \"--ok\") err-port)\n => #t))\n\n;; policy-check-argv-secrets returns #f and writes refusal on leak.\n(launch-policy-reset!)\n(launch-policy-add-secret! \"API_TOKEN=hunter2\")\n(let ([err-port (open-output-string)])\n (check (policy-check-argv-secrets (current-launch-policy)\n '(\"cmd\" \"--token=hunter2\") err-port)\n => #f)\n (let ([out (get-output-string err-port)])\n (check-true (search-substring? out \"secret value leaks\"))\n (check-true (search-substring? out \"(1)\"))))\n\n;; Refusal emits a policy_violation audit record.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-audit-violation-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (launch-policy-set-audit-out! tmp)\n (launch-policy-add-secret! \"API_TOKEN=hunter2\")\n (let ([err-port (open-output-string)])\n (policy-check-argv-secrets (current-launch-policy)\n '(\"cmd\" \"hunter2\") err-port))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"policy_violation\\\"\"))\n (check-true (search-substring? line \"\\\"reason\\\":\\\"argv_secret_leak\\\"\"))\n (check-true (search-substring? line \"\\\"positions\\\":[1]\")))\n (delete-file tmp))\n\n;; Audit emit of command_launched applies argv redaction automatically.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-audit-argv-redact-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (launch-policy-set-audit-out! tmp)\n (launch-policy-add-secret! \"API_TOKEN=hunter2\")\n (audit-emit-command-launched! (current-launch-policy) \"/bin/curl\"\n '(\"/bin/curl\" \"-H\" \"Bearer hunter2\")\n 4242)\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"command_launched\\\"\"))\n ;; Raw secret value never appears.\n (check-true (not (search-substring? line \"hunter2\")))\n ;; Redaction placeholder appears in argv element.\n (check-true (search-substring? line \"Bearer [redacted]\")))\n (delete-file tmp))\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10i. Launch-policy — per-axis enforcement status table\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy enforcement-status tests ---~n\")\n\n(import (only (jsh limits)\n policy-enforcement-status\n policy-enforcement-status-print\n audit-emit-enforcement-status!\n audit-emit-sandbox-install!\n policy-path-sandbox-available?))\n\n;; Empty policy: nothing requested → almost all axes 'na. shims are partial:\n;; dispatch shims are active, but same-uid child write-denial of the shim dir\n;; still needs a filesystem sandbox. Home defaults to ephemeral → 'enforced.\n(launch-policy-reset!)\n(let* ([s (policy-enforcement-status (current-launch-policy))]\n [get (lambda (k) (cdr (assq k s)))])\n (check (get 'read) => 'na)\n (check (get 'write) => 'na)\n (check (get 'exec) => 'na)\n (check (get 'net) => 'na)\n (check (get 'deny) => 'na)\n (check (get 'env) => 'na)\n (check (get 'secrets) => 'na)\n (check (get 'home) => 'enforced)\n (check (get 'cache) => 'na)\n (check (get 'limits) => 'na)\n (check (get 'shims) => 'partial)\n (check (get 'tracefs) => 'na)\n (check (get 'audit) => 'na)\n (check (get 'workspace) => 'na))\n\n;; exec axis: any allowlist entry → 'enforced (realpath-checked at fork time).\n(launch-policy-reset!)\n(launch-policy-add-exec! \"/bin/echo\")\n(check (cdr (assq 'exec (policy-enforcement-status (current-launch-policy))))\n => 'enforced)\n\n;; env + secrets: env builder filters non-allowlisted vars → 'enforced.\n;; secrets axis enforced (argv-leak detection lives in #14).\n(launch-policy-reset!)\n(launch-policy-add-env! \"PATH\")\n(launch-policy-add-secret! \"API_KEY=hunter2\")\n(let* ([s (policy-enforcement-status (current-launch-policy))]\n [get (lambda (k) (cdr (assq k s)))])\n (check (get 'env) => 'enforced)\n (check (get 'secrets) => 'enforced))\n\n;; read/write/net are enforced when the filesystem/network sandbox wrapper is\n;; available; otherwise they fail closed.\n(launch-policy-reset!)\n(launch-policy-add-read! \"/etc\")\n(launch-policy-add-write! \"/tmp/x\")\n(launch-policy-add-net! \"github.com:443\")\n(launch-policy-add-deny! \"/usr/bin/curl\")\n(let* ([s (policy-enforcement-status (current-launch-policy))]\n [get (lambda (k) (cdr (assq k s)))])\n (check (get 'read) => (if (policy-path-sandbox-available?)\n 'enforced\n 'fail-closed))\n (check (get 'write) => (if (policy-path-sandbox-available?)\n 'enforced\n 'fail-closed))\n (check (get 'net) => (if (policy-net-direct-sandbox-available?)\n 'enforced\n 'fail-closed))\n (check (get 'deny) => 'fail-closed))\n\n;; home opt-out via \"real\" / \"inherit\" → 'parsed (child sees real $HOME).\n(launch-policy-reset!)\n(launch-policy-set-home! \"real\")\n(check (cdr (assq 'home (policy-enforcement-status (current-launch-policy))))\n => 'parsed)\n(launch-policy-reset!)\n(launch-policy-set-home! \"inherit\")\n(check (cdr (assq 'home (policy-enforcement-status (current-launch-policy))))\n => 'parsed)\n;; home explicit ephemeral → enforced\n(launch-policy-reset!)\n(launch-policy-set-home! \"ephemeral\")\n(check (cdr (assq 'home (policy-enforcement-status (current-launch-policy))))\n => 'enforced)\n\n;; cache opt-in: \"ephemeral\" → 'enforced; \"real\"/\"inherit\" → 'parsed.\n(launch-policy-reset!)\n(launch-policy-set-cache! \"ephemeral\")\n(check (cdr (assq 'cache (policy-enforcement-status (current-launch-policy))))\n => 'enforced)\n(launch-policy-reset!)\n(launch-policy-set-cache! \"inherit\")\n(check (cdr (assq 'cache (policy-enforcement-status (current-launch-policy))))\n => 'parsed)\n\n;; limits: rlimit-backed kinds and parent-side time/out are enforced.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"cpu\" \"30\")\n(check (cdr (assq 'limits (policy-enforcement-status (current-launch-policy))))\n => 'enforced)\n(launch-policy-reset!)\n(launch-policy-add-limit! \"out\" \"1m\")\n(check (cdr (assq 'limits (policy-enforcement-status (current-launch-policy))))\n => 'enforced)\n\n;; tracefs: enforced when the trace wrapper backend is available; otherwise\n;; fail-closed.\n(launch-policy-reset!)\n(launch-policy-set-tracefs! \"summary\")\n(check (cdr (assq 'tracefs (policy-enforcement-status (current-launch-policy))))\n => (if (string=? (cdr (assoc \"status\" (tracefs-capabilities))) \"installed\")\n 'enforced\n 'fail-closed))\n(launch-policy-reset!)\n(launch-policy-set-tracefs! \"off\")\n(check (cdr (assq 'tracefs (policy-enforcement-status (current-launch-policy))))\n => 'na)\n\n;; audit: requires audit-out path → 'enforced.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-enf-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (check (cdr (assq 'audit (policy-enforcement-status (current-launch-policy))))\n => 'enforced)\n (when (file-exists? tmp) (delete-file tmp)))\n\n;; workspace: ephemeral → enforced (a temp cwd is provisioned).\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"ephemeral\")\n(check (cdr (assq 'workspace (policy-enforcement-status (current-launch-policy))))\n => 'enforced)\n\n;; audit-emit-enforcement-status!: writes a policy_enforcement record with\n;; per-axis fields nested under \"fields\".\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-enfaudit-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (launch-policy-add-exec! \"/bin/echo\")\n (audit-emit-enforcement-status! (current-launch-policy))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"policy_enforcement\\\"\"))\n (check-true (search-substring? line \"\\\"exec\\\":\\\"enforced\\\"\"))\n (check-true (search-substring? line \"\\\"read\\\":\\\"na\\\"\"))\n (check-true (search-substring? line \"\\\"shims\\\":\\\"partial\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n\n;; audit-emit-sandbox-install!: records unavailable required sandbox axes and\n;; degraded optional hardening axes.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-sandbox-install-audit-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (launch-policy-add-read! \"/etc\")\n (launch-policy-add-net! \"api.example.com:443\")\n (audit-emit-sandbox-install! (current-launch-policy))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"sandbox_install\\\"\"))\n (check-true (search-substring?\n line\n (if (policy-path-sandbox-available?)\n \"\\\"status\\\":\\\"installed\\\"\"\n \"\\\"status\\\":\\\"unavailable\\\"\")))\n (check-true (search-substring? line \"\\\"axis\\\":\\\"read\\\"\"))\n (check-true (search-substring? line \"\\\"axis\\\":\\\"net-direct-denial\\\"\"))\n (check-true (search-substring? line \"\\\"axis\\\":\\\"net-proxy\\\"\"))\n (check-true (search-substring? line \"\\\"axis\\\":\\\"shim-dir-write-denial\\\"\"))\n (check-true (search-substring? line \"\\\"required\\\":true\"))\n (check-true (search-substring? line \"\\\"required\\\":false\")))\n (when (file-exists? tmp) (delete-file tmp)))\n\n;; policy-enforcement-status-print emits human-readable lines.\n(launch-policy-reset!)\n(launch-policy-add-exec! \"/bin/echo\")\n(let* ([sp (open-output-string)])\n (policy-enforcement-status-print (current-launch-policy) sp)\n (let ([out (get-output-string sp)])\n (check-true (search-substring? out \"exec\"))\n (check-true (search-substring? out \"enforced\"))\n (check-true (search-substring? out \"shims\"))))\n(launch-policy-reset!)\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10l. Launch-policy — transactional workspace\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy workspace tests ---~n\")\n\n(import (only (jsh limits)\n policy-resolve-workspace\n policy-prepare-workspace!\n policy-workspace-cwd\n policy-finalize-workspace!\n policy-workspace-last-transaction\n policy-workspace-reject!\n audit-emit-workspace-resolved!))\n\n;; parse-workspace-mode accepts ephemeral / transaction / overlay / off.\n(check (parse-workspace-mode \"off\") => \"off\")\n(check (parse-workspace-mode \"ephemeral\") => \"ephemeral\")\n(check (parse-workspace-mode \"transaction\") => \"transaction\")\n(check (parse-workspace-mode \"overlay\") => \"overlay\")\n(check (parse-workspace-mode \"bogus\") => #f)\n\n;; Unset → resolve to #f.\n(launch-policy-reset!)\n(check (policy-resolve-workspace (current-launch-policy)) => #f)\n\n;; \"off\" → #f (no override).\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"off\")\n(check (policy-resolve-workspace (current-launch-policy)) => #f)\n\n;; \"ephemeral\" → fresh temp dir.\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"ephemeral\")\n(let ([p (policy-resolve-workspace (current-launch-policy))])\n (check-true (and p (file-exists? p)))\n (policy-cleanup-temp-dirs!))\n\n;; \"overlay\" → #f (needs root; jsh can't provision).\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"overlay\")\n(check (policy-resolve-workspace (current-launch-policy)) => #f)\n\n;; policy-workspace-cwd aliases resolve.\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"ephemeral\")\n(let ([p (policy-workspace-cwd (current-launch-policy))])\n (check-true (and p (file-exists? p)))\n (policy-cleanup-temp-dirs!))\n\n;; Per-axis status maps each mode correctly.\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"ephemeral\")\n(check (cdr (assq 'workspace (policy-enforcement-status (current-launch-policy))))\n => 'enforced)\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"transaction\")\n(check (cdr (assq 'workspace (policy-enforcement-status (current-launch-policy))))\n => (if (policy-path-sandbox-available?) 'enforced 'partial))\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"overlay\")\n(check (cdr (assq 'workspace (policy-enforcement-status (current-launch-policy))))\n => 'fail-closed)\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"off\")\n(check (cdr (assq 'workspace (policy-enforcement-status (current-launch-policy))))\n => 'na)\n\n;; audit-emit-workspace-resolved! writes a workspace_resolved record with\n;; the chosen mode + status.\n(launch-policy-reset!)\n(launch-policy-set-workspace! \"ephemeral\")\n(let ([tmp \"/tmp/jsh-workspace-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-workspace-resolved! (current-launch-policy))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"workspace_resolved\\\"\"))\n (check-true (search-substring? line \"\\\"mode\\\":\\\"ephemeral\\\"\"))\n (check-true (search-substring? line \"\\\"status\\\":\\\"enforced\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(policy-cleanup-temp-dirs!)\n(launch-policy-reset!)\n\n;; transaction mode snapshots cwd, records a pending diff after rc=0, and\n;; reject deletes the preserved snapshot.\n(let* ([root \"/tmp/jsh-workspace-src-test\"]\n [file (string-append root \"/a.txt\")])\n (system (string-append \"rm -rf \" root))\n (system (string-append \"mkdir -p \" root))\n (with-output-to-file file (lambda () (display \"old\")) 'truncate)\n (launch-policy-reset!)\n (launch-policy-set-workspace! \"transaction\")\n (let ([old-cwd (current-directory)])\n (dynamic-wind\n (lambda () (current-directory root))\n (lambda ()\n (let ([snapshot (policy-prepare-workspace! (current-launch-policy))])\n (check-true (and snapshot\n (file-exists? (string-append snapshot \"/a.txt\"))))\n (with-output-to-file (string-append snapshot \"/a.txt\")\n (lambda () (display \"new\"))\n 'truncate)\n (policy-finalize-workspace! (current-launch-policy) 0)\n (let ([tx (policy-workspace-last-transaction)])\n (check (cdr (assq 'status tx)) => \"pending\")\n (check (cdr (assq 'changed tx)) => \"changed\")\n (let ([r (policy-workspace-reject! (current-launch-policy))])\n (check (cdr (assq 'ok r)) => #t)\n (check-true (not (file-exists? snapshot)))))))\n (lambda () (current-directory old-cwd))))\n (system (string-append \"rm -rf \" root)))\n(policy-cleanup-temp-dirs!)\n(launch-policy-reset!)\n\n;; policy reset abandons any pending transaction snapshot.\n(let* ([root \"/tmp/jsh-workspace-reset-test\"]\n [file (string-append root \"/a.txt\")])\n (system (string-append \"rm -rf \" root))\n (system (string-append \"mkdir -p \" root))\n (with-output-to-file file (lambda () (display \"old\")) 'truncate)\n (launch-policy-reset!)\n (launch-policy-set-workspace! \"transaction\")\n (let ([old-cwd (current-directory)])\n (dynamic-wind\n (lambda () (current-directory root))\n (lambda ()\n (let ([snapshot (policy-prepare-workspace! (current-launch-policy))])\n (policy-finalize-workspace! (current-launch-policy) 0)\n (launch-policy-reset!)\n (check (policy-workspace-last-transaction) => #f)\n (check-true (not (file-exists? snapshot)))))\n (lambda () (current-directory old-cwd))))\n (system (string-append \"rm -rf \" root)))\n(policy-cleanup-temp-dirs!)\n(launch-policy-reset!)\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10k. Launch-policy — tracefs (mirror std/os/tracefs)\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy tracefs tests ---~n\")\n\n(import (only (jsh limits)\n tracefs-capabilities\n tracefs-wrap-command\n tracefs-normalize-events\n tracefs-policy-suggestions\n audit-emit-tracefs-status!\n audit-emit-tracefs-events!\n audit-emit-tracefs-suggestions!))\n\n(import (only (std os tracefs)\n tracefs-parse-strace))\n\n;; tracefs-capabilities returns an alist with backend / status / platform /\n;; events / caveats. On macOS the backend is \"none\" / \"unavailable\"; on\n;; Linux it depends on whether strace is on PATH.\n(let* ([caps (tracefs-capabilities)]\n [get (lambda (k) (cdr (assoc k caps)))])\n (check-true (member (get \"backend\") '(\"strace\" \"none\")))\n (check-true (member (get \"status\") '(\"installed\" \"unavailable\")))\n (check-true (member (get \"platform\") '(\"macos\" \"linux\" \"unknown\")))\n (check-true (list? (get \"events\")))\n (check-true (list? (get \"caveats\"))))\n\n;; tracefs-wrap-command: on macOS backend is unavailable. Without\n;; fail-closed?: returns the bare argv with status \"untraced\".\n(let-values ([(cmd st) (tracefs-wrap-command '(\"/bin/echo\" \"hi\")\n \"/tmp/trace.txt\")])\n ;; Either traced (linux + strace) or untraced (macOS / no strace).\n (check-true (member st '(\"traced\" \"untraced\")))\n (cond\n [(string=? st \"traced\")\n (check-true (list? cmd))\n (check-true (search-substring? (car cmd) \"strace\"))\n (check-true (member \"/tmp/trace.txt\" cmd))]\n [else\n (check cmd => '(\"/bin/echo\" \"hi\"))]))\n\n;; fail-closed?: when backend unavailable, returns #f + \"refused\".\n;; On macOS this is the expected path.\n(let-values ([(cmd st) (tracefs-wrap-command '(\"/bin/echo\" \"hi\")\n \"/tmp/trace.txt\" #t)])\n (cond\n [(string=? st \"traced\")\n ;; Linux build with strace installed — still wraps.\n (check-true (list? cmd))]\n [else\n (check cmd => #f)\n (check st => \"refused\")]))\n\n;; audit-emit-tracefs-status! writes a tracefs_status record when policy\n;; sets ,tracefs, and is silent when ,tracefs is unset.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-tracefs-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n ;; No tracefs set → no record.\n (audit-emit-tracefs-status! (current-launch-policy))\n (check-true (or (not (file-exists? tmp)) (zero? (file-size tmp))))\n ;; tracefs set → record emitted.\n (launch-policy-set-tracefs! \"summary\")\n (audit-emit-tracefs-status! (current-launch-policy))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"tracefs_status\\\"\"))\n (check-true (search-substring? line \"\\\"mode\\\":\\\"summary\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; Normalized tracefs events are emitted as fs_event audit records, and\n;; suggestions are grouped into read/write/exec paths for callers.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-tracefs-events-test.jsonl\"])\n (when (file-exists? tmp) (delete-file tmp))\n (launch-policy-set-audit-out! tmp)\n (launch-policy-set-tracefs! \"summary\")\n (let* ([raw (tracefs-parse-strace\n (open-input-string\n (string-append\n \"123 openat(AT_FDCWD, \\\"/repo/package.json\\\", O_RDONLY|O_CLOEXEC) = 3\\n\"\n \"123 openat(AT_FDCWD, \\\"/repo/out.txt\\\", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4\\n\"\n \"123 execve(\\\"/usr/bin/node\\\", [\\\"node\\\"], 0x7ffe) = 0\\n\")))]\n [events (tracefs-normalize-events raw)]\n [suggestions (tracefs-policy-suggestions events)])\n (audit-emit-tracefs-events! (current-launch-policy) events)\n (audit-emit-tracefs-suggestions! (current-launch-policy) suggestions)\n (let ([content (slurp-file tmp)])\n (check-true (search-substring? content \"\\\"type\\\":\\\"fs_event\\\"\"))\n (check-true (search-substring? content \"\\\"op\\\":\\\"read\\\"\"))\n (check-true (search-substring? content \"\\\"op\\\":\\\"create\\\"\"))\n (check-true (search-substring? content \"\\\"op\\\":\\\"write\\\"\"))\n (check-true (search-substring? content \"\\\"op\\\":\\\"exec\\\"\"))\n (check-true (search-substring? content \"\\\"result\\\":\\\"ok\\\"\"))\n (check-true (search-substring? content \"\\\"type\\\":\\\"tracefs_suggestions\\\"\"))\n (check-true (search-substring? content \"\\\"read\\\":[\\\"/repo/package.json\\\"]\"))\n (check-true (search-substring? content \"\\\"write\\\":[\\\"/repo/out.txt\\\"]\"))\n (check-true (search-substring? content \"\\\"exec\\\":[\\\"/usr/bin/node\\\"]\"))))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10i++. Launch-policy — limit syntax (std/os/limits-backed)\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy limit-syntax tests ---~n\")\n\n(import (only (jsh limits)\n parse-bytes parse-cpu-seconds parse-pids\n parse-limit-resolved policy-resolved-limits\n audit-emit-limit-install!))\n\n;; parse-bytes: SI-style binary suffixes (k/m/g/t case-insensitive).\n(check (parse-bytes \"1024\") => 1024)\n(check (parse-bytes \"1k\") => 1024)\n(check (parse-bytes \"1K\") => 1024)\n(check (parse-bytes \"512m\") => (* 512 1024 1024))\n(check (parse-bytes \"2g\") => (* 2 1024 1024 1024))\n(check (parse-bytes \"1t\") => (* 1024 1024 1024 1024))\n;; Rejection: empty, non-numeric, bad suffix, zero, negative.\n(check (parse-bytes \"\") => #f)\n(check (parse-bytes \"0\") => #f)\n(check (parse-bytes \"-1m\") => #f)\n(check (parse-bytes \"abc\") => #f)\n(check (parse-bytes \"2x\") => #f)\n(check (parse-bytes \"1.5g\") => #f) ;; integer-only\n\n;; parse-cpu-seconds: bare integer or s/m/h suffix.\n(check (parse-cpu-seconds \"30\") => 30)\n(check (parse-cpu-seconds \"30s\") => 30)\n(check (parse-cpu-seconds \"5m\") => (* 5 60))\n(check (parse-cpu-seconds \"1h\") => 3600)\n(check (parse-cpu-seconds \"\") => #f)\n(check (parse-cpu-seconds \"abc\") => #f)\n(check (parse-cpu-seconds \"0\") => #f)\n\n;; parse-pids: positive integer.\n(check (parse-pids \"16\") => 16)\n(check (parse-pids \"1\") => 1)\n(check (parse-pids \"0\") => #f)\n(check (parse-pids \"-1\") => #f)\n(check (parse-pids \"abc\") => #f)\n\n;; parse-limit-resolved by kind.\n(check (parse-limit-resolved \"mem\" \"30%\") => 0.3)\n(check (parse-limit-resolved \"mem\" \"2g\") => (* 2 1024 1024 1024))\n(check (parse-limit-resolved \"cpu\" \"30\") => 30)\n(check (parse-limit-resolved \"cpu\" \"30s\") => 30)\n(check (parse-limit-resolved \"pids\" \"16\") => 16)\n(check (parse-limit-resolved \"fsize\" \"1m\") => (* 1024 1024))\n(check (parse-limit-resolved \"out\" \"64k\") => (* 64 1024))\n(check (parse-limit-resolved \"time\" \"30s\") => 30000)\n;; Unknown kind → #f\n(check (parse-limit-resolved \"bogus\" \"1\") => #f)\n\n;; parse-limit-value rejects shapes that don't resolve.\n(check (parse-limit-value \"mem\" \"bogus\") => #f)\n(check (parse-limit-value \"fsize\" \"100x\") => #f)\n(check (parse-limit-value \"fsize\" \"1m\") => \"1m\")\n(check (parse-limit-value \"cpu\" \"30\") => \"30\")\n(check (parse-limit-value \"pids\" \"16\") => \"16\")\n;; Existing tests still pass.\n(check (parse-limit-value \"mem\" \"2g\") => \"2g\")\n(check (parse-limit-value \"time\" \"30m\") => \"30m\")\n\n;; policy-resolved-limits returns the canonical numeric form.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"fsize\" \"1m\")\n(launch-policy-add-limit! \"cpu\" \"30\")\n(launch-policy-add-limit! \"out\" \"64k\")\n(let ([rs (policy-resolved-limits (current-launch-policy))])\n (check (cdr (assoc \"fsize\" rs)) => (* 1024 1024))\n (check (cdr (assoc \"cpu\" rs)) => 30)\n (check (cdr (assoc \"out\" rs)) => (* 64 1024)))\n(launch-policy-reset!)\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10i+. Launch-policy — limit-plan (mirror std/os/limits)\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy limit-plan tests ---~n\")\n\n(import (only (jsh limits)\n policy-limit-plan\n audit-emit-limit-plan!))\n\n;; Empty policy → empty plan.\n(launch-policy-reset!)\n(check (policy-limit-plan (current-launch-policy)) => '())\n\n;; mem is installed in the target child pre-exec path where the platform has a\n;; useful address-space rlimit. On macOS it remains unavailable/fail-closed.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"mem\" \"30%\")\n(let ([plan (policy-limit-plan (current-launch-policy))])\n (check (length plan) => 1)\n (check (caar plan) => \"mem\")\n (check (cdar plan) => (if (test-platform-macos?)\n 'unavailable\n 'attempt-installed)))\n\n;; cpu and fsize are installed in the target pre-exec path\n;; so it cannot break shim/audit setup in the launcher.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"cpu\" \"30\")\n(launch-policy-add-limit! \"fsize\" \"1m\")\n(let ([plan (policy-limit-plan (current-launch-policy))])\n (check (cdr (assoc \"cpu\" plan)) => 'attempt-installed)\n (check (cdr (assoc \"fsize\" plan)) => 'attempt-installed))\n\n;; time/out are parent-side supervision.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"time\" \"30s\")\n(launch-policy-add-limit! \"out\" \"1m\")\n(let ([plan (policy-limit-plan (current-launch-policy))])\n (check (cdr (assoc \"time\" plan)) => 'parent)\n (check (cdr (assoc \"out\" plan)) => 'parent))\n\n;; pids is parent-side process-tree supervision on Linux; otherwise it is\n;; installed in the target pre-exec path.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"pids\" \"16\")\n(let ([plan (policy-limit-plan (current-launch-policy))])\n (check (cdr (assoc \"pids\" plan))\n => (if (test-platform-linux?) 'parent 'attempt-installed)))\n\n;; audit-emit-limit-plan! writes a \"limit_plan\" record when limits exist.\n(launch-policy-reset!)\n(launch-policy-add-limit! \"cpu\" \"30\")\n(let ([tmp \"/tmp/jsh-limplan-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-limit-plan! (current-launch-policy))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"limit_plan\\\"\"))\n (check-true (search-substring? line \"\\\"cpu\\\":\\\"attempt-installed\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n\n;; audit-emit-limit-install! records the child-side install report.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-liminstall-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-limit-install! (current-launch-policy)\n '((\"cpu\" . installed)))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"limit_install\\\"\"))\n (check-true (search-substring? line \"\\\"cpu\\\":\\\"installed\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n\n;; No emit when plan is empty (avoid empty audit noise).\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-limplan-empty.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-limit-plan! (current-launch-policy))\n (check-true (or (not (file-exists? tmp))\n (zero? (file-size tmp))))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; ──────────────────────────────────────────────────────────────\n;;; 10j. Launch-policy — net allowlist predicates (std/net/allowlist-backed)\n;;; ──────────────────────────────────────────────────────────────\n(printf \"--- Launch policy net allowlist tests ---~n\")\n\n(import (only (jsh limits)\n host-is-ipv4-literal?\n host-is-ipv6-literal?\n host-is-localnet?\n policy-net-denial-reason\n policy-net-host-allowed?\n policy-net-connect-decision\n audit-emit-net-denied!))\n\n;; IPv4 literal detection.\n(check-true (host-is-ipv4-literal? \"1.2.3.4\"))\n(check-true (host-is-ipv4-literal? \"127.0.0.1\"))\n(check-true (host-is-ipv4-literal? \"0.0.0.0\"))\n(check-true (not (host-is-ipv4-literal? \"example.com\")))\n(check-true (not (host-is-ipv4-literal? \"256.1.1.1\")))\n(check-true (not (host-is-ipv4-literal? \"1.2.3\")))\n\n;; IPv6 literal detection.\n(check-true (host-is-ipv6-literal? \"::1\"))\n(check-true (host-is-ipv6-literal? \"fe80::1\"))\n(check-true (host-is-ipv6-literal? \"2001:db8::1\"))\n(check-true (host-is-ipv6-literal? \"[::1]\"))\n(check-true (not (host-is-ipv6-literal? \"example.com\")))\n(check-true (not (host-is-ipv6-literal? \"1.2.3.4\")))\n\n;; Localnet categorization.\n(check (host-is-localnet? \"127.0.0.1\") => 'loopback4)\n(check (host-is-localnet? \"10.0.0.5\") => 'rfc1918-10)\n(check (host-is-localnet? \"172.16.0.1\") => 'rfc1918-172)\n(check (host-is-localnet? \"172.31.255.255\") => 'rfc1918-172)\n(check (host-is-localnet? \"192.168.1.1\") => 'rfc1918-192)\n(check (host-is-localnet? \"169.254.169.254\") => 'link-local4)\n(check (host-is-localnet? \"100.64.0.1\") => 'cgnat)\n(check (host-is-localnet? \"224.0.0.1\") => 'multicast4)\n(check (host-is-localnet? \"::1\") => 'loopback6)\n(check (host-is-localnet? \"fe80::1\") => 'link-local6)\n(check (host-is-localnet? \"fc00::1\") => 'rfc1918-6)\n(check (host-is-localnet? \"ff02::1\") => 'multicast6)\n(check (host-is-localnet? \"localhost\") => 'loopback4)\n(check (host-is-localnet? \"example.com\") => #f)\n(check (host-is-localnet? \"8.8.8.8\") => #f)\n\n;; Denial reason: IP literal and localnet both block.\n(check (policy-net-denial-reason \"1.2.3.4\") => 'ip-literal)\n(check (policy-net-denial-reason \"127.0.0.1\") => 'ip-literal) ;; IP wins\n(check (policy-net-denial-reason \"localhost\") => 'loopback4)\n(check (policy-net-denial-reason \"169.254.169.254\") => 'ip-literal)\n(check (policy-net-denial-reason \"example.com\") => #f)\n\n;; Wildcard allowlist still denies IP literals and localnet.\n(launch-policy-reset!)\n(launch-policy-add-net! \"*:443\")\n(check (policy-net-host-allowed? (current-launch-policy) \"1.2.3.4\" 443)\n => #f)\n(check (policy-net-host-allowed? (current-launch-policy) \"169.254.169.254\" 443)\n => #f)\n(check (policy-net-host-allowed? (current-launch-policy) \"localhost\" 443)\n => #f)\n;; Hostnames still pass through the wildcard.\n(check-true\n (and (policy-net-host-allowed? (current-launch-policy) \"example.com\" 443) #t))\n\n;; Glob matching with **. and *.\n(launch-policy-reset!)\n(launch-policy-add-net! \"**.googleapis.com:443\")\n(launch-policy-add-net! \"*.example.com:443\")\n(check-true\n (and (policy-net-host-allowed? (current-launch-policy) \"api.googleapis.com\" 443) #t))\n(check-true\n (and (policy-net-host-allowed? (current-launch-policy) \"a.b.googleapis.com\" 443) #t))\n;; *.example.com matches one label only.\n(check-true\n (and (policy-net-host-allowed? (current-launch-policy) \"api.example.com\" 443) #t))\n(check (policy-net-host-allowed? (current-launch-policy) \"a.b.example.com\" 443)\n => #f)\n;; Port mismatch denies.\n(check (policy-net-host-allowed? (current-launch-policy) \"api.example.com\" 80)\n => #f)\n\n;; Empty net allowlist → deny everything.\n(launch-policy-reset!)\n(check (policy-net-host-allowed? (current-launch-policy) \"example.com\" 443)\n => #f)\n\n;; Full CONNECT decision uses Jerboa's reusable proxy DNS/recheck path.\n(launch-policy-reset!)\n(launch-policy-add-net! \"localhost:443\")\n(let ([d (policy-net-connect-decision (current-launch-policy) \"localhost\" 443)])\n (check (cdr (assq 'allowed d)) => #f)\n (check (cdr (assq 'reason d)) => 'loopback4)\n (check (cdr (assq 'resolved-host d)) => \"127.0.0.1\"))\n\n;; audit-emit-net-denied! writes a \"net_denied\" record with host/port/reason.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-netdeny-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-net-denied! (current-launch-policy) \"169.254.169.254\" 443 'ip-literal)\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"net_denied\\\"\"))\n (check-true (search-substring? line \"\\\"host\\\":\\\"169.254.169.254\\\"\"))\n (check-true (search-substring? line \"\\\"reason\\\":\\\"ip-literal\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n(printf \"--- Launch policy audit-depth tests ---~n\")\n\n(import (only (jsh limits)\n audit-emit-net-allowed!\n audit-emit-process-start!\n audit-emit-process-exit!\n audit-emit-process-fork!\n audit-emit-fs-event!\n audit-emit-net-violation!\n audit-emit-net-proxy-event!))\n\n;; audit-emit-net-allowed! writes a \"net_allowed\" record with host/port/match.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-netallow-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-net-allowed! (current-launch-policy) \"api.example.com\" 443 \"*.example.com:443\")\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"net_allowed\\\"\"))\n (check-true (search-substring? line \"\\\"host\\\":\\\"api.example.com\\\"\"))\n (check-true (search-substring? line \"\\\"port\\\":\\\"443\\\"\"))\n (check-true (search-substring? line \"\\\"match\\\":\\\"*.example.com:443\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; audit-emit-net-allowed! handles pair patterns (host . port).\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-netallow2-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-net-allowed! (current-launch-policy) \"h\" \"80\" (cons \"h\" 80))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"match\\\":\\\"h:80\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; audit-emit-process-start! writes a \"process_start\" record with pid/exec/argv.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-procstart-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-process-start! (current-launch-policy) 4242 \"/bin/true\" '(\"/bin/true\" \"-x\"))\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"process_start\\\"\"))\n (check-true (search-substring? line \"\\\"pid\\\":4242\"))\n (check-true (search-substring? line \"\\\"exec\\\":\\\"/bin/true\\\"\"))\n (check-true (search-substring? line \"\\\"argv\\\":[\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; audit-emit-process-exit! writes a \"process_exit\" record with pid/rc.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-procexit-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-process-exit! (current-launch-policy) 4242 137)\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"process_exit\\\"\"))\n (check-true (search-substring? line \"\\\"pid\\\":4242\"))\n (check-true (search-substring? line \"\\\"rc\\\":137\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; audit-emit-process-fork! writes a \"process_fork\" record with ppid/pid.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-procfork-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-process-fork! (current-launch-policy) 100 200)\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"process_fork\\\"\"))\n (check-true (search-substring? line \"\\\"ppid\\\":100\"))\n (check-true (search-substring? line \"\\\"pid\\\":200\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; audit-emit-fs-event! writes \"fs_event\" with op/path; accepts symbol or string op.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-fsev-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-fs-event! (current-launch-policy) 'open \"/etc/passwd\")\n (audit-emit-fs-event! (current-launch-policy) \"write\" \"/tmp/out\")\n (let* ([port (open-input-file tmp)]\n [l1 (get-line port)]\n [l2 (get-line port)])\n (close-port port)\n (check-true (search-substring? l1 \"\\\"type\\\":\\\"fs_event\\\"\"))\n (check-true (search-substring? l1 \"\\\"op\\\":\\\"open\\\"\"))\n (check-true (search-substring? l1 \"\\\"path\\\":\\\"/etc/passwd\\\"\"))\n (check-true (search-substring? l2 \"\\\"op\\\":\\\"write\\\"\"))\n (check-true (search-substring? l2 \"\\\"path\\\":\\\"/tmp/out\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; audit-emit-net-violation! writes \"net_violation\" with reason normalised.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-netvio-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-net-violation! (current-launch-policy) \"evil.example\" 1337 'sandbox-block)\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"net_violation\\\"\"))\n (check-true (search-substring? line \"\\\"host\\\":\\\"evil.example\\\"\"))\n (check-true (search-substring? line \"\\\"port\\\":\\\"1337\\\"\"))\n (check-true (search-substring? line \"\\\"reason\\\":\\\"sandbox-block\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; Jerboa proxy logger events are bridged into jsh net audit records.\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-netproxy-event-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-net-proxy-event!\n (current-launch-policy)\n '((kind . allow)\n (host . \"api.example.com\")\n (port . 443)\n (pattern . \"*.example.com:443\")))\n (audit-emit-net-proxy-event!\n (current-launch-policy)\n '((kind . deny)\n (host . \"bad.example\")\n (port . 443)\n (reason . not-in-allowlist)))\n (let* ([port (open-input-file tmp)]\n [l1 (get-line port)]\n [l2 (get-line port)]\n [l3 (get-line port)])\n (close-port port)\n (check-true (search-substring? l1 \"\\\"type\\\":\\\"net_allowed\\\"\"))\n (check-true (search-substring? l1 \"\\\"match\\\":\\\"*.example.com:443\\\"\"))\n (check-true (search-substring? l2 \"\\\"type\\\":\\\"net_denied\\\"\"))\n (check-true (search-substring? l2 \"\\\"reason\\\":\\\"not-in-allowlist\\\"\"))\n (check-true (search-substring? l3 \"\\\"type\\\":\\\"net_violation\\\"\"))\n (check-true (search-substring? l3 \"\\\"host\\\":\\\"bad.example\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n(printf \"--- Launch policy shim hardening + package-policy tests ---~n\")\n\n(import (only (jsh limits)\n *default-package-policy*\n launch-policy-set-package-policy!\n launch-policy-package-policy-get\n parse-package-policy-flag\n policy-package-extra-args\n sh-single-quote\n audit-emit-shim-written!\n launch-policy-add!))\n\n;; sh-single-quote: empty string becomes ''\n(check (sh-single-quote \"\") => \"''\")\n(check (sh-single-quote #f) => \"''\")\n;; sh-single-quote: simple string is wrapped in '...'\n(check (sh-single-quote \"hello\") => \"'hello'\")\n(check (sh-single-quote \"/usr/local/bin/node\") => \"'/usr/local/bin/node'\")\n;; sh-single-quote: embedded single-quote becomes '\\''\n(check (sh-single-quote \"it's\") => \"'it'\\\\''s'\")\n(check (sh-single-quote \"a'b'c\") => \"'a'\\\\''b'\\\\''c'\")\n;; sh-single-quote: spaces, dollar signs, backticks pass through unescaped\n(check (sh-single-quote \"hello world\") => \"'hello world'\")\n(check (sh-single-quote \"$PATH; rm -rf /\") => \"'$PATH; rm -rf /'\")\n(check (sh-single-quote \"`whoami`\") => \"'`whoami`'\")\n\n;; *default-package-policy* is an alist with the documented keys\n(check-true (pair? (assq 'install-scripts *default-package-policy*)))\n(check-true (pair? (assq 'lockfile-required *default-package-policy*)))\n(check-true (pair? (assq 'registry-pinned *default-package-policy*)))\n(check-true (pair? (assq 'min-package-age *default-package-policy*)))\n(check-true (pair? (assq 'ad-hoc-allowed *default-package-policy*)))\n\n;; launch-policy-package-policy-get returns defaults when unset\n(launch-policy-reset!)\n(check (launch-policy-package-policy-get 'install-scripts) => #f)\n(check (launch-policy-package-policy-get 'lockfile-required) => #f)\n(check (launch-policy-package-policy-get 'ad-hoc-allowed) => #f)\n;; ...and the unknown key returns #f\n(check (launch-policy-package-policy-get 'no-such-key) => #f)\n\n;; launch-policy-set-package-policy! overrides defaults; second set replaces\n(launch-policy-reset!)\n(launch-policy-set-package-policy! 'install-scripts #t)\n(check (launch-policy-package-policy-get 'install-scripts) => #t)\n(launch-policy-set-package-policy! 'install-scripts #f)\n(check (launch-policy-package-policy-get 'install-scripts) => #f)\n(launch-policy-set-package-policy! 'registry-pinned \"https://registry.internal/\")\n(check (launch-policy-package-policy-get 'registry-pinned) => \"https://registry.internal/\")\n(launch-policy-reset!)\n\n;; parse-package-policy-flag: KEY=true/false → symbol + boolean\n(let-values ([(k v) (parse-package-policy-flag \"install-scripts=true\")])\n (check k => 'install-scripts)\n (check v => #t))\n(let-values ([(k v) (parse-package-policy-flag \"install-scripts=false\")])\n (check k => 'install-scripts)\n (check v => #f))\n(let-values ([(k v) (parse-package-policy-flag \"ad-hoc-allowed=yes\")])\n (check k => 'ad-hoc-allowed)\n (check v => #t))\n(let-values ([(k v) (parse-package-policy-flag \"ad-hoc-allowed=no\")])\n (check v => #f))\n;; non-boolean values come through as strings\n(let-values ([(k v) (parse-package-policy-flag \"registry-pinned=https://x/\")])\n (check k => 'registry-pinned)\n (check v => \"https://x/\"))\n;; missing equals sign → (#f, #f)\n(let-values ([(k v) (parse-package-policy-flag \"no-eq\")])\n (check k => #f)\n (check v => #f))\n\n;; launch-policy-add! routes 'package-policy\n(launch-policy-reset!)\n(check (launch-policy-add! 'package-policy \"install-scripts=true\") => #t)\n(check (launch-policy-package-policy-get 'install-scripts) => #t)\n(check (launch-policy-add! 'package-policy \"bogus-no-eq\") => #f)\n(launch-policy-reset!)\n\n;; policy-package-extra-args: default keeps --ignore-scripts for npm-family\n(launch-policy-reset!)\n(check (policy-package-extra-args \"npm\" '(pass \"--ignore-scripts\"))\n => '(\"--ignore-scripts\"))\n(check (policy-package-extra-args \"yarn\" '(pass \"--ignore-scripts\"))\n => '(\"--ignore-scripts\"))\n(check (policy-package-extra-args \"pnpm\" '(pass \"--ignore-scripts\"))\n => '(\"--ignore-scripts\"))\n;; ...and unrelated tools are unaffected\n(check (policy-package-extra-args \"node\" '(pass)) => '())\n(check (policy-package-extra-args \"cargo\" '(pass \"--frozen\")) => '(\"--frozen\"))\n;; install-scripts=#t drops --ignore-scripts (opt-in)\n(launch-policy-set-package-policy! 'install-scripts #t)\n(check (policy-package-extra-args \"npm\" '(pass \"--ignore-scripts\")) => '())\n(check (policy-package-extra-args \"yarn\" '(pass \"--ignore-scripts\")) => '())\n;; ...but unrelated tools are still untouched\n(check (policy-package-extra-args \"cargo\" '(pass \"--frozen\")) => '(\"--frozen\"))\n(launch-policy-reset!)\n\n;; audit-emit-shim-written! writes a \"shim_written\" record\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-shim-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-shim-written! (current-launch-policy) \"npm\" \"/tmp/x/shims/npm\" 'pass \"/usr/local/bin/npm\")\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"type\\\":\\\"shim_written\\\"\"))\n (check-true (search-substring? line \"\\\"name\\\":\\\"npm\\\"\"))\n (check-true (search-substring? line \"\\\"mode\\\":\\\"pass\\\"\"))\n (check-true (search-substring? line \"\\\"target\\\":\\\"/usr/local/bin/npm\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; audit-emit-shim-written! handles deny shims (target = \"\" allowed)\n(launch-policy-reset!)\n(let ([tmp \"/tmp/jsh-shim2-test.jsonl\"])\n (launch-policy-set-audit-out! tmp)\n (audit-emit-shim-written! (current-launch-policy) \"npx\" \"/tmp/x/shims/npx\" 'deny #f)\n (let* ([port (open-input-file tmp)]\n [line (get-line port)])\n (close-port port)\n (check-true (search-substring? line \"\\\"name\\\":\\\"npx\\\"\"))\n (check-true (search-substring? line \"\\\"mode\\\":\\\"deny\\\"\"))\n (check-true (search-substring? line \"\\\"target\\\":\\\"\\\"\")))\n (when (file-exists? tmp) (delete-file tmp)))\n(launch-policy-reset!)\n\n;; *default-shim-tools* now covers pip/cargo/go/gem/bundle as well as the\n;; Node family.\n(import (only (jsh limits) *default-shim-tools*))\n(define (shim-tool-names) (map car *default-shim-tools*))\n(check-true (member \"node\" (shim-tool-names)))\n(check-true (member \"npm\" (shim-tool-names)))\n(check-true (member \"pip\" (shim-tool-names)))\n(check-true (member \"pip3\" (shim-tool-names)))\n(check-true (member \"cargo\" (shim-tool-names)))\n(check-true (member \"go\" (shim-tool-names)))\n(check-true (member \"gem\" (shim-tool-names)))\n(check-true (member \"bundle\" (shim-tool-names)))\n(check-true (member \"npx\" (shim-tool-names)))\n\n;; ──────────────────────────────────────────────────────────────\n;;; 11. Property-based tests (QuickCheck)\n;;; ──────────────────────────────────────────────────────────────\n;; Helper: check all elements satisfy predicate\n(define (every-in-list? lst pred)\n (cond [(null? lst) #t]\n [(pred (car lst)) (every-in-list? (cdr lst) pred)]\n [else #f]))\n\n(printf \"--- QuickCheck property tests ---~n\")\n\n(import (only (std test quickcheck)\n check-property for-all gen-int gen-nat gen-string gen-choose))\n\n;; Helper: check a property result\n(define-syntax check-property-pass\n (syntax-rules ()\n [(_ desc prop-result)\n (let ([r prop-result])\n (if (eq? (cdr (assq 'status r)) 'pass)\n (set! pass-count (+ 1 pass-count))\n (begin\n (set! fail-count (+ 1 fail-count))\n (printf \"FAIL (property): ~a~n result: ~s~n\" desc r))))]))\n\n;; Arithmetic identity: a + 0 = a\n(let* ([env (make-shell-environment)]\n [get-fn (arith-env-getter env)]\n [set-fn (arith-env-setter env)])\n (check-property-pass \"a + 0 = a\"\n (for-all ([a (gen-choose -1000 1000)])\n (= (arith-eval (string-append (number->string a) \"+0\") get-fn set-fn) a)))\n\n ;; Arithmetic identity: a * 1 = a\n (check-property-pass \"a * 1 = a\"\n (for-all ([a (gen-choose -1000 1000)])\n (= (arith-eval (string-append (number->string a) \"*1\") get-fn set-fn) a)))\n\n ;; Arithmetic identity: a - a = 0\n (check-property-pass \"a - a = 0\"\n (for-all ([a (gen-choose 0 1000)])\n (= (arith-eval (string-append (number->string a) \"-\" (number->string a))\n get-fn set-fn) 0))))\n\n;; Lexer: tokenize never crashes on printable ASCII strings\n(check-property-pass \"lexer doesn't crash on random strings\"\n (for-all ([s gen-string])\n (guard (__exn [#t #t]) ;; exceptions are OK (invalid syntax), crashes are not\n (begin (tokenize s) #t))))\n\n;; History: search results all start with the prefix\n(check-property-pass \"history search prefix\"\n (for-all ([prefix-len (gen-choose 0 4)])\n (let* ([prefix (substring \"echo\" 0 (min prefix-len 4))]\n [results (history-search prefix)])\n (or (null? results)\n (every-in-list? results\n (lambda (r)\n (and (>= (string-length r) (string-length prefix))\n (string=? prefix\n (substring r 0 (string-length prefix))))))))))\n\n\n;;; ─────────────────────────────────────\n;;; Summary\n;;; ─────────────────────────────────────\n(printf \"~njsh unit tests: ~a passed, ~a failed~n\" pass-count fail-count)\n(when (> fail-count 0) (exit 1))\n"} {"text":";; FILE: jerboa-shell/_vendor/oils/README-index.md\n\nOils Repo READMEs\n=================\n\nThis page is useful for finding docs that are out of date.\n\nGenerate it with:\n\n build/doc.sh gen-readme-index\n\n\n- [asdl/README.md](asdl/README.md)\n- [benchmarks/README.md](benchmarks/README.md)\n- [build/README.md](build/README.md)\n- [builtin/README.md](builtin/README.md)\n- [cpp/README.md](cpp/README.md)\n- [data_lang/README.md](data_lang/README.md)\n- [deps/README.md](deps/README.md)\n- [devtools/README.md](devtools/README.md)\n- [doc/README.md](doc/README.md)\n- [doctools/README.md](doctools/README.md)\n- [metrics/README.md](metrics/README.md)\n- [mycpp/README.md](mycpp/README.md)\n- [opy/README.md](opy/README.md)\n- [prebuilt/README.md](prebuilt/README.md)\n- [soil/README.md](soil/README.md)\n- [spec/README.md](spec/README.md)\n- [stdlib/README.md](stdlib/README.md)\n- [tools/README.md](tools/README.md)\n- [trees/README.md](trees/README.md)\n- [vendor/README.md](vendor/README.md)\n- [yaks/README.md](yaks/README.md)\n"} --- a/cpt_corpus_v6_suggestions.jsonl +++ b/cpt_corpus_v6_suggestions.jsonl @@ -240,7 +240,7 @@ {"text":";; FILE: jerboa-shell/script.ss\n;;; script.ss — Script execution for gsh\n;;; Handles running script files and sourcing files into the current environment.\n\n(export #t)\n(import :std/sugar\n :std/format\n (except (std misc string) string-join string-index)\n :jsh/util\n :jsh/ast\n :jsh/environment\n :jsh/functions\n :jsh/lexer\n :jsh/parser\n :jsh/executor\n :jsh/signals\n :jsh/jobs\n :jsh/static-compat\n :jsh/registry)\n\n;;; --- Meta-command handler (set by main.ss to wire up ,compile etc.) ---\n\n(def *meta-command-handler* (make-parameter #f))\n(def *meta-command-positional* (make-parameter '()))\n\n;;; --- Jerboa Expander (lazy init) ---\n\n(def *jerboa-eval-initialized* #f)\n\n(def (ensure-jerboa-eval!)\n ;; Jerboa: eval uses Chez's (interaction-environment) — no separate expander\n ;; needed. The 'tiny' tier still blocks eval to keep the binary lean.\n (when (string=? (*jsh-tier*) \"tiny\")\n (error #f \"eval not available in this build (tier: tiny). Rebuild with JSH_TIER=small or higher\"))\n (unless *jerboa-eval-initialized*\n (set! *jerboa-eval-initialized* #t)\n (ensure-static-compat!)))\n\n;;; --- Scheme Evaluation Helpers ---\n\n(def (fmt-bytes b)\n \"Format a byte count as a human-readable string (B/KB/MB/GB).\"\n (cond\n ((>= b (* 1024 1024 1024))\n (string-append (number->string (/ (floor (* (/ b (* 1024 1024 1024)) 100)) 100.0)) \" GB\"))\n ((>= b (* 1024 1024))\n (string-append (number->string (/ (floor (* (/ b (* 1024 1024)) 100)) 100.0)) \" MB\"))\n ((>= b 1024)\n (string-append (number->string (/ (floor (* (/ b 1024) 100)) 100.0)) \" KB\"))\n (else\n (string-append (number->string (inexact->exact (floor b))) \" B\"))))\n\n(def (handle-room-command)\n ;; Display GC, heap, and runtime information (Chez equivalent of Common Lisp's ROOM).\n (with-catch\n (lambda (e)\n (cons (call-with-output-string\n (lambda (port) (display \"Error: \" port) (display-exception e port)))\n 1))\n (lambda ()\n (collect)\n (let* ((alloc-total (bytes-allocated))\n (cpu-ms (cpu-time))\n (real-ms (real-time))\n (num-gcs (collections)))\n (cons\n (call-with-output-string\n (lambda (port)\n (display \"--- GC & Heap ---\\n\" port)\n (display \" Bytes allocated: \" port) (display (fmt-bytes alloc-total) port) (newline port)\n (display \" GC runs: \" port) (display num-gcs port) (newline port)\n (newline port)\n (display \"--- Process ---\\n\" port)\n (display \" CPU time: \" port) (display cpu-ms port) (display \" ms\" port) (newline port)\n (display \" Real time: \" port) (display real-ms port) (display \" ms\" port) (newline port)\n (newline port)\n (display \"--- Runtime ---\\n\" port)\n (display \" Chez Scheme: \" port) (display (scheme-version) port) (newline port)\n (display \" Machine type: \" port) (display (machine-type) port)))\n 0)))))\n\n(def (eval-scheme-expr expr-str)\n ;; Evaluate a Jerboa Scheme expression string and return (cons result-string status)\n ;; Status: 0 = success, 1 = error\n ;; Handle built-in meta-commands that don't need any tier\n (cond\n ((string=? expr-str \"room\") (handle-room-command))\n (else\n ;; Check for meta-commands (,compile, ,load, ,use, ,exports)\n (let ((handler (*meta-command-handler*)))\n (or (and handler (handler expr-str))\n ;; Normal Scheme eval\n (begin\n (ensure-jerboa-eval!)\n (with-catch\n (lambda (e)\n (cons (call-with-output-string\n (lambda (port)\n (display \"Scheme error: \" port)\n (display-exception e port)))\n 1))\n (lambda ()\n (let* ((expr (call-with-input-string expr-str read))\n (result (eval expr)))\n (cons\n (cond\n ;; void: no output\n ((eq? result (void)) \"\")\n ;; Multiline results: use pretty-print\n ((or (pair? result) (vector? result))\n (call-with-output-string\n (lambda (port)\n (pretty-print result port))))\n ;; Simple values: use write for unambiguous output\n (else\n (call-with-output-string\n (lambda (port)\n (write result port)))))\n 0))))))))))\n\n(def (scheme-eval-line? line)\n ;; Check if line starts with comma meta-command\n (and (> (string-length line) 0)\n (char=? (string-ref line 0) #\\,)))\n\n(def (extract-scheme-expr line)\n ;; Strip leading comma and whitespace\n (let* ((without-comma (substring line 1 (string-length line)))\n (start 0)\n (end (string-length without-comma)))\n ;; Trim leading whitespace\n (let loop-start ((i 0))\n (if (and (< i end) (char-whitespace? (string-ref without-comma i)))\n (loop-start (+ i 1))\n (substring without-comma i end)))))\n\n;;; --- Public interface ---\n\n;; Execute a script file with arguments.\n;; Sets $0 to filename, $1.. to args.\n;; Returns exit status.\n(def (execute-script filename args env)\n (if (not (file-exists? filename))\n (begin\n (fprintf (current-error-port) \"jsh: ~a: No such file or directory~n\" filename)\n 127)\n (with-catch\n (lambda (e)\n (cond\n ((break-exception? e) 0)\n ((continue-exception? e) 0)\n ((subshell-exit-exception? e) (subshell-exit-exception-status e))\n ((nounset-exception? e) (nounset-exception-status e))\n (else\n (fprintf (current-error-port) \"jsh: ~a: ~a~n\" filename (exception-message e))\n 1)))\n (lambda ()\n (let* ((content (read-file-to-string filename))\n ;; Strip shebang if present\n (script-content (strip-shebang content))\n ;; Create child environment for script\n (script-env (env-push-scope env)))\n ;; Set positional parameters\n (env-set-shell-name! script-env filename)\n (env-set-positional! script-env args)\n ;; Set LINENO tracking\n (env-set! script-env \"LINENO\" \"0\")\n ;; Execute the script content\n (parameterize ((*current-source-file* filename))\n (execute-string script-content script-env)))))))\n\n;; Source a file into the current environment (like bash's `source` or `.`)\n;; Runs in the CURRENT environment, not a child.\n;; Returns exit status.\n(def (source-file! filename env)\n (if (not (file-exists? filename))\n (begin\n (fprintf (current-error-port) \"jsh: ~a: No such file or directory~n\" filename)\n 1)\n (with-catch\n (lambda (e)\n (cond\n ;; break/continue must propagate to caller's loop\n ((break-exception? e) (raise e))\n ((continue-exception? e) (raise e))\n ;; return exits the sourced file, not the calling function\n ((return-exception? e) (return-exception-status e))\n ((errexit-exception? e) (raise e))\n ((subshell-exit-exception? e) (raise e))\n ((nounset-exception? e) (raise e))\n (else\n (fprintf (current-error-port) \"jsh: ~a: ~a~n\" filename (exception-message e))\n 1)))\n (lambda ()\n (let* ((content (read-file-to-string filename))\n (script-content (strip-shebang content)))\n (parameterize ((*current-source-file* filename))\n (execute-string script-content env)))))))\n\n;;; --- String execution ---\n\n;; Parse and execute a string of shell commands.\n;; Used by both execute-script and source-file!\n;; Lines starting with comma (,) are evaluated as Scheme instead of being parsed as shell.\n(def (execute-string input env (interactive? #f))\n ;; Split input into lines for preprocessing\n (let ((lines (string-split input #\\newline)))\n (let line-loop ((remaining-lines lines) (status 0) (shell-buffer '()))\n (cond\n ;; No more lines - execute any pending shell commands\n ((null? remaining-lines)\n (if (null? shell-buffer)\n status\n (let ((shell-input (string-join (reverse shell-buffer) \"\\n\")))\n (execute-shell-lines shell-input env interactive? status))))\n ;; Scheme eval line (starts with comma)\n ((scheme-eval-line? (car remaining-lines))\n ;; First, execute any accumulated shell commands\n (let* ((shell-status (if (null? shell-buffer)\n status\n (let ((shell-input (string-join (reverse shell-buffer) \"\\n\")))\n (execute-shell-lines shell-input env interactive? status))))\n ;; Then evaluate the Scheme expression\n (expr-str (extract-scheme-expr (car remaining-lines)))\n (result-status\n (parameterize ((*meta-command-positional*\n (env-positional-list env)))\n (eval-scheme-expr expr-str)))\n (result (car result-status))\n (scheme-status (cdr result-status)))\n ;; Display result if non-empty\n (unless (string=? result \"\")\n (display result)\n (newline))\n (env-set-last-status! env scheme-status)\n (line-loop (cdr remaining-lines) scheme-status '())))\n ;; Regular shell line - accumulate\n (else\n (line-loop (cdr remaining-lines) status (cons (car remaining-lines) shell-buffer)))))))\n\n;; Execute accumulated shell lines using the lexer/parser\n(def (execute-shell-lines input env interactive? initial-status)\n (let ((lexer (make-shell-lexer input (env-shopt? env \"extglob\"))))\n (let loop ((status initial-status))\n (let ((cmd (with-catch\n (lambda (e)\n (fprintf (current-error-port) \"jsh: syntax error: ~a~n\"\n (exception-message e))\n 'error)\n (lambda ()\n ;; Update lexer extglob flag in case shopt changed it\n (set! (lexer-extglob? lexer) (env-shopt? env \"extglob\"))\n ;; Build alias lookup: checks expand_aliases shopt, returns value or #f\n (let ((alias-fn (and (env-shopt? env \"expand_aliases\")\n (lambda (word) (alias-get env word)))))\n (parse-one-line lexer (env-shopt? env \"extglob\") alias-fn))))))\n (cond\n ((eq? cmd 'error) 2) ;; syntax error\n ((not cmd) status) ;; end of input\n ;; Unterminated quote/construct after parsing — syntax error\n ((lexer-want-more? lexer)\n (fprintf (current-error-port)\n \"jsh: syntax error: unexpected end of file~n\")\n (env-set-last-status! env 2)\n 2)\n (else\n (let ((new-status\n (with-catch\n (lambda (e)\n (cond\n ((nounset-exception? e)\n ;; In interactive mode, nounset only aborts current line\n (if interactive?\n (nounset-exception-status e)\n (raise e)))\n ((errexit-exception? e)\n (errexit-exception-status e))\n ((break-exception? e) (raise e))\n ((continue-exception? e) (raise e))\n ((subshell-exit-exception? e) (raise e))\n ((return-exception? e) (raise e))\n (else\n ;; Catch-all: print error and continue\n (let ((msg (exception-message e)))\n (with-catch (lambda (_) #!void)\n (lambda ()\n (fprintf (current-error-port) \"jsh: ~a~n\" msg)))\n ;; POSIX: syntax errors / unclosed bad substitution → exit code 2\n (if (and (string? msg)\n (or (string-prefix? \"parse error\" msg)\n (string-prefix? \"bad substitution: unclosed\" msg)))\n 2 1)))))\n (lambda ()\n (execute-command cmd env)))))\n ;; Flush stdout/stderr so builtin output appears before next command\n (with-catch (lambda (_) #!void)\n (lambda ()\n (force-output (current-output-port))\n (force-output (current-error-port))))\n (let ((__cth (*command-trace-hook*)))\n (when __cth\n (with-catch (lambda (_) #!void)\n (lambda () (__cth cmd new-status env)))))\n (env-set-last-status! env new-status)\n ;; Process pending signals between commands\n (process-pending-traps! env)\n ;; If errexit triggered, stop executing further commands\n (if (and (not (= new-status 0))\n (env-option? env \"errexit\")\n (not (*in-condition-context*)))\n new-status\n (loop new-status)))))))))\n\n;; Process pending signals and execute trap commands\n;; Lightweight version for script.ss (avoids circular import with main.ss)\n;; Traps execute with $? isolated — they don't affect the main script's $?\n(def (process-pending-traps! env)\n ;; No sleep needed — C-level signal flags are checked synchronously\n ;; in pending-signals! via ffi-signal-flag-check\n (let ((signals (pending-signals!)))\n (for-each\n (lambda (sig-name)\n (cond\n ((string=? sig-name \"CHLD\")\n (job-update-status!)\n (job-notify!))\n (else #!void))\n (let ((action (trap-get sig-name)))\n (cond\n ;; Signal has a trap command — execute it\n ((and action (string? action))\n ;; Save and restore $? so trap doesn't affect main flow\n (let ((saved-status (shell-environment-last-status env))\n (exec-fn (*execute-input*)))\n (when exec-fn\n (exec-fn action env))\n (env-set-last-status! env saved-status)))\n ;; Fatal signal with no trap — exit the script\n ;; (POSIX: default action for INT/TERM/HUP/XFSZ is to terminate)\n ((and (not action)\n (member sig-name '(\"INT\" \"TERM\" \"HUP\" \"XFSZ\")))\n (let ((signum (signal-name->number sig-name)))\n (raise (make-subshell-exit-exception (+ 128 (or signum 2)))))))))\n signals)))\n\n;;; --- Helpers ---\n\n(def (strip-shebang content)\n ;; Replace #! line with blank line (preserves line numbering for extdebug)\n (if (and (>= (string-length content) 2)\n (char=? (string-ref content 0) #\\#)\n (char=? (string-ref content 1) #\\!))\n ;; Find end of first line and replace with empty\n (let loop ((i 0))\n (cond\n ((>= i (string-length content)) \"\")\n ((char=? (string-ref content i) #\\newline)\n (substring content i (string-length content)))\n (else (loop (+ i 1)))))\n content))\n\n(def (read-file-to-string filename)\n ;; Read entire file contents as a string\n (call-with-input-file filename\n (lambda (port)\n (let ((out (open-output-string)))\n (let loop ()\n (let ((ch (read-char port)))\n (unless (eof-object? ch)\n (write-char ch out)\n (loop))))\n (get-output-string out)))))\n"} {"text":";; FILE: jerboa-shell/build-jerboa.ss\n#!chezscheme\n;;; build-jerboa.ss — Legacy compiler path for jerboa-shell .ss modules\n;;; with Jerboa imports.\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib> --compile-imported-libraries < build-jerboa.ss\n;;;\n;;; The output .sls files import from Jerboa's module paths:\n;;; (jerboa runtime), (std ...), etc.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (compiler compile))\n\n;; Thread-safe mutex helper used throughout this file\n(define-syntax with-mutex\n (syntax-rules ()\n [(_ m body ...)\n (dynamic-wind\n (lambda () (mutex-acquire m))\n (lambda () body ...)\n (lambda () (mutex-release m)))]))\n\n;; --- Configuration ---\n(define submodule-dir \"jerboa-shell\")\n(define output-dir \"src/jsh\")\n\n;; Find source file: check local override first, then submodule\n(define (find-source name)\n (let ([local (string-append \"./\" name \".ss\")]\n [sub (string-append submodule-dir \"/\" name \".ss\")])\n (cond\n ((file-exists? local) local)\n ((file-exists? sub) sub)\n (else (error 'find-source \"source file not found\" name)))))\n\n;; --- Import map: Jerboa module → Jerboa library ---\n;; KEY DIFFERENCE from the legacy shell: maps to (std ...) / (jerboa ...) paths\n(define jsh-import-map\n '(;; Standard library → Jerboa stdlib\n (:std/sugar . (std sugar))\n (:std/format . (std format))\n (:std/sort . (std sort))\n (:std/pregexp . (std pregexp))\n (:std/regex . (std regex))\n (:std/misc/string . (std misc string))\n (:std/misc/list . (std misc list))\n (:std/misc/path . (std os path))\n (:std/misc/hash . (jerboa runtime))\n (:std/iter . #f) ;; stripped — Gherkin compiles for-loops natively\n (:std/error . (std error))\n (:std/os/signal . (std os signal))\n (:std/os/signal-handler . (std os signal))\n (:std/os/fdio . (std os fdio))\n (:std/srfi/1 . (std misc list))\n (:std/foreign . #f) ;; stripped\n (:std/build-script . #f) ;; stripped\n ;; Jerboa runtime\n (:jerboa/core . #f) ;; stripped\n (:jerboa/runtime . #f) ;; stripped\n (:jerboa/runtime/init . #f)\n (:jerboa/runtime/loader . #f)\n (:jerboa/expander . #f)\n (:jerboa/compiler . #f)\n ;; Relative imports\n (\"./pregexp-compat\" . (jsh pregexp-compat))\n ;; gsh module mappings → jsh\n (:jsh/arithmetic . (jsh arithmetic))\n (:jsh/ast . (jsh ast))\n (:jsh/builtins . (jsh builtins))\n (:jsh/completion . (jsh completion))\n (:jsh/control . (jsh control))\n (:jsh/environment . (jsh environment))\n (:jsh/executor . (jsh executor))\n (:jsh/expander . (jsh expander))\n (:jsh/ffi . (jsh ffi))\n (:jsh/functions . (jsh functions))\n (:jsh/fuzzy . (jsh fuzzy))\n (:jsh/fzf . (jsh fzf))\n (:jsh/glob . (jsh glob))\n (:jsh/history . (jsh history))\n (:jsh/jobs . (jsh jobs))\n (:jsh/lexer . (jsh lexer))\n (:jsh/lineedit . (jsh lineedit))\n (:jsh/macros . (jsh macros))\n (:jsh/main . (jsh main))\n (:jsh/parser . (jsh parser))\n (:jsh/pipeline . (jsh pipeline))\n (:jsh/pregexp-compat . (jsh pregexp-compat))\n (:jsh/prompt . (jsh prompt))\n (:jsh/redirect . (jsh redirect))\n (:jsh/registry . (jsh registry))\n (:jsh/script . (jsh script))\n (:jsh/signals . (jsh signals))\n (:jsh/stage . (jsh stage))\n (:jsh/startup . (jsh startup))\n (:jsh/static-compat . (jsh static-compat))\n (:jsh/util . (jsh util))\n (:jsh/recorder . (jsh recorder))\n (:jsh/player . (jsh player))\n (:jsh/recording-index . (jsh recording-index))\n ;; Actor system\n (:std/actor/core . (std actor core))\n (:std/actor/protocol . (std actor protocol))\n (:std/actor/transport . (std actor transport))\n (:std/actor/registry . (std actor registry))\n (:std/actor/supervisor . (std actor supervisor))\n ;; Crypto\n (:std/crypto/cipher . (std crypto cipher))\n (:std/crypto/hmac . (std crypto hmac))\n (:std/crypto/etc . (std crypto etc))\n ))\n\n;; --- Base imports for all compiled modules ---\n;; KEY DIFFERENCE: uses (jerboa runtime) and local (compat gambit), not\n;; legacy runtime util/table/mop/hash modules.\n(define jsh-base-imports\n '((except (chezscheme) box box? unbox set-box!\n iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error? raise with-exception-handler identifier?\n hash-table? make-hash-table\n sort sort! path-extension\n printf fprintf\n ;; Exclude Chez builtins that (compat gambit) replaces\n file-directory? file-exists? getenv close-port\n ;; Chez void takes 0 args; Jerboa's is variadic\n void\n ;; Gambit-compatible: handles /dev/fd/N and keyword args\n open-output-file open-input-file)\n ;; Jerboa runtime provides: hash tables, keywords, errors, utilities,\n ;; and method dispatch.\n ;; Exclude void — (jerboa runtime) re-exports Chez's 0-arg void,\n ;; but Jerboa's void is variadic. Let (compat gambit)'s version win.\n (except (jerboa runtime) ~ void)\n\n ;; Import most of (compat gambit) — u8vector, threading, etc.\n ;; Exclude names that conflict with (chezscheme) builtins we still need:\n (except (compat gambit) number->string make-mutex\n with-output-to-string)\n ;; Std error for Error type, error predicates\n (std error)\n ;; Std misc for string-split, string-join, string-prefix?, path-expand, etc.\n (std misc string)\n (std misc list)\n (std misc alist)\n (std os path)\n (std format)\n (std sort)\n (std pregexp)\n (std regex)\n ))\n\n;; --- Import conflict resolution ---\n;; Import conflict resolution is compiler infrastructure, not runtime.\n(define (fix-import-conflicts lib-form)\n (let* ([lib-name (cadr lib-form)]\n [export-clause (caddr lib-form)]\n [import-clause (cadddr lib-form)]\n [body (cddddr lib-form)]\n [imports (cdr import-clause)]\n [local-defs\n (let lp ([forms body] [names '()])\n (if (null? forms)\n names\n (lp (cdr forms)\n (append (extract-def-names (car forms)) names))))]\n [all-earlier-names\n (let lp ([imps imports] [seen '()] [result '()])\n (if (null? imps)\n (reverse result)\n (let* ([imp (car imps)]\n [lib (get-import-lib-name imp)]\n [exports (if lib (cached-library-exports lib) '())]\n [provided (cond\n ((and (pair? imp) (eq? (car imp) 'except))\n (filter (lambda (s) (not (memq s (cddr imp))))\n exports))\n ((and (pair? imp) (eq? (car imp) 'only))\n (cddr imp))\n (else exports))])\n (lp (cdr imps)\n (append provided seen)\n (cons seen result)))))])\n (let ([fixed-imports\n (map (lambda (imp earlier-names)\n (fix-one-import imp\n (append local-defs earlier-names)))\n imports all-earlier-names)])\n (let ([fixed-body (fix-assigned-exports\n (cdr export-clause)\n (list (cons 'import fixed-imports))\n body)])\n `(library ,lib-name ,export-clause\n (import ,@fixed-imports) ,@fixed-body)))))\n\n;; Fix exported variables that are set!'d (R6RS forbids this)\n(define (fix-assigned-exports exports import-forms body)\n (let ([assigned-names\n (let lp ([tree body] [names '()])\n (cond\n ((not (pair? tree)) names)\n ((and (eq? (car tree) 'set!)\n (pair? (cdr tree))\n (symbol? (cadr tree))\n (memq (cadr tree) exports)\n (not (memq (cadr tree) names)))\n (cons (cadr tree) names))\n (else\n (lp (cdr tree) (lp (car tree) names)))))])\n (if (null? assigned-names)\n body\n (let ([new-body\n (let lp ([forms body] [result '()])\n (if (null? forms)\n (reverse result)\n (let ([form (car forms)])\n (cond\n ((and (pair? form)\n (eq? (car form) 'define)\n (let ([def-name (if (pair? (cadr form)) (caadr form) (cadr form))])\n (and (symbol? def-name) (memq def-name assigned-names))))\n (let* ([def-name (if (pair? (cadr form)) (caadr form) (cadr form))]\n [init (if (pair? (cadr form))\n `(lambda ,(cdadr form) ,@(cddr form))\n (if (pair? (cddr form)) (caddr form) '(void)))]\n [cell-name (string->symbol\n (string-append (symbol->string def-name) \"-cell\"))])\n (lp (cdr forms)\n (append\n (list\n `(define-syntax ,def-name\n (identifier-syntax\n (id (vector-ref ,cell-name 0))\n ((set! id v) (vector-set! ,cell-name 0 v))))\n `(define ,cell-name (vector ,init)))\n result))))\n (else\n (lp (cdr forms) (cons form result)))))))])\n new-body))))\n\n(define (extract-def-names form)\n (cond\n ((not (pair? form)) '())\n ((eq? (car form) 'define)\n (cond\n ((symbol? (cadr form)) (list (cadr form)))\n ((pair? (cadr form)) (list (caadr form)))\n (else '())))\n ((eq? (car form) 'define-syntax)\n (if (symbol? (cadr form)) (list (cadr form)) '()))\n ((eq? (car form) 'begin)\n (let lp ([forms (cdr form)] [names '()])\n (if (null? forms) names\n (lp (cdr forms) (append (extract-def-names (car forms)) names)))))\n (else '())))\n\n;; Serialize eval/library-exports across threads — both load and cache are protected\n(define eval-mutex (make-mutex 'eval))\n\n(define (ensure-library-loaded lib-name)\n (with-mutex eval-mutex\n (guard (e (#t #f))\n (eval `(import ,lib-name) (interaction-environment))\n #t)))\n\n;; Export cache: avoids repeated library-exports calls for the same library\n;; across all module compilations. With ~15 imports × 22 modules = ~330 calls,\n;; most hit the same base libraries — cache turns these into hash lookups.\n(define export-cache (make-hashtable equal-hash equal?))\n\n(define (cached-library-exports lib-name)\n (with-mutex eval-mutex\n (let ([hit (hashtable-ref export-cache lib-name 'miss)])\n (if (not (eq? hit 'miss))\n hit\n (begin\n (guard (e (#t #f))\n (eval `(import ,lib-name) (interaction-environment)))\n (let ([exports (or (guard (e (#t #f)) (library-exports lib-name))\n (read-sls-exports lib-name)\n '())])\n (hashtable-set! export-cache lib-name exports)\n exports))))))\n\n(define (read-sls-exports lib-name)\n (let ([path (lib-name->sls-path lib-name)])\n (if (and path (file-exists? path))\n (guard (e (#t #f))\n (call-with-input-file path\n (lambda (port)\n ;; Chez doesn't expand #,(...) at read or eval time, so reading\n ;; an untrusted (library ...) header is safe — no need to gate.\n (let ([first (read port)])\n (let ([lib-form (if (and (pair? first) (eq? (car first) 'library))\n first\n (read port))])\n (if (and (pair? lib-form) (eq? (car lib-form) 'library))\n (let ([export-clause (caddr lib-form)])\n (if (and (pair? export-clause) (eq? (car export-clause) 'export))\n (cdr export-clause)\n #f))\n #f))))))\n #f)))\n\n(define (lib-name->sls-path lib-name)\n (cond\n ((and (pair? lib-name) (= (length lib-name) 2)\n (eq? (car lib-name) 'jsh))\n (string-append output-dir \"/\" (symbol->string (cadr lib-name)) \".sls\"))\n ((and (pair? lib-name) (= (length lib-name) 2)\n (eq? (car lib-name) 'compat))\n (string-append \"src/compat/\" (symbol->string (cadr lib-name)) \".sls\"))\n (else #f)))\n\n(define (fix-one-import imp local-defs)\n (let ([lib-name (get-import-lib-name imp)])\n (if (not lib-name)\n imp\n (let* ([lib-exports (cached-library-exports lib-name)]\n [conflicts (filter (lambda (d) (memq d lib-exports))\n local-defs)])\n (if (null? conflicts)\n imp\n (cond\n ((and (pair? imp) (eq? (car imp) 'except))\n (let ([existing (cddr imp)])\n `(except ,(cadr imp)\n ,@existing\n ,@(filter (lambda (d) (not (memq d existing)))\n conflicts))))\n ((and (pair? imp) (eq? (car imp) 'only))\n (let ([kept (filter (lambda (s) (not (memq s conflicts)))\n (cddr imp))])\n `(only ,(cadr imp) ,@kept)))\n ((pair? imp)\n `(except ,imp ,@conflicts))\n (else imp)))))))\n\n(define (get-import-lib-name spec)\n (cond\n ((and (pair? spec)\n (memq (car spec) '(except only rename prefix)))\n (get-import-lib-name (cadr spec)))\n ((and (pair? spec) (symbol? (car spec)))\n spec)\n (else #f)))\n\n;; --- Incremental builds: skip unchanged modules ---\n;; Set FORCE=1 in env to rebuild everything regardless of timestamps.\n(define force-rebuild?\n (let ([v (getenv \"FORCE\")])\n (and v (not (string=? v \"\")))))\n\n(define (needs-rebuild? input-path output-path)\n (or force-rebuild?\n (not (file-exists? output-path))\n (< (time-second (file-modification-time output-path))\n (time-second (file-modification-time input-path)))))\n\n;; Track which .sls files were freshly generated this session.\n;; Post-build patches should only apply to these files (not to up-to-date ones).\n(define compiled-files-mutex (make-mutex 'compiled-files))\n(define compiled-files '())\n(define (record-compiled! path)\n (with-mutex compiled-files-mutex\n (set! compiled-files (cons path compiled-files))))\n(define (compiled-this-session? path)\n (with-mutex compiled-files-mutex\n (member path compiled-files)))\n\n;; --- Module compilation ---\n(define (compile-module name)\n (let* ([input-path (find-source name)]\n [output-path (string-append output-dir \"/\" name \".sls\")]\n [lib-name `(jsh ,(string->symbol name))])\n (if (not (needs-rebuild? input-path output-path))\n (begin (display (string-append \" Up-to-date: \" name \".ss\\n\")) #t)\n (begin\n (display (string-append \" Compiling: \" name \".ss → \" name \".sls\\n\"))\n (guard (exn\n (#t (display (string-append \" ERROR: \" name \".ss failed: \"))\n (display (condition-message exn))\n (when (irritants-condition? exn)\n (display \" — \")\n (display (condition-irritants exn)))\n (newline)\n #f))\n (let* ([lib-form (jerboa-compile-to-library\n input-path lib-name\n jsh-import-map jsh-base-imports)]\n [lib-form (fix-import-conflicts lib-form)])\n (call-with-output-file output-path\n (lambda (port)\n (display \"#!chezscheme\\n\" port)\n (parameterize ([print-gensym #f])\n (pretty-print lib-form port)))\n 'replace)\n (record-compiled! output-path)\n (display (string-append \" OK: \" output-path \"\\n\"))\n #t))))))\n\n;; --- Parallel tier compilation ---\n;; Modules within a tier are independent; compile them concurrently.\n;; eval/library-exports calls are serialized via eval-mutex; the\n;; jerboa-compile-to-library step (reading + parsing .ss) runs in parallel.\n(define (compile-tier label modules)\n (display (format \"\\n--- ~a ---\\n\" label))\n (if (= (length modules) 1)\n ;; Single module: avoid thread overhead\n (compile-module (car modules))\n (let* ([done-mutex (make-mutex 'done)]\n [done-cond (make-condition)]\n [remaining (length modules)])\n (for-each\n (lambda (mod)\n (fork-thread\n (lambda ()\n (compile-module mod)\n (with-mutex done-mutex\n (set! remaining (- remaining 1))\n (when (= remaining 0)\n (condition-signal done-cond))))))\n modules)\n (with-mutex done-mutex\n (let lp ()\n (when (> remaining 0)\n (condition-wait done-cond done-mutex)\n (lp)))))))\n\n;; --- Main ---\n(display \"=== Jerboa Shell Builder ===\\n\\n\")\n\n;; When JERBUILD_SKIP_COMPILE=1, the .ss→.sls step was already done by jerbuild.ss.\n;; Skip Gherkin compilation and go straight to post-build patching.\n(unless (equal? (getenv \"JERBUILD_SKIP_COMPILE\") \"1\")\n ;; Pre-warm export cache with all base imports so parallel threads get cache hits\n (display \"--- Pre-warming export cache ---\\n\")\n (for-each\n (lambda (imp)\n (let ([lib (get-import-lib-name imp)])\n (when lib (cached-library-exports lib))))\n jsh-base-imports)\n\n (compile-tier \"Tier 1: Foundation\" '(\"ast\" \"registry\"))\n (compile-tier \"Tier 2: Core\" '(\"macros\" \"util\"))\n (compile-tier \"Tier 3: Modules\" '(\"environment\" \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\" \"recorder\" \"player\"))\n (compile-tier \"Tier 4: Processing\" '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (compile-tier \"Tier 5: Execution\" '(\"redirect\" \"control\" \"jobs\"))\n (compile-tier \"Tier 5b: Builtins\" '(\"builtins\"))\n (compile-tier \"Tier 6: Pipeline\" '(\"pipeline\"))\n (compile-tier \"Tier 6b: UI\" '(\"executor\" \"completion\" \"prompt\" \"lineedit\"))\n (compile-tier \"Tier 7: Top-level\" '(\"fzf\" \"script\" \"startup\" \"main\")))\n\n;; --- Post-build: Force library invocation for side-effecting modules ---\n(display \"\\n--- Post-build: Patching for Chez lazy invocation ---\\n\")\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (let* ([path \"src/jsh/main.sls\"]\n [content (call-with-input-file path\n (lambda (p) (get-string-all p)))])\n (if (or (not (compiled-this-session? path)) (string-find content \"_force-builtins\"))\n (display \" (skip) main.sls lazy invocation\\n\")\n (let ([needle (string #\\newline #\\space #\\space #\\( #\\d #\\e #\\f #\\i #\\n #\\e #\\space)])\n (let ([idx (string-find content needle)])\n (if idx\n (begin\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display \"\\n ;; Force invocation of (jsh builtins) for defbuiltin registration\\n\" p)\n (display \" (define _force-builtins special-builtin?)\" p)\n (display (substring content idx (string-length content)) p))\n 'replace)\n (display \" Patched main.sls for lazy invocation\\n\"))\n (display \" WARNING: Could not find insertion point in main.sls\\n\")))))))\n\n;; BSD sed (macOS, FreeBSD) requires -i '' (empty backup suffix); Linux sed uses -i alone\n(define sed-i-flag\n (let* ([mt (symbol->string (machine-type))]\n [len (string-length mt)]\n [ends-fb (and (>= len 2)\n (string=? (substring mt (- len 2) len) \"fb\"))]\n [ends-osx (and (>= len 3)\n (string=? (substring mt (- len 3) len) \"osx\"))])\n (if (or ends-fb ends-osx)\n \"-i ''\" \"-i\")))\n;; SECURITY: pattern and files are interpolated into a shell command.\n;; Only call with hard-coded string literals — never with dynamic input.\n(define (sed-replace pattern files)\n (system (format \"sed ~a '~a' ~a\" sed-i-flag pattern files)))\n\n;; --- Post-build patches ---\n(display \"\\n--- Post-build: Applying patches ---\\n\")\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n ;; Fix env-push-scope: keyword-style args → positional\n (patch-file! \"src/jsh/environment.sls\"\n \"(define (env-push-scope env)\\n (make-shell-environment\\n 'parent:\\n env\\n 'name:\\n (shell-environment-shell-name env)))\"\n \"(define (env-push-scope env)\\n (make-shell-environment env (shell-environment-shell-name env)))\")\n\n ;; Fix env-clone: no-arg constructor + manual set\n (patch-file! \"src/jsh/environment.sls\"\n \"(define (env-clone env)\\n (let ([clone (make-shell-environment\\n 'name:\\n (shell-environment-shell-name env))])\"\n \"(define (env-clone env)\\n (let ([clone (let ([e (make-shell-environment)])\\n (shell-environment-shell-name-set! e (shell-environment-shell-name env))\\n e)])\")\n\n ;; Fix exception-message: Chez condition-message returns raw format templates\n (patch-file! \"src/jsh/util.sls\"\n \"(define (exception-message e)\\n (cond\\n [(Error? e) (Error-message e)]\\n [(error-exception? e) (error-exception-message e)]\\n [(string? e) e]\\n [(os-exception? e)\\n (call-with-output-string\\n (lambda (p) (display-exception e p)))]\\n [else\\n (call-with-output-string\\n (lambda (p) (display-exception e p)))]))\"\n \"(define (exception-message e)\\n (define (format-condition e)\\n (let ([msg (call-with-string-output-port\\n (lambda (p) (display-condition e p)))])\\n (if (and (> (string-length msg) 11)\\n (string=? (substring msg 0 11) \\\"Exception: \\\"))\\n (substring msg 11 (string-length msg))\\n (if (and (> (string-length msg) 13)\\n (string=? (substring msg 0 13) \\\"Exception in \\\"))\\n (let loop ([i 13])\\n (cond\\n [(>= (+ i 1) (string-length msg)) msg]\\n [(and (char=? (string-ref msg i) #\\\\:)\\n (char=? (string-ref msg (+ i 1)) #\\\\space))\\n (substring msg (+ i 2) (string-length msg))]\\n [else (loop (+ i 1))]))\\n msg))))\\n (cond\\n [(string? e) e]\\n [(condition? e) (format-condition e)]\\n [else (call-with-string-output-port\\n (lambda (p) (display e p)))]))\")\n\n ;; Fix make-mutex: Chez requires symbol or #f, not strings\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(make-mutex \\\"pipeline-fd\\\")\"\n \"(make-mutex 'pipeline-fd)\")\n\n ;; In pipelines, prefer fork+exec over builtins for true multi-CPU parallelism.\n ;; External binaries use optimized C I/O with large buffers and zero contention.\n (patch-file! \"src/jsh/pipeline.sls\"\n \"[(and cmd-name (builtin-lookup cmd-name))\\n (launch-thread-piped cmd child-env execute-fn\\n has-pipe-in? has-pipe-out?)]\\n [(and cmd-name (which cmd-name))\"\n \"[(and cmd-name (which cmd-name))\")\n\n ;; --- FreeBSD portability: /dev/fd/N doesn't work for pipe fds ---\n ;; Patching is done via a separate script (support/patch-devfd.ss)\n ;; called from the Makefile after the jerboa step.\n\n ;; --- Performance optimizations ---\n\n ;; Add (std misc lru-cache) import to util.sls for which-cached\n (patch-file! \"src/jsh/util.sls\"\n \"(except (jerboa runtime)\"\n \"(std misc lru-cache)\\n (except (jerboa runtime)\")\n\n ;; Add (std misc trie) import to completion.sls for command completion cache\n (patch-file! \"src/jsh/completion.sls\"\n \"(except (jerboa runtime)\"\n \"(std misc trie)\\n (except (jerboa runtime)\")\n\n ;; Add trie-based PATH command completion cache to completion.sls\n (patch-file! \"src/jsh/completion.sls\"\n \"(define (complete-command prefix env)\"\n (string-append\n \";; Trie-based command completion cache — rebuilt when PATH changes\\n\"\n \" (define *cmd-trie* #f)\\n\"\n \" (define *cmd-trie-path* #f)\\n\"\n \" (define (ensure-cmd-trie! env)\\n\"\n \" (let ([current-path (or (env-get env \\\"PATH\\\") \\\"\\\")])\\n\"\n \" (unless (equal? current-path *cmd-trie-path*)\\n\"\n \" (set! *cmd-trie-path* current-path)\\n\"\n \" (let ([t (make-trie)])\\n\"\n \" ;; PATH executables\\n\"\n \" (let ([path-dirs (string-split-path current-path)])\\n\"\n \" (for-each\\n\"\n \" (lambda (dir)\\n\"\n \" (with-catch\\n\"\n \" (lambda (e) (void))\\n\"\n \" (lambda ()\\n\"\n \" (when (file-exists? dir)\\n\"\n \" (for-each\\n\"\n \" (lambda (name)\\n\"\n \" (let ([full-path (string-append dir \\\"/\\\" name)])\\n\"\n \" (when (executable? full-path)\\n\"\n \" (trie-insert! t name))))\\n\"\n \" (directory-files dir))))))\\n\"\n \" path-dirs))\\n\"\n \" (set! *cmd-trie* t)))\\n\"\n \" *cmd-trie*))\\n\"\n \" (define (complete-command prefix env)\"))\n\n ;; Replace PATH executable scanning with trie lookup in complete-command\n (patch-file! \"src/jsh/completion.sls\"\n (string-append\n \" ;; PATH executables\\n\"\n \" (let ((path-dirs (string-split-path (or (env-get env \\\"PATH\\\") \\\"\\\"))))\\n\"\n \" (for-each\\n\"\n \" (lambda (dir)\\n\"\n \" (with-catch\\n\"\n \" (lambda (e) #!void)\\n\"\n \" (lambda ()\\n\"\n \" (when (file-exists? dir)\\n\"\n \" (for-each\\n\"\n \" (lambda (name)\\n\"\n \" (when (and (string-prefix-match? prefix name)\\n\"\n \" (not (member name results)))\\n\"\n \" (let ((full-path (string-append dir \\\"/\\\" name)))\\n\"\n \" (when (executable? full-path)\\n\"\n \" (set! results (cons name results))))))\\n\"\n \" (directory-files dir))))))\\n\"\n \" path-dirs))\")\n (string-append\n \" ;; PATH executables via trie cache\\n\"\n \" (let ([trie (ensure-cmd-trie! env)])\\n\"\n \" (for-each\\n\"\n \" (lambda (name)\\n\"\n \" (unless (member name results)\\n\"\n \" (set! results (cons name results))))\\n\"\n \" (trie-prefix-search trie prefix)))\"))\n\n ;; Add PATH lookup cache to util.sls (which-cached) using LRU cache\n (patch-file! \"src/jsh/util.sls\"\n \"string-last-index-of which find-file-in-path executable?\"\n \"string-last-index-of which which-cached which-cache-invalidate!\\n find-file-in-path executable?\")\n\n (patch-file! \"src/jsh/util.sls\"\n \"(def (find-file-in-path\"\n (string-append\n \";; PATH lookup cache — LRU bounded cache (bash command_hash equivalent)\\n\"\n \" (define *which-cache* (make-lru-cache 256))\\n\"\n \" (define *which-cache-path* #f)\\n\"\n \" (define (which-cache-invalidate!)\\n\"\n \" (lru-cache-clear! *which-cache*)\\n\"\n \" (set! *which-cache-path* #f))\\n\"\n \" (define (which-cached name)\\n\"\n \" (if (string-contains? name \\\"/\\\")\\n\"\n \" (which name)\\n\"\n \" (let ([current-path (or (getenv \\\"PATH\\\" #f) \\\"/usr/bin:/bin\\\")])\\n\"\n \" (unless (equal? current-path *which-cache-path*)\\n\"\n \" (lru-cache-clear! *which-cache*)\\n\"\n \" (set! *which-cache-path* current-path))\\n\"\n \" (let ([cached (lru-cache-get *which-cache* name #f)])\\n\"\n \" (or cached\\n\"\n \" (let ([found (which name)])\\n\"\n \" (when found (lru-cache-put! *which-cache* name found))\\n\"\n \" found))))))\\n\"\n \" ;; Validate a file path: reject null bytes that would truncate at C level\\n\"\n \" (define (validate-file-path path who)\\n\"\n \" (let loop ([i 0])\\n\"\n \" (when (< i (string-length path))\\n\"\n \" (when (char=? (string-ref path i) #\\\\nul)\\n\"\n \" (error who (string-append path \\\": path contains null byte\\\")))\\n\"\n \" (loop (+ i 1))))\\n\"\n \" path)\\n\"\n \" (def (find-file-in-path\"))\n\n ;; Replace (which cmd-name) with (which-cached cmd-name) in executor.sls\n ;; Only apply when executor.sls was freshly generated this session\n (let ([path \"src/jsh/executor.sls\"])\n (when (or force-rebuild? (compiled-this-session? path))\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [old \"(which cmd-name)\"]\n [new \"(which-cached cmd-name)\"]\n [olen (string-length old)]\n [nlen (string-length new)])\n (let loop ([i 0] [result \"\"])\n (cond\n ((> (+ i olen) (string-length content))\n (let ([final (string-append result (substring content i (string-length content)))])\n (call-with-output-file path\n (lambda (p) (display final p))\n 'replace)\n (printf \" Patched executor.sls: which -> which-cached~n\")))\n ((string=? (substring content i (+ i olen)) old)\n (loop (+ i olen) (string-append result new)))\n (else\n (loop (+ i 1) (string-append result (substring content i (+ i 1))))))))))\n\n ;; env-get single-pass: eliminate double hash-table scan in env-get\n (patch-file! \"src/jsh/environment.sls\"\n (string-append\n \" [else\\n\"\n \" (let ([resolved (resolve-nameref name env)])\\n\"\n \" (let ([var (find-var-in-chain env resolved)])\\n\"\n \" (if (and var (shell-var-nameref? var))\\n\"\n \" #f\\n\"\n \" (env-get-chain env resolved))))])\")\n (string-append\n \" [else\\n\"\n \" (let ([resolved (resolve-nameref name env)])\\n\"\n \" (let ([var (find-var-in-chain env resolved)])\\n\"\n \" (cond\\n\"\n \" [(not var) (getenv resolved #f)]\\n\"\n \" [(shell-var-nameref? var) #f]\\n\"\n \" [else (shell-var-scalar-value var)])))])\"))\n\n ;; Add *command-trace-hook* parameter to environment.sls — used by ,debug\n (patch-file! \"src/jsh/environment.sls\"\n \"(export *execute-input* *arith-eval-fn*\"\n \"(export *execute-input* *command-trace-hook* *arith-eval-fn*\")\n\n (patch-file! \"src/jsh/environment.sls\"\n \"(define *execute-input* (make-parameter #f))\"\n \"(define *execute-input* (make-parameter #f))\\n (define *command-trace-hook* (make-parameter #f))\")\n\n ;; Call *command-trace-hook* after each command executes in execute-shell-lines\n (patch-file! \"src/jsh/script.sls\"\n \" (env-set-last-status! env new-status)\"\n (string-append\n \" (let ([__cth (*command-trace-hook*)])\\n\"\n \" (when __cth\\n\"\n \" (guard (__e [#t (%%void)])\\n\"\n \" (__cth cmd new-status env))))\\n\"\n \" (env-set-last-status! env new-status)\")))\n\n;; --- main.sls patches: wire *current-jsh-env* for meta-commands ---\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n ;; Add harden-startup! call early in main (after init-smp!, before user input)\n (patch-file! \"src/jsh/main.sls\"\n \"(*gambit-scheduler-wfd* (ffi-gambit-scheduler-wfd))\\n (let* ([args-hash (parse-args args)])\"\n \"(*gambit-scheduler-wfd* (ffi-gambit-scheduler-wfd))\\n (harden-startup!)\\n (let* ([args-hash (parse-args args)])\")\n\n ;; Rename all gsh references to jsh throughout all generated files\n (sed-replace \"s/\\\\*gsh-/\\\\*jsh-/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\"gsh: /\\\"jsh: /g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\"gsh -/\\\"jsh -/g\" \"src/jsh/*.sls\")\n (sed-replace \"s|/bin/gsh|/bin/jsh|g\" \"src/jsh/*.sls\")\n (sed-replace \"s/GSH_VERSION/JSH_VERSION/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/GSH_PROCESSORS/JSH_PROCESSORS/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/GSH_ENV/JSH_ENV/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/Jerboa Shell/jsh/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gshrc/.jshrc/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gsh_profile/.jsh_profile/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gsh_login/.jsh_login/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gsh_logout/.jsh_logout/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/\\\\.gsh_history/.jsh_history/g\" \"src/jsh/*.sls\")\n (sed-replace \"s/GSH_EXE/JSH_EXE/g\" \"src/jsh/*.sls\")\n (display \" Patched all .sls files (gsh → jsh)\\n\"))\n\n;; --- Embed support: patch startup.sls and script.sls ---\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n (display \"\\n--- Embed patches ---\\n\")\n\n ;; Add (jsh embed) import to startup.sls\n (patch-file! \"src/jsh/startup.sls\"\n \"(jsh environment) (jsh script) (jsh recorder))\"\n \"(jsh environment) (jsh script) (jsh recorder) (jsh embed))\")\n\n ;; Patch startup.sls: source embedded .jshrc before filesystem .jshrc\n ;; Login shell\n (patch-file! \"src/jsh/startup.sls\"\n \"(source-if-exists! \\\"/etc/profile\\\" env)\"\n \"(source-if-exists! \\\"//embed/.jsh_profile\\\" env)\\n (source-if-exists! \\\"/etc/profile\\\" env)\")\n\n ;; Interactive shell: source embedded .jshrc, then filesystem .jshrc\n ;; Note: target uses .jshrc because gsh→jsh sed rename runs before embed patches\n (patch-file! \"src/jsh/startup.sls\"\n \"(source-if-exists! (string-append home \\\"/.jshrc\\\") env))]\"\n \"(source-if-exists! \\\"//embed/.jshrc\\\" env)\\n (source-if-exists! (string-append home \\\"/.jshrc\\\") env))]\")\n\n ;; Patch source-if-exists! to handle //embed/ paths\n ;; For embed paths, read content via embed module and execute directly\n ;; (source-file! doesn't know about embed, so we bypass it)\n (patch-file! \"src/jsh/startup.sls\"\n \"(define (source-if-exists! path env)\\n (if (file-exists? path)\\n (begin (source-file! path env) #t)\\n #f))\"\n \"(define (source-if-exists! path env)\\n (cond\\n [(embed-path? path)\\n (cond\\n [(not (embed-unlocked?))\\n #f]\\n [else\\n (let ([content (embed-file->string path)])\\n (if content\\n (begin (execute-string (strip-shebang content) env) #t)\\n #f))])]\\n [(file-exists? path)\\n (begin (source-file! path env) #t)]\\n [else #f]))\")\n\n ;; embed-ls / embed-cat / embed-cp / embed-fd builtins and the (jsh embed)\n ;; import are now defined directly in main.ss — no patch needed here.\n\n ;; Add (jsh embed) import to script.sls\n (patch-file! \"src/jsh/script.sls\"\n \"(jsh registry))\"\n \"(jsh registry)\\n (jsh embed))\")\n\n ;; Patch source-file! in script.sls to handle //embed/ paths\n ;; This fixes ALL callers (source builtin, startup, etc.)\n (patch-file! \"src/jsh/script.sls\"\n \"(define (source-file! filename env)\\n (if (not (file-exists? filename))\"\n (string-append\n \"(define (source-file! filename env)\\n\"\n \" (if (embed-path? filename)\\n\"\n \" (let ([content (embed-file->string filename)])\\n\"\n \" (if content\\n\"\n \" (guard (__exn\\n\"\n \" [#t\\n\"\n \" ((lambda (e)\\n\"\n \" (cond\\n\"\n \" [(break-exception? e) (raise e)]\\n\"\n \" [(continue-exception? e) (raise e)]\\n\"\n \" [(return-exception? e) (return-exception-status e)]\\n\"\n \" [(errexit-exception? e) (raise e)]\\n\"\n \" [(subshell-exit-exception? e) (raise e)]\\n\"\n \" [(nounset-exception? e) (raise e)]\\n\"\n \" [else\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: ~a: ~a~n\\\" filename (exception-message e))\\n\"\n \" 1]))\\n\"\n \" __exn)])\\n\"\n \" (execute-string (strip-shebang content) env))\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: ~a: No such embedded file~n\\\" filename)\\n\"\n \" 1)))\\n\"\n \" (if (not (file-exists? filename))\"))\n\n ;; Close the extra outer if we added to source-file!\n (patch-file! \"src/jsh/script.sls\"\n \"(parameterize ([*current-source-file* filename])\\n (execute-string script-content env)))))))\"\n \"(parameterize ([*current-source-file* filename])\\n (execute-string script-content env))))))))\")\n\n ;; Add (jsh embed) import to executor.sls\n (patch-file! \"src/jsh/executor.sls\"\n \"(jsh arithmetic) (jsh ffi) (jsh glob) (jsh signals))\"\n \"(jsh arithmetic) (jsh ffi) (jsh glob) (jsh signals)\\n (jsh embed))\")\n\n ;; Add embed-rewrite-arg helper to executor.sls (after execute-external definition begins)\n ;; Rewrites //embed/ paths to /proc/self/fd/N via memfd, so external commands can read them.\n ;; Uses embed-register-fd!/embed-cleanup-fds! from (jsh embed) for fd tracking.\n (patch-file! \"src/jsh/executor.sls\"\n \"(define (execute-external cmd-name args env)\"\n (string-append\n \"(define (embed-rewrite-arg arg)\\n\"\n \" (if (embed-path? arg)\\n\"\n \" (let ([fd-path (embed-file->fd-path arg)])\\n\"\n \" (if fd-path\\n\"\n \" (begin\\n\"\n \" (embed-register-fd!\\n\"\n \" (string->number\\n\"\n \" (substring fd-path 14 (string-length fd-path))))\\n\"\n \" fd-path)\\n\"\n \" arg))\\n\"\n \" arg))\\n\"\n \" (define (execute-external cmd-name args env)\"))\n\n ;; Patch pack-with-soh call to rewrite embed args\n (patch-file! \"src/jsh/executor.sls\"\n \"(let* ([packed-argv (pack-with-soh\\n (map string->c-safe\\n (cons cmd-name args)))])\"\n \"(let* ([packed-argv (pack-with-soh\\n (map string->c-safe\\n (map embed-rewrite-arg\\n (cons cmd-name args))))])\")\n\n ;; Add embed memfd fds to keep-fds so child process inherits them\n (patch-file! \"src/jsh/executor.sls\"\n \"(let* ([keep-fds (pack-fds-with-soh\\n (*active-redirect-fds*))])\"\n \"(let* ([keep-fds (pack-fds-with-soh\\n (append (embed-active-fds)\\n (*active-redirect-fds*)))])\")\n\n ;; Close memfds in parent after child exits.\n ;; The child inherits its own fd table, so these parent fds are only\n ;; for bookkeeping. Closed after wait + sigchld-unblock.\n (patch-file! \"src/jsh/executor.sls\"\n \"(ffi-sigchld-unblock)\\n (if stopped?\"\n \"(ffi-sigchld-unblock)\\n (embed-cleanup-fds!)\\n (if stopped?\")\n ;; Clear decrypted plaintext cache and zero derived key on exit.\n ;; Patching run-exit-trap! covers all exit paths (EOF, exit builtin, -c, stdin).\n (patch-file! \"src/jsh/main.sls\"\n \"(define (run-exit-trap! env)\\n (let ([action (trap-get \\\"EXIT\\\")])\\n (when (and action (string? action))\\n (trap-set! \\\"EXIT\\\" 'default)\\n (execute-input action env))))\"\n \"(define (run-exit-trap! env)\\n (embed-clear-cache!)\\n (guard (e [#t (void)])\\n ((foreign-procedure \\\"jerboa_ssh_agent_stop\\\" () void)))\\n (let ([action (trap-get \\\"EXIT\\\")])\\n (when (and action (string? action))\\n (trap-set! \\\"EXIT\\\" 'default)\\n (execute-input action env))))\")\n\n) ;; end (let () ...) for embed patches\n\n;; --- Recording patches ---\n(display \"\\n--- Recording patches ---\\n\")\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n ;; 1. Add (jsh recorder) import to main.sls\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh embed))\"\n \"(jsh embed)\\n (jsh recorder))\")\n\n ;; 2. Patch REPL to emit input recording after line-edit\n ;; NOTE: After the 12ade70 commit, history-add! is wrapped in (when (*jsh-history-enabled*) ...)\n ;; so the patch target has an extra ) closing that wrapper.\n (patch-file! \"src/jsh/main.sls\"\n \"(history-add! expanded))\\n (when execute?\"\n \"(history-add! expanded))\\n (when (*recording?*)\\n (recorder-input! (string-append expanded \\\"\\\\n\\\")))\\n (when execute?\")\n\n ;; 3. Patch REPL to emit output recording — record prompt display\n ;; NOTE: After 12ade70, line-edit args are split across lines (Chez formatting)\n (patch-file! \"src/jsh/main.sls\"\n \"(let* ([input (line-edit\\n prompt-str\\n complete-fn\\n edit-mode)])\"\n \"(let* ([input (begin (when (*recording?*) (recorder-output! prompt-str))\\n (line-edit\\n prompt-str\\n complete-fn\\n edit-mode))])\")\n\n ;; 4. Patch execute-input to emit command and exit-status events\n ;; NOTE: After 12ade70, (let ([status...]) is indented 2 more spaces + includes (__exn\n (patch-file! \"src/jsh/main.sls\"\n \"(when execute?\\n (let ([status (guard (__exn\"\n \"(when execute?\\n (when (*recording?*)\\n (recorder-command! expanded (or (env-get env \\\"PWD\\\") \\\".\\\")))\\n (let ([status (guard (__exn\")\n\n ;; 5. Patch after execute to record exit status + duration\n ;; NOTE: After 12ade70, indented 31 spaces (was 29)\n (patch-file! \"src/jsh/main.sls\"\n \"(env-set-last-status! env status)\\n (process-traps! env)\"\n \"(env-set-last-status! env status)\\n (when (*recording?*)\\n (recorder-exit-status! status))\\n (process-traps! env)\")\n\n ;; 6. Patch run-exit-trap! to stop recording on exit\n (patch-file! \"src/jsh/main.sls\"\n \"(define (run-exit-trap! env)\\n (embed-clear-cache!)\"\n \"(define (run-exit-trap! env)\\n (when (*recording?*) (recorder-stop!))\\n (embed-clear-cache!)\")\n\n ;; 7. Add (jsh recorder) import to executor.sls\n (patch-file! \"src/jsh/executor.sls\"\n \"(jsh redirect) (jsh control)\"\n \"(jsh redirect) (jsh recorder) (jsh control)\")\n\n ;; 8. Patch SIGWINCH to record resize events\n (patch-file! \"src/jsh/main.sls\"\n \"[(string=? sig-name \\\"WINCH\\\") (%%void)]\"\n \"[(string=? sig-name \\\"WINCH\\\")\\n (when (*recording?*)\\n (recorder-resize! (ffi-terminal-columns 1) (ffi-terminal-rows 1)))\\n (%%void)]\")\n\n ;; 9. Add PTY output capture to recorder.sls — captures command stdout/stderr\n ;; 9a. Add capture state parameters after existing parameters\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define *recorder-flush-threshold* (make-parameter 32))\"\n \"(define *recorder-flush-threshold* (make-parameter 32))\\n (define *recorder-pty-master* (make-parameter -1))\\n (define *recorder-saved-fd1* (make-parameter -1))\\n (define *recorder-saved-fd2* (make-parameter -1))\\n (define *recorder-capture-thread* (make-parameter #f))\")\n\n ;; 9b. Add capture functions before recorder-elapsed\n ;; NOTE: recorder-start-capture! is called AFTER (*recording?* #t) so the\n ;; forked thread inherits *recording?*=#t and can use recorder-output!.\n ;; The capture loop calls recorder-flush! after each event because the\n ;; thread-local buffer would otherwise never reach threshold and be lost.\n ;; Uses ffi-bv-write (write()) not ffi-bv-send (send()) — PTY fds not sockets.\n ;; Sets PTY slave to cfmakeraw to eliminate line discipline interference.\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define (recorder-elapsed)\"\n \"(define (recorder-start-capture!)\\n (guard (e [#t (void)])\\n (when (= 1 (ffi-isatty 0))\\n (let-values ([(master slave) (ffi-pty-open)])\\n (let ([cols (ffi-terminal-columns 1)]\\n [rows (ffi-terminal-rows 1)])\\n (when (and (> cols 0) (> rows 0))\\n (ffi-pty-set-size slave cols rows)))\\n (ffi-set-pty-raw slave)\\n (let ([saved-1 (ffi-dup 1)]\\n [saved-2 (ffi-dup 2)])\\n (ffi-dup2 slave 1)\\n (ffi-dup2 slave 2)\\n (ffi-close-fd slave)\\n (ffi-set-nonblock master)\\n (*recorder-pty-master* master)\\n (*recorder-saved-fd1* saved-1)\\n (*recorder-saved-fd2* saved-2)\\n (let ([thread (fork-thread\\n (lambda ()\\n (recorder-capture-loop master saved-1 saved-2)))])\\n (*recorder-capture-thread* thread)))))))\\n (define (recorder-capture-loop master-fd real-fd1 real-fd2)\\n (let ([buf (make-bytevector 4096)])\\n (let loop ()\\n (let ([n (guard (e [#t -1])\\n (ffi-bv-read-nonblock master-fd buf 0 4096))])\\n (cond\\n [(> n 0)\\n (let ([data (make-bytevector n)])\\n (bytevector-copy! buf 0 data 0 n)\\n (guard (e [#t (void)])\\n (ffi-bv-write real-fd1 data 0 n))\\n (guard (e [#t (void)])\\n (let ([str (guard (e [#t\\n (let ([s (make-string n)])\\n (do ([i 0 (+ i 1)])\\n ((= i n) s)\\n (string-set! s i\\n (integer->char\\n (bytevector-u8-ref data i)))))])\\n (utf8->string data))])\\n (recorder-output! str)\\n (recorder-flush!))))\\n (loop)]\\n [(= n 0)\\n (ffi-nanosleep-us 500)\\n (loop)]\\n [else (void)])))))\\n (define (recorder-stop-capture!)\\n (let ([saved-1 (*recorder-saved-fd1*)]\\n [saved-2 (*recorder-saved-fd2*)]\\n [master (*recorder-pty-master*)])\\n (when (> saved-1 0)\\n (ffi-dup2 saved-1 1)\\n (ffi-close-fd saved-1)\\n (*recorder-saved-fd1* -1))\\n (when (> saved-2 0)\\n (ffi-dup2 saved-2 2)\\n (ffi-close-fd saved-2)\\n (*recorder-saved-fd2* -1))\\n (when (> master 0)\\n (guard (e [#t (void)])\\n (ffi-close-fd master))\\n (*recorder-pty-master* -1))\\n (*recorder-capture-thread* #f)))\\n (define (recorder-elapsed)\")\n\n ;; 9c. Add recorder-start-capture! in all 3 arms of recorder-start!\n ;; Each call patches the next unpatched occurrence (patch-file! finds first match)\n (patch-file! \"src/jsh/recorder.sls\"\n \"(*recorder-buffer-count* 0)\\n (when stream-url\"\n \"(*recorder-buffer-count* 0)\\n (recorder-start-capture!)\\n (when stream-url\")\n (patch-file! \"src/jsh/recorder.sls\"\n \"(*recorder-buffer-count* 0)\\n (when stream-url\"\n \"(*recorder-buffer-count* 0)\\n (recorder-start-capture!)\\n (when stream-url\")\n ;; Third arm has different indentation (10 spaces vs 13)\n (patch-file! \"src/jsh/recorder.sls\"\n \"(*recorder-buffer-count* 0)\\n (when stream-url\"\n \"(*recorder-buffer-count* 0)\\n (recorder-start-capture!)\\n (when stream-url\")\n\n ;; 9f. Add recorder-stop-capture! call in recorder-stop!\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define (recorder-stop!)\\n (when (*recording?*)\\n (recorder-flush!)\"\n \"(define (recorder-stop!)\\n (when (*recording?*)\\n (recorder-stop-capture!)\\n (recorder-flush!)\")\n\n ;; 10. Fix export-script in player.sls to show command output, not just commands\n (patch-file! \"src/jsh/player.sls\"\n \"(define (export-script filename)\"\n \"(define (strip-ansi str)\\n (let ([len (string-length str)])\\n (let loop ([i 0] [acc '()])\\n (cond\\n [(>= i len)\\n (list->string (reverse acc))]\\n [(and (char=? (string-ref str i) #\\\\x1b)\\n (< (+ i 1) len))\\n (let ([next (string-ref str (+ i 1))])\\n (cond\\n [(char=? next #\\\\[)\\n (let skip ([j (+ i 2)])\\n (cond\\n [(>= j len) (loop j acc)]\\n [(char-alphabetic? (string-ref str j))\\n (loop (+ j 1) acc)]\\n [else (skip (+ j 1))]))]\\n [else (loop (+ i 2) acc)]))]\\n [(char=? (string-ref str i) #\\\\return)\\n (loop (+ i 1) acc)]\\n [else\\n (loop (+ i 1) (cons (string-ref str i) acc))]))))\\n (define (extract-json-field data field)\\n (let ([pos (str-contains data (string-append \\\"\\\\\\\"\\\" field \\\"\\\\\\\":\\\\\\\"\\\" ))])\\n (if pos\\n (let ([start (+ pos (+ 4 (string-length field)))])\\n (let sloop ([i start])\\n (cond\\n [(>= i (string-length data)) (substring data start i)]\\n [(and (char=? (string-ref data i) #\\\\\\\\)\\n (< (+ i 1) (string-length data)))\\n (sloop (+ i 2))]\\n [(char=? (string-ref data i) #\\\\\\\") (substring data start i)]\\n [else (sloop (+ i 1))])))\\n #f)))\\n (define (export-script filename)\")\n\n ;; Rename old export-script so it's kept but unused, then add new one after it\n (patch-file! \"src/jsh/player.sls\"\n \"(define (export-script filename)\"\n \"(define (export-script-old filename)\")\n\n ;; Add new export-script that shows commands AND output, right before list-recordings\n (patch-file! \"src/jsh/player.sls\"\n \"(define (list-recordings)\"\n \"(define (export-script filename)\\n (if (not (file-exists? filename))\\n (begin\\n (fprintf (current-error-port) \\\"record export: ~a: No such file~n\\\" filename)\\n 1)\\n (call-with-input-file filename\\n (lambda (port)\\n (let ([header (get-line port)])\\n (fprintf (current-output-port) \\\"#!/bin/sh~n\\\")\\n (fprintf (current-output-port)\\n \\\"# Exported from: ~a~n~n\\\"\\n (path-strip-directory filename))\\n (let loop ([in-cmd? #f])\\n (let ([line (get-line port)])\\n (if (eof-object? line)\\n 0\\n (if (= 0 (string-length line))\\n (loop in-cmd?)\\n (let-values ([(ts typ data) (parse-event-line line)])\\n (cond\\n [(and ts (string=? typ \\\"c\\\"))\\n (let ([cmd (extract-json-field data \\\"cmd\\\")])\\n (when cmd\\n (fprintf (current-output-port) \\\"$ ~a~n\\\" cmd)))\\n (loop #t)]\\n [(and ts (string=? typ \\\"o\\\") in-cmd?)\\n (let ([clean (strip-ansi data)])\\n (when (> (string-length clean) 0)\\n (display clean (current-output-port))))\\n (loop #t)]\\n [(and ts (string=? typ \\\"x\\\"))\\n (let ([status (extract-json-field data \\\"status\\\")])\\n (when (and status (not (string=? status \\\"0\\\")))\\n (fprintf (current-output-port)\\n \\\"# exit: ~a~n\\\" status)))\\n (loop #f)]\\n [else (loop in-cmd?)]))))))))))) (define (list-recordings)\")\n\n ;; ---- Auto-Record with Encrypted Sessions ----\n\n ;; 10a. Rename console-logs → .jsh/logs in auto-generated recorder.sls and player.sls\n (let ()\n (define (str-replace-all s old new)\n (let ([old-len (string-length old)]\n [slen (string-length s)])\n (let loop ([start 0] [acc '()])\n (let ([rest (substring s start slen)])\n (let ([idx (string-find rest old)])\n (if idx\n (loop (+ start idx old-len)\n (cons new (cons (substring s start (+ start idx)) acc)))\n (apply string-append\n (reverse (cons rest acc)))))))))\n (define (replace-all-in-file! path old new)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [updated (str-replace-all content old new)])\n (unless (string=? content updated)\n (call-with-output-file path\n (lambda (p) (display updated p))\n 'replace)\n (printf \" Patched ~a (replace-all ~s → ~s)~n\" path old new))))\n (when (or force-rebuild? (compiled-this-session? \"src/jsh/recorder.sls\"))\n (replace-all-in-file! \"src/jsh/recorder.sls\" \"ensure-console-logs-dir!\" \"ensure-jsh-logs-dir!\")\n (replace-all-in-file! \"src/jsh/recorder.sls\" \"/console-logs\" \"/.jsh/logs\"))\n (when (or force-rebuild? (compiled-this-session? \"src/jsh/player.sls\"))\n (replace-all-in-file! \"src/jsh/player.sls\" \"/console-logs\" \"/.jsh/logs\")))\n\n ;; 10b. Fix ensure-jsh-logs-dir! to create parent ~/.jsh/ before ~/.jsh/logs/\n (patch-file! \"src/jsh/recorder.sls\"\n \"(unless (file-exists? dir)\\n (guard (__exn [#t ((lambda (e) #f) __exn)]) (mkdir dir)))\"\n \"(unless (file-exists? dir)\\n (let ([parent (string-append (or (getenv \\\"HOME\\\" #f) \\\"/tmp\\\") \\\"/.jsh\\\")])\\n (unless (file-exists? parent)\\n (guard (__exn [#t ((lambda (e) #f) __exn)]) (mkdir parent))))\\n (guard (__exn [#t ((lambda (e) #f) __exn)]) (mkdir dir)))\")\n\n ;; 11. Add encryption exports and embed-data import to recorder.sls\n (patch-file! \"src/jsh/recorder.sls\"\n \"recorder-duration! recorder-status)\"\n \"recorder-duration! recorder-status\\n *recorder-encrypt?* recorder-ensure-record-key!)\")\n (patch-file! \"src/jsh/recorder.sls\"\n \"(jsh recording-index)\"\n \"(jsh recording-index) (only (jsh embed-data) %record-pubkey)\")\n\n ;; 12. Add *recorder-encrypt?* parameter and in-memory encryption after capture thread.\n ;; Public key is baked into the binary at build time (%record-pubkey from embed-data).\n ;; Private key is in //embed/record.key — only accessible after ,unlock.\n ;; SECURITY: Recording data is NEVER written to disk as plaintext. All data is\n ;; accumulated in an in-memory string port and only written as encrypted .cast.enc.\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define *recorder-capture-thread* (make-parameter #f))\"\n (string-append\n \"(define *recorder-capture-thread* (make-parameter #f))\\n\"\n \" (define *recorder-encrypt?* (make-parameter #f))\\n\"\n \"\\n\"\n \" ;; Load public key from the binary. If present, enable encryption.\\n\"\n \" (define (recorder-ensure-record-key!)\\n\"\n \" (unless (*recorder-encrypt?*)\\n\"\n \" (when %record-pubkey\\n\"\n \" (*recorder-encrypt?* #t))))\\n\"\n \"\\n\"\n \" ;; Encrypt in-memory recording data -> .cast.enc using sealed-box.\\n\"\n \" ;; No plaintext .cast file is ever created on disk.\\n\"\n \" (define (recorder-encrypt-and-write! cast-data enc-path)\\n\"\n \" (when (and %record-pubkey (> (string-length cast-data) 0))\\n\"\n \" (let* ((plaintext (string->utf8 cast-data))\\n\"\n \" (encrypted (ffi-sealed-box-encrypt %record-pubkey plaintext)))\\n\"\n \" (when encrypted\\n\"\n \" (call-with-port\\n\"\n \" (open-file-output-port enc-path\\n\"\n \" (file-options no-fail) (buffer-mode block))\\n\"\n \" (lambda (p) (put-bytevector p encrypted)))\\n\"\n \" (chmod enc-path #o600)\\n\"\n \" enc-path))))\"))\n\n ;; 12b. Patch recorder-start! to use in-memory port — no plaintext .cast on disk.\n ;; Replace (open-output-file file) with (open-output-string) so all recording\n ;; data stays in RAM until encrypted and written as .cast.enc on stop.\n (patch-file! \"src/jsh/recorder.sls\"\n \"(let* ((file (or filename (generate-recording-filename)))\\n (port (open-output-file file)))\"\n \"(unless (*recorder-encrypt?*)\\n (error 'recorder-start! \\\"encryption key not available — refusing to record unencrypted\\\"))\\n (let* ((file (or filename (generate-recording-filename)))\\n (port (open-output-string)))\")\n\n ;; 12c. Patch generate-recording-filename to produce .cast.enc path\n (patch-file! \"src/jsh/recorder.sls\"\n \"(string-append dir \\\"/\\\" ts \\\".cast\\\"))\"\n \"(string-append dir \\\"/\\\" ts \\\".cast.enc\\\"))\")\n\n ;; 13. Modify recorder-output! to feed into SQLite index for searchability\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define (recorder-output! str) (recorder-emit! \\\"o\\\" str))\"\n \"(define (recorder-output! str)\\n (recorder-emit! \\\"o\\\" str)\\n ;; Feed output into the SQLite index for searchability\\n (guard (__exn [#t ((lambda (e) #f) __exn)])\\n (when (recording-db-ready?)\\n (recording-index-output! str))))\")\n\n ;; 14. Modify recorder-stop! to encrypt in-memory data and write .cast.enc\n ;; The port is an in-memory string port — get the accumulated data, encrypt,\n ;; and write directly to .cast.enc. No plaintext .cast file ever exists.\n (patch-file! \"src/jsh/recorder.sls\"\n \"(let ((port (*recorder-port*)))\\n (when port\\n (flush-output-port port)\\n (close-output-port port)))\"\n \"(let ((port (*recorder-port*)))\\n (when port\\n (let ((cast-data (get-output-string port)))\\n (when (and (*recorder-encrypt?*) (*recorder-file*))\\n (guard (__exn [#t ((lambda (e) #f) __exn)])\\n (recorder-encrypt-and-write! cast-data (*recorder-file*)))))))\")\n\n ;; 15. Add decrypt-cast-file export to player.sls\n (patch-file! \"src/jsh/player.sls\"\n \"session-report export-script list-recordings)\"\n \"session-report export-script list-recordings\\n decrypt-cast-file)\")\n\n ;; 16. (jsh ffi) import already present — Jerboa source imports :jsh/ffi\n\n ;; 17. Add decrypt-cast-file function to player.sls before strip-ansi\n ;; Private key is in //embed/record.key — requires ,unlock first.\n ;; Also need to add (jsh embed) import to player.sls for embed-file-ref.\n ;; Add (jsh embed) import to player.sls\n (patch-file! \"src/jsh/player.sls\"\n \"(std actor core)\"\n \"(std actor core)\\n (only (jsh embed) embed-file-ref embed-unlocked?)\")\n (patch-file! \"src/jsh/player.sls\"\n \"(define (strip-ansi str)\"\n (string-append\n \"(define (decrypt-cast-file enc-path)\\n\"\n \" (and (file-exists? enc-path)\\n\"\n \" (embed-unlocked?)\\n\"\n \" (let ([privkey (guard (e [#t #f]) (embed-file-ref \\\"record.key\\\"))])\\n\"\n \" (when privkey\\n\"\n \" (let* ([encrypted (call-with-port (open-file-input-port enc-path)\\n\"\n \" get-bytevector-all)]\\n\"\n \" [decrypted (ffi-sealed-box-decrypt privkey encrypted)])\\n\"\n \" (when decrypted\\n\"\n \" (let ([tmp (string-append enc-path \\\".tmp\\\")])\\n\"\n \" (call-with-port\\n\"\n \" (open-file-output-port tmp\\n\"\n \" (file-options no-fail) (buffer-mode block))\\n\"\n \" (lambda (p) (put-bytevector p decrypted)))\\n\"\n \" tmp)))))))\\n\"\n \" (define (strip-ansi str)\"))\n\n ;; 18. Modify find-cast-files in player.sls to include .cast.enc files\n (patch-file! \"src/jsh/player.sls\"\n \"(str-ends-with? f \\\".cast\\\")\"\n \"(or (str-ends-with? f \\\".cast\\\") (str-ends-with? f \\\".cast.enc\\\"))\")\n\n ;; 19. Add auto-record startup in main.sls repl function\n ;; Target: after ffi-termios-save in the repl function, before edit-mode let\n (patch-file! \"src/jsh/main.sls\"\n \"(when (= (ffi-isatty 0) 1) (ffi-termios-save 0 0))\\n (let ([edit-mode\"\n \"(when (= (ffi-isatty 0) 1) (ffi-termios-save 0 0))\\n ;; Auto-record every interactive session\\n (guard (__exn\\n [#t ((lambda (e)\\n (fprintf (current-error-port)\\n \\\"jsh: autorecord failed: ~a~n\\\"\\n (if (message-condition? e)\\n (condition-message e)\\n e)))\\n __exn)])\\n (recorder-ensure-record-key!)\\n (recorder-start!))\\n (let ([edit-mode\")\n\n ;; 20. Add explicit cleanup on interactive exit path\n (patch-file! \"src/jsh/main.sls\"\n \"(when (*recording?*) (recorder-stop!))\\n (embed-clear-cache!)\"\n \"(when (*recording?*) (recorder-stop!))\\n (guard (__exn [#t ((lambda (e) #f) __exn)]) (ffi-record-zero-key))\\n (embed-clear-cache!)\")\n\n ;; 21. Fix SIGWINCH: update recorder PTY size so child processes (top, vim, etc.) see correct terminal dimensions\n (patch-file! \"src/jsh/recorder.sls\"\n \"(define (recorder-resize! cols rows)\\n (when (and (*recording?*) (not (*recording-paused?*)))\"\n \"(define (recorder-resize! cols rows)\\n ;; Update PTY size so child processes see the new terminal dimensions\\n (let ([master (*recorder-pty-master*)])\\n (when (> master 0)\\n (ffi-pty-set-size master cols rows)))\\n (when (and (*recording?*) (not (*recording-paused?*)))\")\n\n ;; 22. Ensure fds 0/1/2 are open at the top of main — when started via ffi-fork-exec\n ;; (e.g. ,server), all fds are closed before exec. Without this, code that runs\n ;; before the --server handler (init-smp!, harden-startup!, init-shell-env) crashes\n ;; because Chez Scheme's runtime can't write to stderr.\n (patch-file! \"src/jsh/main.sls\"\n \" (define (main . args)\\n (init-smp!)\"\n \" (define (main . args)\\n (ffi-ensure-std-fds)\\n (init-smp!)\")\n\n ;; 23. Set _git_branch variable before each prompt (reads .git/HEAD, no external commands)\n (patch-file! \"src/jsh/main.sls\"\n \"(let* ([ps1 (or (env-get env \\\"PS1\\\") \\\"$ \\\")])\"\n (string-append\n \";; Set _git_branch before prompt expansion\\n\"\n \" (let ([__branch (guard (__e [#t #f]) (git-branch-name))])\\n\"\n \" (if __branch\\n\"\n \" (env-set! env \\\"_git_branch\\\" (string-append \\\" (\\\" __branch \\\")\\\"))\\n\"\n \" (env-set! env \\\"_git_branch\\\" \\\"\\\")))\\n\"\n \" (let* ([ps1 (or (env-get env \\\"PS1\\\") \\\"$ \\\")])\"))\n\n ;; 23. Add (jsh prompt) import to main.sls for git-branch-name\n ;; NOTE: Must match text BEFORE mux patches add (jsh mux-server)\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh embed)\\n (jsh recorder))\"\n \"(jsh embed)\\n (only (jsh prompt) git-branch-name)\\n (jsh recorder))\")\n\n ;; ---- Prompt: \\g git branch escape is in local prompt.ss override ----\n\n ;; ---- Multiplexer (Phase 1) ----\n\n ;; 1. Add (jsh mux-server) and (jsh mux-client) imports to main.sls\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh recorder))\"\n \"(jsh recorder)\\n (jsh mux-server)\\n (jsh mux-client))\")\n\n ;; Add (jsh harden) import to main.sls (after mux imports)\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh mux-client))\"\n \"(jsh mux-client)\\n (jsh harden))\")\n\n ;; 1b. Add default #f entries for mux keys in parse-args hash\n (patch-file! \"src/jsh/main.sls\"\n \"(hash-put! ht 'args (list))\"\n \"(hash-put! ht 'args (list))\\n (hash-put! ht 'server #f)\\n (hash-put! ht 'attach #f)\\n (hash-put! ht 'list-servers #f)\\n (hash-put! ht 'mux-name #f)\\n (hash-put! ht 'listen-port #f)\\n (hash-put! ht 'mux-cert #f)\\n (hash-put! ht 'mux-key #f)\\n (hash-put! ht 'mux-ca #f)\\n (hash-put! ht 'mux-remote #f)\\n (hash-put! ht 'verbose #f)\")\n\n ;; 2. Add --server, --attach, --list-servers and TLS flags to parse-args\n (patch-file! \"src/jsh/main.sls\"\n \"[(string=? (car args) \\\"-c\\\")\"\n \"[(string=? (car args) \\\"--server\\\")\\n (hash-put! result 'server #t)\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--attach\\\")\\n (hash-put! result 'attach #t)\\n (loop (cdr args))]\\n [(or (string=? (car args) \\\"-A\\\"))\\n (hash-put! result 'attach #t)\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--list-servers\\\")\\n (hash-put! result 'list-servers #t)\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--name\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-name (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--listen-port\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'listen-port (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--cert\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-cert (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--key\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-key (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--ca\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-ca (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--remote\\\")\\n (when (pair? (cdr args))\\n (hash-put! result 'mux-remote (cadr args))\\n (set! args (cdr args)))\\n (loop (cdr args))]\\n [(string=? (car args) \\\"--verbose\\\")\\n (hash-put! result 'verbose #t)\\n (loop (cdr args))]\\n [(string=? (car args) \\\"-c\\\")\")\n\n ;; 3. Dispatch to server/client/list before the normal command/script/repl dispatch\n ;; Server password comes ONLY from _JSH_MUX_PW env var (set in-process by ,server).\n ;; --password is NEVER accepted on the CLI (would leak via ps/pgrep).\n ;; Attach password is NEVER accepted either — the protocol prompts interactively.\n (patch-file! \"src/jsh/main.sls\"\n \"(cond\\n [command\"\n \"(cond\\n [(hash-ref args-hash 'server)\\n (when (hash-ref args-hash 'verbose)\\n (let ([logpath (string-append (or (getenv \\\"HOME\\\") \\\"/tmp\\\") \\\"/.jsh/mux-server.log\\\")])\\n (putenv \\\"MUX_SERVER_DEBUG\\\" logpath)\\n (fprintf (current-error-port) \\\" verbose log: ~a~n\\\" logpath)))\\n (guard (e [#t (exit 1)])\\n (let* ([name (or (hash-ref args-hash 'mux-name) \\\"default\\\")]\\n [pw (let ([v (getenv \\\"_JSH_MUX_PW\\\")])\\n (putenv \\\"_JSH_MUX_PW\\\" \\\"\\\")\\n (and v (> (string-length v) 0) v))]\\n [listen-port (hash-ref args-hash 'listen-port)]\\n [cert (hash-ref args-hash 'mux-cert)]\\n [key (hash-ref args-hash 'mux-key)]\\n [ca (hash-ref args-hash 'mux-ca)])\\n (if listen-port\\n (let ([port (string->number listen-port)])\\n (if ca\\n (mux-server-start-tcp name port cert key pw ca)\\n (mux-server-start-tcp name port cert key pw)))\\n (if pw\\n (mux-server-start name pw)\\n (mux-server-start name)))))\\n (exit 0)]\\n [(hash-ref args-hash 'list-servers)\\n (mux-list-servers)\\n (exit 0)]\\n [(hash-ref args-hash 'attach)\\n (guard (e [#t (exit 1)])\\n (let ([name (or (hash-ref args-hash 'mux-name) \\\"default\\\")]\\n [remote (hash-ref args-hash 'mux-remote)])\\n (if remote\\n (let* ([parts (let ([colon (let loop ([i (- (string-length remote) 1)])\\n (cond [(< i 0) #f]\\n [(char=? (string-ref remote i) #\\\\:) i]\\n [else (loop (- i 1))]))])\\n (if colon\\n (cons (substring remote 0 colon)\\n (string->number (substring remote (+ colon 1) (string-length remote))))\\n (cons remote 443)))]\\n [host (car parts)]\\n [port (cdr parts)])\\n (mux-client-attach-remote host port))\\n (mux-client-attach name))))\\n (exit 0)]\\n [command\")\n\n ;; 4. Add \"mux\" builtin to register-late-builtins! (after embed-fd)\n ;; The function ends with embed-fd's closing: 1)))))))\n ;; We change the 7th ) to 6 parens (close embed-fd), add mux builtin, then close the function.\n (patch-file! \"src/jsh/main.sls\"\n \" 1)))))))\\n (define (init-shell-env args-hash)\"\n (string-append\n \" 1))))))\\n\"\n \" ;; ---- mux — multiplexer control ----\\n\"\n \" (builtin-register! \\\"mux\\\"\\n\"\n \" (lambda (args env)\\n\"\n \" (if (null? args)\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"usage: mux <command> [options]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux server [--name NAME]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux server -p PORT[,PORT...] [--name NAME]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux attach [--name NAME]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux attach HOST[:PORT]~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux list~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"~nCerts/keys come from //embed/keys/. Passwords are prompted~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"interactively (never passed on the command line).~n\\\")\\n\"\n \" 2)\\n\"\n \" (let ([subcmd (car args)]\\n\"\n \" [rest (cdr args)])\\n\"\n \" (cond\\n\"\n \" [(string=? subcmd \\\"server\\\")\\n\"\n \" (mux-cmd-server rest)]\\n\"\n \" [(string=? subcmd \\\"attach\\\")\\n\"\n \" (mux-cmd-attach rest)]\\n\"\n \" [(string=? subcmd \\\"list\\\")\\n\"\n \" (mux-list-servers) 0]\\n\"\n \" [else\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux: unknown subcommand '~a'~n\\\" subcmd) 2]))))))\\n\"\n \" ;; Parse --flag VALUE pairs from an arg list into an alist.\\n\"\n \" ;; Bare --flag (no value or next arg is also a flag) is stored as (flag . #t).\\n\"\n \" ;; -p PORT is shorthand for --listen PORT.\\n\"\n \" ;; Positional args with '.' or ':' are treated as remote targets.\\n\"\n \" (define (mux-parse-flags args)\\n\"\n \" (let loop ([args args] [result '()])\\n\"\n \" (cond\\n\"\n \" [(null? args) result]\\n\"\n \" [(and (string=? (car args) \\\"-p\\\") (pair? (cdr args)))\\n\"\n \" (loop (cddr args) (cons (cons \\\"listen\\\" (cadr args)) result))]\\n\"\n \" [(and (> (string-length (car args)) 2)\\n\"\n \" (char=? (string-ref (car args) 0) #\\\\-)\\n\"\n \" (char=? (string-ref (car args) 1) #\\\\-))\\n\"\n \" (let ([flag (substring (car args) 2 (string-length (car args)))])\\n\"\n \" (if (and (pair? (cdr args))\\n\"\n \" (or (= (string-length (cadr args)) 0)\\n\"\n \" (not (char=? (string-ref (cadr args) 0) #\\\\-))))\\n\"\n \" (loop (cddr args) (cons (cons flag (cadr args)) result))\\n\"\n \" (loop (cdr args) (cons (cons flag #t) result))))]\\n\"\n \" [(let ([a (car args)])\\n\"\n \" (or (string-contains a \\\".\\\") (string-contains a \\\":\\\")))\\n\"\n \" (loop (cdr args) (cons (cons \\\"remote\\\" (car args)) result))]\\n\"\n \" [else (loop (cdr args) result)])))\\n\"\n \" (define (mux-flag-ref flags key default)\\n\"\n \" (let ([pair (assoc key flags)])\\n\"\n \" (if pair (cdr pair) default)))\\n\"\n \" ;; Resolve //embed/ path to /proc/self/fd/N via memfd, or return path as-is\\n\"\n \" (define (resolve-embed-path path who)\\n\"\n \" (if (embed-path? path)\\n\"\n \" (let ([fd-path (embed-file->fd-path path)])\\n\"\n \" (unless fd-path\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux ~a: embedded file not found: ~a~n\\\" who path)\\n\"\n \" (error 'mux \\\"embedded file not found\\\" path))\\n\"\n \" fd-path)\\n\"\n \" path))\\n\"\n \" (define (mux-parse-ports str)\\n\"\n \" (let loop ([s str] [acc '()])\\n\"\n \" (let ([comma (string-contains s \\\",\\\")])\\n\"\n \" (if comma\\n\"\n \" (loop (substring s (+ comma 1) (string-length s))\\n\"\n \" (cons (substring s 0 comma) acc))\\n\"\n \" (reverse (cons s acc))))))\\n\"\n \" (define (mux-validate-port str)\\n\"\n \" (let ([p (string->number str)])\\n\"\n \" (if (and p (fixnum? p) (> p 0) (< p 65536))\\n\"\n \" p\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux server: invalid port '~a'~n\\\" str)\\n\"\n \" (error 'mux \\\"invalid port\\\" str)))))\\n\"\n \" (define (mux-cmd-server args)\\n\"\n \" (let* ([flags (mux-parse-flags args)]\\n\"\n \" [name (mux-flag-ref flags \\\"name\\\" \\\"default\\\")]\\n\"\n \" [listen-port (mux-flag-ref flags \\\"listen\\\" #f)]\\n\"\n \" [cert-path \\\"//embed/keys/cert.pem\\\"]\\n\"\n \" [key-path \\\"//embed/keys/key.pem\\\"])\\n\"\n \" ;; Password is never read from CLI (leaks via ps). When auth is\\n\"\n \" ;; required, _JSH_MUX_PW (set in-process by ,server) is consumed;\\n\"\n \" ;; otherwise the server runs unauthenticated.\\n\"\n \" (let ([pw (let ([v (getenv \\\"_JSH_MUX_PW\\\")])\\n\"\n \" (putenv \\\"_JSH_MUX_PW\\\" \\\"\\\")\\n\"\n \" (and v (> (string-length v) 0) v))])\\n\"\n \" (cond\\n\"\n \" [listen-port\\n\"\n \" (let ([ports (map mux-validate-port (mux-parse-ports listen-port))])\\n\"\n \" (let ([real-cert (resolve-embed-path cert-path \\\"server\\\")]\\n\"\n \" [real-key (resolve-embed-path key-path \\\"server\\\")])\\n\"\n \" (guard (e [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux server: ~a~n\\\"\\n\"\n \" (if (message-condition? e) (condition-message e) (format \\\"~a\\\" e)))\\n\"\n \" 1])\\n\"\n \" (mux-server-start-tcp name ports real-cert real-key pw)\\n\"\n \" 0)))]\\n\"\n \" [else\\n\"\n \" (guard (e [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux server: ~a~n\\\"\\n\"\n \" (if (message-condition? e) (condition-message e) (format \\\"~a\\\" e)))\\n\"\n \" 1])\\n\"\n \" (if pw\\n\"\n \" (mux-server-start name pw)\\n\"\n \" (mux-server-start name))\\n\"\n \" 0)]))))\\n\"\n \" (define (mux-cmd-attach args)\\n\"\n \" (let* ([flags (mux-parse-flags args)]\\n\"\n \" [name (mux-flag-ref flags \\\"name\\\" \\\"default\\\")]\\n\"\n \" [remote (mux-flag-ref flags \\\"remote\\\" #f)]\\n\"\n \" [cert-path \\\"//embed/keys/cert.pem\\\"]\\n\"\n \" [key-path \\\"//embed/keys/key.pem\\\"]\\n\"\n \" [mtls? (assoc \\\"mtls\\\" flags)]\\n\"\n \" [ws? (assoc \\\"ws\\\" flags)])\\n\"\n \" ;; Password is NEVER taken from CLI (leaks via ps). The client\\n\"\n \" ;; prompts interactively when the server requests auth.\\n\"\n \" (cond\\n\"\n \" [remote\\n\"\n \" (let-values ([(host port) (mux-parse-host-port remote 443)])\\n\"\n \" (guard (e [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux attach: ~a~n\\\"\\n\"\n \" (if (message-condition? e) (condition-message e) (format \\\"~a\\\" e)))\\n\"\n \" 1])\\n\"\n \" (cond\\n\"\n \" [(and mtls? ws?)\\n\"\n \" (let ([rc (resolve-embed-path cert-path \\\"attach\\\")]\\n\"\n \" [rk (resolve-embed-path key-path \\\"attach\\\")])\\n\"\n \" (mux-client-attach-remote-ws host port rc rk rc #f))]\\n\"\n \" [mtls?\\n\"\n \" (let ([rc (resolve-embed-path cert-path \\\"attach\\\")]\\n\"\n \" [rk (resolve-embed-path key-path \\\"attach\\\")])\\n\"\n \" (mux-client-attach-remote-mtls host port rc rk rc #f))]\\n\"\n \" [ws?\\n\"\n \" (mux-client-attach-remote-ws host port #f)]\\n\"\n \" [else\\n\"\n \" (mux-client-attach-remote host port)])\\n\"\n \" 0))]\\n\"\n \" [else\\n\"\n \" (guard (e [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux attach: ~a~n\\\"\\n\"\n \" (if (message-condition? e) (condition-message e) (format \\\"~a\\\" e)))\\n\"\n \" 1])\\n\"\n \" (mux-client-attach name)\\n\"\n \" 0)])))\\n\"\n \" ;; Parse \\\"host:port\\\" or \\\"host\\\" (defaulting port)\\n\"\n \" (define (mux-parse-host-port str default-port)\\n\"\n \" (let ([colon-pos (let loop ([i (- (string-length str) 1)])\\n\"\n \" (cond\\n\"\n \" [(< i 0) #f]\\n\"\n \" [(char=? (string-ref str i) #\\\\:) i]\\n\"\n \" [else (loop (- i 1))]))])\\n\"\n \" (if colon-pos\\n\"\n \" (let ([host (substring str 0 colon-pos)]\\n\"\n \" [port-str (substring str (+ colon-pos 1) (string-length str))])\\n\"\n \" (let ([port (string->number port-str)])\\n\"\n \" (if (and port (fixnum? port) (> port 0) (< port 65536))\\n\"\n \" (values host port)\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: invalid port '~a' in '~a'~n\\\" port-str str)\\n\"\n \" (error 'mux \\\"invalid port\\\")))))\\n\"\n \" (values str default-port))))\\n\"\n \" (define (init-shell-env args-hash)\"))\n\n) ;; end (let () ...) for recording + mux patches\n\n;; --- mux gen-certs subcommand ---\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n;; Add \"gen-certs\" to mux help text\n(patch-file! \"src/jsh/main.sls\"\n \" \\\" mux list~n\\\")\"\n (string-append\n \" \\\" mux list~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" mux gen-certs [--ip IP] [--days N] [--cert PATH] [--key PATH]~n\\\")\"))\n\n;; Add \"gen-certs\" dispatch case\n(patch-file! \"src/jsh/main.sls\"\n \" [(string=? subcmd \\\"list\\\")\\n (mux-list-servers) 0]\"\n (string-append\n \" [(string=? subcmd \\\"list\\\")\\n\"\n \" (mux-list-servers) 0]\\n\"\n \" [(string=? subcmd \\\"gen-certs\\\")\\n\"\n \" (mux-cmd-gen-certs rest)]\"))\n\n;; Add mux-cmd-gen-certs handler (before mux-parse-host-port)\n(patch-file! \"src/jsh/main.sls\"\n \" ;; Parse \\\"host:port\\\" or \\\"host\\\" (defaulting port)\"\n (string-append\n \" ;; --- mux gen-certs: generate self-signed TLS certs ---\\n\"\n \" (define c-x509-generate\\n\"\n \" (foreign-procedure \\\"jerboa_x509_generate_self_signed\\\"\\n\"\n \" (u8* size_t int u8* size_t u8* size_t) int))\\n\"\n \" (define c-x509-fingerprint\\n\"\n \" (foreign-procedure \\\"jerboa_x509_cert_fingerprint\\\"\\n\"\n \" (u8* size_t u8* size_t) int))\\n\"\n \" (define c-last-error\\n\"\n \" (foreign-procedure \\\"jerboa_last_error\\\" (u8* size_t) size_t))\\n\"\n \" (define (x509-last-error)\\n\"\n \" (let ([buf (make-bytevector 1024)])\\n\"\n \" (let ([len (c-last-error buf 1024)])\\n\"\n \" (if (> len 0)\\n\"\n \" (utf8->string (let ([out (make-bytevector (min len 1023))])\\n\"\n \" (bytevector-copy! buf 0 out 0 (min len 1023)) out))\\n\"\n \" \\\"unknown error\\\"))))\\n\"\n \" (define (x509-fingerprint cert-path)\\n\"\n \" (let ([cert-bv (string->utf8 cert-path)]\\n\"\n \" [out (make-bytevector 32)])\\n\"\n \" (let ([rc (c-x509-fingerprint cert-bv (bytevector-length cert-bv) out 32)])\\n\"\n \" (if (< rc 0) \\\"??\\\" (bytevector->hex-string out)))))\\n\"\n \" (define (bytevector->hex-string bv)\\n\"\n \" (let* ([len (bytevector-length bv)]\\n\"\n \" [out (make-string (* len 2))])\\n\"\n \" (do ([i 0 (+ i 1)]) ((= i len) out)\\n\"\n \" (let* ([b (bytevector-u8-ref bv i)]\\n\"\n \" [hi (bitwise-arithmetic-shift-right b 4)]\\n\"\n \" [lo (bitwise-and b #xf)])\\n\"\n \" (string-set! out (* i 2) (string-ref \\\"0123456789abcdef\\\" hi))\\n\"\n \" (string-set! out (+ (* i 2) 1) (string-ref \\\"0123456789abcdef\\\" lo))))))\\n\"\n \" (define (x509-ensure-parent-dirs! path)\\n\"\n \" (let ([idx (let loop ([i (- (string-length path) 1)])\\n\"\n \" (cond [(< i 0) #f]\\n\"\n \" [(char=? (string-ref path i) #\\\\/) i]\\n\"\n \" [else (loop (- i 1))]))])\\n\"\n \" (when idx\\n\"\n \" (let ([dir (substring path 0 idx)])\\n\"\n \" (unless (or (string=? dir \\\"\\\") (file-exists? dir))\\n\"\n \" (guard (e [#t (void)]) (mkdir dir)))))))\\n\"\n \" (define (mux-cmd-gen-certs args)\\n\"\n \" (let* ([flags (mux-parse-flags args)]\\n\"\n \" [ip (mux-flag-ref flags \\\"ip\\\" \\\"0.0.0.0\\\")]\\n\"\n \" [days-str (mux-flag-ref flags \\\"days\\\" \\\"365\\\")]\\n\"\n \" [cert-path (mux-flag-ref flags \\\"cert\\\"\\n\"\n \" (string-append (or (getenv \\\"HOME\\\") \\\".\\\") \\\"/.embed/keys/cert.pem\\\"))]\\n\"\n \" [key-path (mux-flag-ref flags \\\"key\\\"\\n\"\n \" (string-append (or (getenv \\\"HOME\\\") \\\".\\\") \\\"/.embed/keys/key.pem\\\"))])\\n\"\n \" (let ([days (or (string->number days-str) 365)])\\n\"\n \" (x509-ensure-parent-dirs! cert-path)\\n\"\n \" (x509-ensure-parent-dirs! key-path)\\n\"\n \" (let* ([ip-bv (string->utf8 ip)]\\n\"\n \" [cert-bv (string->utf8 cert-path)]\\n\"\n \" [key-bv (string->utf8 key-path)]\\n\"\n \" [rc (c-x509-generate ip-bv (bytevector-length ip-bv)\\n\"\n \" days cert-bv (bytevector-length cert-bv)\\n\"\n \" key-bv (bytevector-length key-bv))])\\n\"\n \" (cond\\n\"\n \" [(< rc 0)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: mux gen-certs: ~a~n\\\" (x509-last-error))\\n\"\n \" 1]\\n\"\n \" [else\\n\"\n \" (let ([fp (x509-fingerprint cert-path)])\\n\"\n \" (printf \\\"Certificate generated:~n\\\")\\n\"\n \" (printf \\\" cert: ~a~n\\\" cert-path)\\n\"\n \" (printf \\\" key: ~a~n\\\" key-path)\\n\"\n \" (printf \\\" ip: ~a~n\\\" ip)\\n\"\n \" (printf \\\" days: ~a~n\\\" days)\\n\"\n \" (printf \\\" fingerprint: ~a~n\\\" fp)\\n\"\n \" 0)])))))\\n\"\n \" ;; Parse \\\"host:port\\\" or \\\"host\\\" (defaulting port)\"))\n\n) ;; end (let () ...) for mux gen-certs patches\n\n;; --- Security hardening patches ---\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n ;; Replace all occurrences of a string in a file\n (define (patch-file-all! path old new)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [olen (string-length old)]\n [nlen (string-length new)])\n (let loop ([i 0] [result \"\"])\n (cond\n ((> (+ i olen) (string-length content))\n (let ([final (string-append result (substring content i (string-length content)))])\n (call-with-output-file path\n (lambda (p) (display final p))\n 'replace)))\n ((string=? (substring content i (+ i olen)) old)\n (loop (+ i olen) (string-append result new)))\n (else\n (loop (+ i 1) (string-append result (substring content i (+ i 1)))))))))\n\n (display \"\\n--- Security hardening patches ---\\n\")\n\n ;; 1. Randomize process substitution FIFO names with CSPRNG\n ;; Replace predictable /tmp/jsh-procsub-{pid}-{counter} with random hex\n (patch-file! \"src/jsh/expander.sls\"\n (string-append\n \"(define (make-procsub-fifo!)\\n\"\n \" (set! *procsub-counter* (+ *procsub-counter* 1))\\n\"\n \" (let ([path (string-append\\n\"\n \" \\\"/tmp/jsh-procsub-\\\"\\n\"\n \" (number->string (ffi-getpid))\\n\"\n \" \\\"-\\\"\\n\"\n \" (number->string *procsub-counter*))])\\n\"\n \" (let ([rc (ffi-mkfifo path 384)])\\n\"\n \" (when (< rc 0) (error 'jerboa \\\"mkfifo failed\\\" path))\\n\"\n \" path)))\")\n (string-append\n \"(define (random-hex-string n)\\n\"\n \" ;; Generate a hex string of 2*n characters from n random bytes\\n\"\n \" (let ([bv (make-bytevector n)])\\n\"\n \" (if (ffi-embed-random-bytes bv)\\n\"\n \" (let ([out (make-string (* n 2))])\\n\"\n \" (let loop ([i 0])\\n\"\n \" (if (>= i n) out\\n\"\n \" (let* ([b (bytevector-u8-ref bv i)]\\n\"\n \" [hi (bitwise-arithmetic-shift-right b 4)]\\n\"\n \" [lo (bitwise-and b #xf)])\\n\"\n \" (string-set! out (* i 2)\\n\"\n \" (string-ref \\\"0123456789abcdef\\\" hi))\\n\"\n \" (string-set! out (+ (* i 2) 1)\\n\"\n \" (string-ref \\\"0123456789abcdef\\\" lo))\\n\"\n \" (loop (+ i 1))))))\\n\"\n \" ;; Fallback if /dev/urandom unavailable\\n\"\n \" (string-append (number->string (ffi-getpid)) \\\"-\\\"\\n\"\n \" (number->string *procsub-counter*)))))\\n\"\n \" (define (make-procsub-fifo!)\\n\"\n \" (set! *procsub-counter* (+ *procsub-counter* 1))\\n\"\n \" (let ([path (string-append\\n\"\n \" \\\"/tmp/jsh-procsub-\\\"\\n\"\n \" (random-hex-string 16))])\\n\"\n \" (let ([rc (ffi-mkfifo path #o600)])\\n\"\n \" (when (< rc 0) (error 'jerboa \\\"mkfifo failed\\\" path))\\n\"\n \" path)))\"))\n\n ;; 2. Add validate-file-path to util.sls for null-byte rejection\n (patch-file! \"src/jsh/util.sls\"\n \"shell-display shell-display-raw file-regular?\"\n (string-append\n \"shell-display shell-display-raw validate-file-path file-regular?\"))\n\n ;; 3. Add null-byte validation to redirect-fd-to-file!\n (patch-file! \"src/jsh/redirect.sls\"\n \"(define (redirect-fd-to-file! fd filename flags mode)\\n (let ([raw-fd (ffi-open-raw filename flags mode)])\"\n \"(define (redirect-fd-to-file! fd filename flags mode)\\n (validate-file-path filename 'redirect)\\n (let ([raw-fd (ffi-open-raw filename flags mode)])\")\n\n ;; 4. Add null-byte validation to source builtin in main.sls\n (patch-file! \"src/jsh/main.sls\"\n \"(let* ([filename (car args)])\\n (let* ([filepath (if (string-contains?\"\n \"(let* ([filename (validate-file-path (car args) 'source)])\\n (let* ([filepath (if (string-contains?\")\n\n ;; 5. Expand sandbox import in main.sls + add conditions import\n ;; Use (except ...) to avoid duplicate definitions — (jsh functions) already\n ;; exports the exception types that (jsh conditions) re-exports.\n (patch-file! \"src/jsh/main.sls\"\n \"(only (jsh sandbox) *current-jsh-env*)\"\n \"(jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\")\n\n ;; 6. Add sandbox builtin registration in main.sls\n (patch-file! \"src/jsh/main.sls\"\n \" ;; Embed builtins\"\n (string-append\n \" ;; Sandbox builtin — runs commands under Landlock + seccomp + timeout\\n\"\n \" (builtin-register! \\\"sandbox\\\"\\n\"\n \" (lambda (args env)\\n\"\n \" (if (null? args)\\n\"\n \" (begin\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: usage: sandbox [opts] -c 'cmd' | script~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -r PATH allow read -w PATH allow write~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -x PATH allow exec -t MS timeout~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" --net allow network --no-net deny network~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -c CMD run command~n\\\")\\n\"\n \" 2)\\n\"\n \" (guard (exn\\n\"\n \" [(jsh-sandbox-error? exn)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: ~a: ~a~n\\\"\\n\"\n \" (jsh-sandbox-error-phase exn)\\n\"\n \" (jsh-sandbox-error-detail exn))\\n\"\n \" 1]\\n\"\n \" [(jsh-path-security-error? exn)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: ~a: ~a~n\\\"\\n\"\n \" (jsh-path-security-error-path exn)\\n\"\n \" (jsh-path-security-error-reason exn))\\n\"\n \" 1]\\n\"\n \" [#t\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: ~a~n\\\"\\n\"\n \" (if (message-condition? exn)\\n\"\n \" (condition-message exn)\\n\"\n \" (format \\\"~a\\\" exn)))\\n\"\n \" 1])\\n\"\n \" (let* ([parsed (parse-sb-args args)]\\n\"\n \" [script (sb-parsed-script parsed)]\\n\"\n \" [cmd (sb-parsed-cmd parsed)]\\n\"\n \" [opts (sb-parsed-opts parsed)])\\n\"\n \" (cond\\n\"\n \" [cmd\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda () (run-cmd cmd)))]\\n\"\n \" [script\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda () (run-script script)))]\\n\"\n \" [else\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: sandbox: no command or script specified~n\\\")\\n\"\n \" 2]))))))\\n\"\n \" ;; Embed builtins\"))\n\n ;; 7-9. Sandbox patches are applied directly to src/jsh/sandbox.sls\n ;; (hand-written file, not auto-generated)\n\n ;; ========== Round 2: Additional security hardening ==========\n\n ;; 10. Seccomp FFI bindings are in ffi.sls directly (hand-written file)\n\n ;; 11-14. Sandbox, seccomp, safe-eval patches applied directly to src/jsh/sandbox.sls\n ;; (hand-written file, not auto-generated)\n\n ;; 12. Guardian-based FD/FIFO leak detection in main.sls\n ;; Track FD opens/closes and warn on leaks in the REPL loop\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\"\n (string-append\n \"(jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\\n\"\n \" ;; Leak detection\\n\"))\n\n (patch-file! \"src/jsh/main.sls\"\n \"(define (main args)\"\n (string-append\n \";; ========== Guardian-based resource leak detection ==========\\n\"\n \" (define *fd-guardian* (make-guardian))\\n\"\n \" (define *fd-tracker* (make-hashtable equal-hash equal?))\\n\\n\"\n \" ;; Register an FD with the guardian for leak tracking.\\n\"\n \" ;; info: descriptive string (e.g., \\\"redirect /tmp/foo\\\" or \\\"procsub FIFO\\\")\\n\"\n \" (define (track-fd! fd info)\\n\"\n \" (let ([token (cons fd info)])\\n\"\n \" (hashtable-set! *fd-tracker* fd token)\\n\"\n \" (*fd-guardian* token)))\\n\\n\"\n \" ;; Mark an FD as properly closed (suppress guardian warning)\\n\"\n \" (define (untrack-fd! fd)\\n\"\n \" (hashtable-delete! *fd-tracker* fd))\\n\\n\"\n \" ;; Poll the guardian for leaked FDs and log warnings\\n\"\n \" (define (poll-fd-leaks!)\\n\"\n \" (let loop ([leaked 0])\\n\"\n \" (let ([token (*fd-guardian*)])\\n\"\n \" (if token\\n\"\n \" (let ([fd (car token)] [info (cdr token)])\\n\"\n \" (when (hashtable-contains? *fd-tracker* fd)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"jsh: warning: leaked fd ~a (~a)~n\\\" fd info)\\n\"\n \" (hashtable-delete! *fd-tracker* fd))\\n\"\n \" (loop (+ leaked 1)))\\n\"\n \" leaked))))\\n\\n\"\n \" (define (main args)\"))\n\n ;; 13. SQL safety validation is applied directly to src/jsh/recording-index.sls\n ;; (hand-written file, not auto-generated — no patch needed here)\n\n ;; 14. Add --eval flag to sandbox builtin for restricted eval\n ;; Safe eval with a minimal binding allowlist\n (patch-file! \"src/jsh/main.sls\"\n \" (fprintf (current-error-port)\\n \\\" -c CMD run command~n\\\")\"\n (string-append\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -c CMD run command~n\\\")\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\" -e EXPR eval expression in restricted env~n\\\")\"))\n\n ;; sandbox.sls -e flag, safe-eval, and seccomp are applied directly\n ;; (hand-written file, not auto-generated — no patch needed here)\n\n ;; Wire -e flag in the sandbox builtin (main.sls)\n (patch-file! \"src/jsh/main.sls\"\n \" (cond\\n [cmd\\n (jsh-sandbox-run opts\\n (lambda () (run-cmd cmd)))]\\n [script\\n (jsh-sandbox-run opts\\n (lambda () (run-script script)))]\\n [else\"\n (string-append\n \" (cond\\n\"\n \" [(eq? script 'eval)\\n\"\n \" ;; Safe eval mode: evaluate in restricted environment\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda ()\\n\"\n \" (let ([result (jsh-safe-eval cmd)])\\n\"\n \" (when result (fprintf (current-output-port) \\\"~a~n\\\" result))\\n\"\n \" 0)))]\\n\"\n \" [cmd\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda () (run-cmd cmd)))]\\n\"\n \" [script\\n\"\n \" (jsh-sandbox-run opts\\n\"\n \" (lambda () (run-script script)))]\\n\"\n \" [else\"))\n\n ;; jsh-safe-eval export is in sandbox.sls directly (hand-written)\n\n (display \" Security patches applied (round 1 + round 2)\\n\")\n) ;; end security patches\n\n;; --- Stdlib integration patches ---\n;; Integrate jerboa stdlib modules: terminal, custodian, profile, memoize, config, fmt\n(display \"\\n--- Post-build: Applying stdlib integration patches ---\\n\")\n(let ()\n (define (string-find haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n ((> (+ i nlen) hlen) #f)\n ((string=? (substring haystack i (+ i nlen)) needle) i)\n (else (loop (+ i 1)))))))\n (define (patch-file! path old new)\n ;; Skip if this file was not freshly generated this session\n (if (not (or force-rebuild? (compiled-this-session? path)))\n (begin (printf \" (skip) ~a~n\" path) #f)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))]\n [clen (string-length content)])\n (cond\n ;; Skip if new text is already present (idempotent)\n [(string-find content new)\n (printf \" (skip) ~a~n\" path)\n #f]\n ;; Apply patch: replace first occurrence of old with new\n [(string-find content old)\n => (lambda (idx)\n (call-with-output-file path\n (lambda (p)\n (display (substring content 0 idx) p)\n (display new p)\n (display (substring content (+ idx (string-length old))\n clen) p))\n 'replace)\n (printf \" Patched ~a~n\" path)\n #t)]\n [else\n (printf \" (skip) ~a~n\" path)\n #f]))))\n\n ;; --- 1. Terminal stdlib in lineedit.sls ---\n ;; Add (std misc terminal) import\n (patch-file! \"src/jsh/lineedit.sls\"\n \"(except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\"\n \"(only (std misc terminal) cursor-up cursor-down cursor-forward cursor-back\\n clear-screen clear-line clear-to-end)\\n (except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\")\n ;; Replace hand-rolled ANSI escape functions with stdlib calls\n (patch-file! \"src/jsh/lineedit.sls\"\n (string-append\n \"(define (term-clear-to-eol port)\\n\"\n \" (display (string-append ESC-STR \\\"[K\\\") port))\\n\"\n \" (define (term-clear-line port)\\n\"\n \" (display (string-append \\\"\\\\r\\\" ESC-STR \\\"[K\\\") port))\\n\"\n \" (define (term-clear-screen port)\\n\"\n \" (display (string-append ESC-STR \\\"[H\\\" ESC-STR \\\"[2J\\\") port))\\n\"\n \" (define (term-move-up n port)\\n\"\n \" (when (> n 0) (fprintf port \\\"~a[~aA\\\" ESC-STR n)))\\n\"\n \" (define (term-move-down n port)\\n\"\n \" (when (> n 0) (fprintf port \\\"~a[~aB\\\" ESC-STR n)))\\n\"\n \" (define (term-cursor-forward n port)\\n\"\n \" (when (> n 0) (fprintf port \\\"~a[~aC\\\" ESC-STR n)))\\n\"\n \" (define (term-cursor-back n port)\\n\"\n \" (when (> n 0) (fprintf port \\\"~a[~aD\\\" ESC-STR n)))\")\n (string-append\n \"(define (term-clear-to-eol port)\\n\"\n \" (parameterize ([current-output-port port]) (clear-to-end)))\\n\"\n \" (define (term-clear-line port)\\n\"\n \" (parameterize ([current-output-port port]) (clear-line)))\\n\"\n \" (define (term-clear-screen port)\\n\"\n \" (parameterize ([current-output-port port]) (clear-screen)))\\n\"\n \" (define (term-move-up n port)\\n\"\n \" (when (> n 0) (parameterize ([current-output-port port]) (cursor-up n))))\\n\"\n \" (define (term-move-down n port)\\n\"\n \" (when (> n 0) (parameterize ([current-output-port port]) (cursor-down n))))\\n\"\n \" (define (term-cursor-forward n port)\\n\"\n \" (when (> n 0) (parameterize ([current-output-port port]) (cursor-forward n))))\\n\"\n \" (define (term-cursor-back n port)\\n\"\n \" (when (> n 0) (parameterize ([current-output-port port]) (cursor-back n))))\"))\n\n ;; --- 2. Custodian safety net in pipeline.sls ---\n ;; Add (std misc custodian) import\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\"\n \"(only (std misc custodian) make-custodian custodian-register!\\n custodian-shutdown-all)\\n (except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\")\n ;; Add *pipeline-custodian* parameter and custodian-aware make-pipes\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(define (make-pipes n)\"\n (string-append\n \"(define *pipeline-custodian* (make-parameter #f))\\n\"\n \" (define (pipeline-custodian-cleanup!)\\n\"\n \" (let ([c (*pipeline-custodian*)])\\n\"\n \" (when c (custodian-shutdown-all c))))\\n\"\n \" (define (make-pipes n)\"))\n ;; Register each pipe FD pair with the custodian after creation\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(loop (+ i 1) (cons (list read-fd write-fd) pipes))))\"\n (string-append\n \"(let ([pair (list read-fd write-fd)])\\n\"\n \" (let ([c (*pipeline-custodian*)])\\n\"\n \" (when c\\n\"\n \" (custodian-register! c pair\\n\"\n \" (lambda (p)\\n\"\n \" (when (>= (car p) 0)\\n\"\n \" (guard (__exn [#t (void __exn)])\\n\"\n \" (ffi-close-fd (car p)))\\n\"\n \" (set-car! p -1))\\n\"\n \" (when (>= (cadr p) 0)\\n\"\n \" (guard (__exn [#t (void __exn)])\\n\"\n \" (ffi-close-fd (cadr p)))\\n\"\n \" (set-car! (cdr p) -1))))))\\n\"\n \" (loop (+ i 1) (cons pair pipes)))))\"))\n ;; Wrap 3-arg branch: add custodian parameterize around pipeline body\n (patch-file! \"src/jsh/pipeline.sls\"\n \" [(commands env execute-fn)\\n (let* ([pipe-types #f])\\n (let ([ptypes (or pipe-types\\n (make-list (- (length commands) 1) 'PIPE))])\\n (parameterize ([*procsub-cleanups* (list)])\"\n \" [(commands env execute-fn)\\n (let* ([pipe-types #f])\\n (let ([ptypes (or pipe-types\\n (make-list (- (length commands) 1) 'PIPE))])\\n (parameterize ([*procsub-cleanups* (list)]\\n [*pipeline-custodian* (make-custodian)])\")\n ;; Add custodian cleanup after run-procsub-cleanups in 3-arg branch\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(ffi-sigchld-unblock)\\n (run-procsub-cleanups!)\\n exit-codes)))))))))))))))))]\"\n \"(ffi-sigchld-unblock)\\n (run-procsub-cleanups!)\\n (pipeline-custodian-cleanup!)\\n exit-codes)))))))))))))))))]\")\n ;; Wrap 4-arg branch: add custodian parameterize\n (patch-file! \"src/jsh/pipeline.sls\"\n \" [(commands env execute-fn pipe-types)\\n (let ([ptypes (or pipe-types\\n (make-list (- (length commands) 1) 'PIPE))])\\n (parameterize ([*procsub-cleanups* (list)])\"\n \" [(commands env execute-fn pipe-types)\\n (let ([ptypes (or pipe-types\\n (make-list (- (length commands) 1) 'PIPE))])\\n (parameterize ([*procsub-cleanups* (list)]\\n [*pipeline-custodian* (make-custodian)])\")\n ;; Add custodian cleanup after run-procsub-cleanups in 4-arg branch\n (patch-file! \"src/jsh/pipeline.sls\"\n \"(ffi-sigchld-unblock)\\n (run-procsub-cleanups!)\\n exit-codes))))))))))))))))]))\n (define (make-pipes n)\"\n \"(ffi-sigchld-unblock)\\n (run-procsub-cleanups!)\\n (pipeline-custodian-cleanup!)\\n exit-codes))))))))))))))))]))\n (define (make-pipes n)\")\n\n ;; --- 3. Profile infrastructure in main.sls ---\n ;; Add (std misc profile) import\n (patch-file! \"src/jsh/main.sls\"\n \"(jsh stage)\\n (jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\"\n \"(jsh stage)\\n (jsh sandbox) (except (jsh conditions) errexit-exception? errexit-exception-status subshell-exit-exception? subshell-exit-exception-status nounset-exception? nounset-exception-status return-exception? return-exception-status break-exception? break-exception-levels continue-exception? continue-exception-levels)\\n (only (std misc profile) profiling-active? profile-report profile-reset! profile-data)\")\n ;; Add profile builtin after embed-fd builtin\n (patch-file! \"src/jsh/main.sls\"\n \"(builtin-register! \\\"embed-fd\\\"\"\n (string-append\n \";; Profile builtin — control profiling at shell level\\n\"\n \" (builtin-register! \\\"profile\\\"\\n\"\n \" (lambda (args env)\\n\"\n \" (cond\\n\"\n \" [(null? args)\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"usage: profile on|off|reset|report~n\\\") 2]\\n\"\n \" [(string=? (car args) \\\"on\\\")\\n\"\n \" (profiling-active? #t)\\n\"\n \" (fprintf (current-error-port) \\\"profiling enabled~n\\\") 0]\\n\"\n \" [(string=? (car args) \\\"off\\\")\\n\"\n \" (profiling-active? #f)\\n\"\n \" (fprintf (current-error-port) \\\"profiling disabled~n\\\") 0]\\n\"\n \" [(string=? (car args) \\\"reset\\\")\\n\"\n \" (profile-reset!)\\n\"\n \" (fprintf (current-error-port) \\\"profile data reset~n\\\") 0]\\n\"\n \" [(string=? (car args) \\\"report\\\")\\n\"\n \" (profile-report) 0]\\n\"\n \" [else\\n\"\n \" (fprintf (current-error-port)\\n\"\n \" \\\"profile: unknown subcommand '~a'~n\\\" (car args)) 2])))\\n\"\n \" ;; Activate profiling if JSH_PROFILE=1\\n\"\n \" (let ([v (getenv \\\"JSH_PROFILE\\\" #f)])\\n\"\n \" (when (and v (string=? v \\\"1\\\"))\\n\"\n \" (profiling-active? #t)))\\n\"\n \" (builtin-register! \\\"embed-fd\\\"\"))\n\n ;; --- 4. Config loading is wired directly in startup.ss ---\n ;; (jsh-config-load! is imported + called there; the old patch-file! anchors\n ;; targeted the pre-Jerboa `define` form and silently no-op'd, leaving\n ;; ~/.jsh/config unloaded. Sourcing it in startup.ss is the single source of\n ;; truth — no fragile string patch.)\n\n ;; --- 5. Memoize hostname in prompt.sls ---\n ;; Add (std misc memoize) import\n (patch-file! \"src/jsh/prompt.sls\"\n \"(except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\"\n \"(only (std misc memoize) memoize)\\n (except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\")\n ;; Wrap hostname-short with memoize (called many times, never changes)\n (patch-file! \"src/jsh/prompt.sls\"\n \"(define (hostname-short)\\n (guard (__exn [#t ((lambda (e) \\\"localhost\\\") __exn)])\\n (let ([port (open-input-process\\n (list 'path: \\\"/bin/hostname\\\"))])\\n (let ([result (get-line port)])\\n (close-port port)\\n (if (string? result) result \\\"localhost\\\")))))\"\n \"(define hostname-short\\n (memoize\\n (lambda ()\\n (guard (__exn [#t ((lambda (e) \\\"localhost\\\") __exn)])\\n (let ([port (open-input-process\\n (list 'path: \\\"/bin/hostname\\\"))])\\n (let ([result (get-line port)])\\n (close-port port)\\n (if (string? result) result \\\"localhost\\\")))))))\")\n\n ;; --- 6. fmt import in recorder.sls ---\n (patch-file! \"src/jsh/recorder.sls\"\n \"(except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\"\n \"(only (std misc fmt) fmt fmt/port)\\n (except (jerboa runtime) bind-method! call-method ~ void\\n cons* make-list)\")\n\n (display \" Stdlib integration patches applied\\n\")\n) ;; end stdlib patches\n\n;; --- Global cleanup: replace Jerboa reader syntax with Chez equivalents ---\n;; The transpiler outputs #!void which is valid Jerboa but not Chez reader syntax.\n;; Replace all occurrences in .sls files so pure Chez compilation works.\n;; Helper: string-contains for plain Chez (not in chezscheme, only in Jerboa prelude)\n(define (string-contains haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) i]\n [else (loop (+ i 1))]))))\n(for-each\n (lambda (path)\n (let* ([content (call-with-input-file path\n (lambda (p) (get-string-all p)))])\n (when (string-contains content \"#!void\")\n (let loop ([i 0] [result \"\"])\n (cond\n ((> (+ i 6) (string-length content))\n (let ([final (string-append result (substring content i (string-length content)))])\n (call-with-output-file path\n (lambda (p) (display final p))\n 'replace)\n (printf \" Fixed #!void → (void) in ~a~n\" path)))\n ((string=? (substring content i (+ i 6)) \"#!void\")\n (loop (+ i 6) (string-append result \"(void)\")))\n (else\n (loop (+ i 1) (string-append result (substring content i (+ i 1))))))))))\n (let ([files '()])\n (for-each\n (lambda (name)\n (let ([path (string-append \"src/jsh/\" name \".sls\")])\n (when (file-exists? path) (set! files (cons path files)))))\n '(\"ast\" \"registry\" \"macros\" \"util\" \"environment\" \"lexer\" \"arithmetic\"\n \"glob\" \"fuzzy\" \"history\" \"recorder\" \"player\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\"\n \"lineedit\" \"fzf\" \"script\" \"startup\" \"main\" \"coreutils\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-server\" \"mux-client\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"sandbox\" \"rl\" \"limits\"\n \"ffi\" \"embed-data\" \"embed\"))\n files))\n\n;; Note: read-eval (#. syntax) patches not needed — Chez Scheme's reader\n;; does not support #. read-eval syntax at all, so there is no injection risk.\n\n(display \"\\n=== Build complete ===\\n\")\n"} {"text":";; FILE: jerboa-shell/fix-tests.md\n# Fix Plan: Jerboa-Shell Compat Test Parity with Legacy Shell\n\n## Current Status\n\n| Shell | Passed | Total | Rate | Timeouts |\n|-------|--------|-------|------|----------|\n| Jerboa-shell (Chez) | 1003 | 1179 | 85.1% | 0 |\n| Legacy shell | 1063 | 1179 | 90.2% | 0 |\n| **Delta** | **-60** | | **-5.1pp** | |\n\n## Regression Summary by Spec File\n\n| Spec File | Regressions | Priority |\n|-----------|-------------|----------|\n| builtin-read | 22 | P0 |\n| builtin-printf | 14 | P1 |\n| builtin-echo | 9 | P1 |\n| background | 9 | P1 |\n| redirect | 4 | P2 |\n| tilde | 4 | P2 |\n| builtin-cd | 4 | P2 |\n| builtin-trap | 3 | P2 |\n| builtin-bracket | 2 | P3 |\n| brace-expansion | 2 | P3 |\n| glob | 2 | P3 |\n| quote | 1 | P3 |\n| builtin-eval-source | 1 | P3 |\n| case_ | 1 | P3 |\n| loop | 1 | P3 |\n| builtin-misc | 1 | P3 |\n\n---\n\n## P0: `read` Builtin — 22 Regressions\n\n### Root Cause Analysis\n\nThe `read` builtin implementation in `jerboa-shell/builtins.ss:1008` has multiple issues centered around the Chez port's I/O layer differences. The core problem is that `port-or-fd-read-char` uses `fdread` for pipeline stdin, but many `read` options don't work correctly through this code path.\n\n### Issue 1: `read` with zero args returns status 1 (test #4)\n\n**File:** `jerboa-shell/builtins.ss:1215-1219`\n\nWhen `read` is called with no variable names, `vars` is `[]` and `array-name` is `#f`. The code falls into `use-reply?` path (line 1232) which stores into `$REPLY`. The success paths all return `(if got-eof? 1 0)`. However, when reading from a pipe with no trailing newline, `got-eof?` is set true and status 1 is returned even when non-empty data was successfully read. Bash returns 0 when it successfully reads data, even without a trailing newline, when no var names given.\n\n**Fix:** When `read` with no args successfully reads a non-empty line (even at EOF), return 0. Change the return logic to `(if (and got-eof? (string=? line \"\")) 1 0)` in the use-reply? path.\n\n### Issue 2: `read -n N` not reading from pipe (tests #5, #10, #11, #13, #52)\n\n**File:** `jerboa-shell/builtins.ss:1067-1118`\n\n`read -n N` uses `port-or-fd-read-char` which goes through `fdread` on the raw pipeline fd. The `fdread` path reads one byte at a time. When combined with `-d` (delimiter) the logic is correct in structure, but:\n\n1. The `pipe-fd` may be `#f` when input comes from a heredoc or pipe redirect (not pipeline stdin)\n2. `port-or-fd-read-char` falls back to `read-char` on the Gambit port, which may buffer differently on Chez\n3. `-n` combined with `-d` doesn't respect the delimiter correctly in some edge cases\n\n**Fix:**\n- Ensure `pipe-fd` is set correctly for all input sources (not just pipeline stdin)\n- When `read -n N` reads from a pipe, use `read-char` on the port (not `fdread`) since Chez ports handle pipe EOF correctly\n- Fix `-n` + `-d` interaction: delimiter should stop reading even when count not reached\n\n### Issue 3: `read -d ''` null delimiter (tests #31, #46)\n\n**File:** `jerboa-shell/builtins.ss:1069-1071, 1120-1123`\n\nThe code at lines 1069-1071 and 1120-1123 converts empty `-d ''` to NUL char:\n```scheme\n(delim-ch (if (string=? delim \"\")\n (integer->char 0) ;; Convert empty string to NUL char\n (string-ref delim 0)))\n```\n\n**Semantic mismatch:** POSIX/bash treats `-d ''` as \"read NUL-separated records\" (stop at NUL byte). The conversion to `(integer->char 0)` is correct in principle, but `port-or-fd-read-char` may not handle NUL bytes correctly through the Chez character port — Chez's `read-char` may skip or mishandle NUL.\n\n**Fix:** For null delimiter mode, use byte-level I/O (`get-u8` / `fdread`) instead of `read-char`, since NUL is a valid delimiter byte but problematic as a Scheme character in some implementations. Compare each byte to 0 rather than each char to `#\\nul`.\n\n### Issue 4: `read -s` from pipe (test #23)\n\n**File:** `jerboa-shell/builtins.ss:1030-1032`\n\n`-s` (silent) only disables echo on TTYs via `tty-mode-set!`. The test has `-s` reading from a pipe, which isn't a TTY, so `-s` should be a no-op. The actual failure is likely in the reading logic when `-s` interacts with pipe-fd. The test expects `read -s -n2 var` to read 2 chars from pipe — same as Issue 2.\n\n**Fix:** Same as Issue 2 — fix `-n` reading from pipes.\n\n### Issue 5: `read -p` prompt fd check (test #43)\n\n**File:** `jerboa-shell/builtins.ss:1019-1022`\n\n```scheme\n(when (and (> (string-length prompt) 0)\n (= (ffi-isatty (or fd 0)) 1))\n (display prompt (current-error-port))\n (force-output (current-error-port)))\n```\n\n**Problem:** The isatty check is on the wrong fd. It checks `ffi-isatty(0)` (stdin), but bash checks if **stderr** (fd 2, where the prompt is displayed) is a tty. The prompt should be shown if stderr is a tty, regardless of whether stdin is a pipe.\n\nThe actual test #43 mismatch (`got: 'hi\\nhi\\n'` vs `expected: 'hi\\nh\\n'`) also involves a second `read -n1` not reading correctly — related to Issue 2.\n\n**Fix:** Change isatty check to `(= (ffi-isatty 2) 1)` to check stderr instead of stdin. Also fix the -n reading issue (Issue 2).\n\n### Issue 6: `read` without -r, backslash handling inconsistency (tests #14, #54, #63, #64)\n\n**File:** `jerboa-shell/builtins.ss:1182-1211`\n\n**Critical inconsistency across modes:**\n\n- **Default line read** (lines 1204-1207): Keeps BOTH backslash and char: `(display #\\\\ buf) (display next buf)`\n- **`-d` mode** (line 1159-1160): Keeps only the char: `(display next buf)`\n- **`-n` mode** (line 1113-1114): Keeps only the char: `(display next buf)`\n\nThe default mode keeps backslashes because \"backslash removal happens during IFS split\" (comment at line 1205). But this means the `read-strip-backslashes` function (line 1423) must be called on the result before storing into variables. The issue is:\n- `read` without args → `use-reply?` path (line 1233) calls `read-strip-backslashes` ✓\n- `read` with vars → IFS split path (line 1245-1247) calls `read-ifs-split` which should handle backslashes ✓\n- But `-d` and `-n` modes strip backslashes during reading, then `read-strip-backslashes` is applied AGAIN, double-stripping\n\n**Fix:**\n- Make backslash handling consistent across all modes\n- Ensure `read-strip-backslashes` is applied exactly once in all non-raw paths\n- Fix backslash-newline continuation when `-d delim` is set: continuation should only apply when the delimiter is newline\n\n### Issue 7: IFS splitting with non-default delimiters (tests #56-59, #61-64)\n\n**File:** `jerboa-shell/builtins.ss:1440-1448` (`read-ifs-split-raw`)\n\nMultiple IFS edge cases fail:\n- `IFS='x '` with `read -a`: trailing delimiters should create empty fields\n- Multi-character IFS with mixed whitespace/non-whitespace\n- Backslash + IFS interactions in non-raw mode\n\nThe `read-ifs-split-raw` and `read-ifs-split` functions have subtle differences from bash's IFS splitting:\n- Non-whitespace IFS chars are \"hard\" delimiters (each one creates a field boundary)\n- Whitespace IFS chars are \"soft\" (collapse together)\n- When `max-fields` is 0 (unlimited, for `-a`), trailing non-whitespace delimiters should create empty trailing fields\n- Backslash-escaped IFS chars should not split\n\n**Fix:** Rewrite `read-ifs-split` and `read-ifs-split-raw` to match bash's exact IFS splitting semantics. Test edge cases with `IFS='x '` extensively.\n\n### Issue 8: Smooshed option parsing (test #45)\n\n**File:** `jerboa-shell/builtins.ss:1269-1415`\n\n`read -rn1` with smooshed flags: `-rn1` should set `raw?=#t` and `nchars=1`. The smooshed flag parser at line 1274 looks correct in structure but may have an issue with the order of flag processing when `-n` consumes the rest of the arg.\n\n**Fix:** Verify that `-rn1` correctly parses as `-r -n 1` and that the remaining characters after `-n` are treated as the count, not as more flags.\n\n### Implementation Plan\n\n1. Fix `port-or-fd-read-char` to work correctly on Chez for pipe input (biggest impact — fixes tests #5, #10, #11, #13, #23, #43, #45, #52)\n2. Fix null-delimiter byte-level reading (fixes #31, #46)\n3. Fix IFS splitting for non-default delimiters (fixes #56-59, #61-64)\n4. Fix backslash handling in non-raw mode (fixes #14, #54, #63, #64)\n5. Fix zero-args return status (fixes #4)\n\n**Files to modify:**\n- `jerboa-shell/builtins.ss` — read builtin and IFS split functions\n\n---\n\n## P1: `printf` — 14 Regressions\n\n### Issue 1: `%x` always outputs uppercase (tests #14, #16, #22, #23, #26, #29, #45, #62)\n\n**File:** `jerboa-shell/builtins.ss:3569-3570`\n\n```scheme\n(raw (number->string n 16))\n(raw (if (char=? spec #\\X) (string-upcase raw) raw))\n```\n\nThe code uses `number->string n 16` which on Chez Scheme produces uppercase hex digits (e.g., `\"2A\"` instead of `\"2a\"`). The code only calls `string-upcase` for `%X`, assuming `number->string` returns lowercase. On Chez, `number->string` with radix 16 returns uppercase.\n\n**Fix:** Add `(raw (string-downcase raw))` after `number->string` to normalize to lowercase, then upcase only for `%X`:\n\n```scheme\n(raw (string-downcase (number->string n 16)))\n(raw (if (char=? spec #\\X) (string-upcase raw) raw))\n```\n\n### Issue 2: `printf %c` crashes with status 1 (test #38)\n\n**File:** `jerboa-shell/builtins.ss:3581-3594`\n\nThe `%c` handler uses `open-output-u8vector` and `get-output-u8vector` which are Gambit-specific. On Chez (via jerboa compat), these may not exist or behave differently, causing an exception caught by the outer handler returning status 1.\n\n**Fix:** Replace Gambit-specific u8vector port operations with Chez-compatible bytevector I/O:\n```scheme\n((#\\c)\n (when (and (string? arg) (> (string-length arg) 0))\n (let* ((ch (string-ref arg 0))\n (cp (char->integer ch)))\n (write-u8 cp buf)))\n (values (+ i 1) rest))\n```\nFor multi-byte chars, use `string->utf8` from Chez to get the first byte.\n\n### Issue 3: `printf %c` with unicode only prints char, not first byte (test #39)\n\nRelated to Issue 2. Bash's `%c` outputs the first **byte** of the UTF-8 encoding. The fix for Issue 2 should handle this — extract first byte via `(bytevector-u8-ref (string->utf8 (string ch)) 0)`.\n\n### Issue 4: `printf %b` not handling `\\NNN` octal without leading 0 (tests #58, #59)\n\n**File:** `jerboa-shell/builtins.ss:3742-3787` (`printf-interpret-b-escapes`)\n\nThe `%b` handler delegates non-octal escapes to `printf-escape` (line 3784). `printf-escape` handles `\\NNN` (octal without leading 0) at lines 3728-3737. But the issue is that `printf-interpret-b-escapes` checks for `\\0NNN` and `\\1`-`\\7` starts — it does handle both forms. The actual bug may be in the `write-u8` call going to `buf` which is a u8vector output port, and the raw byte may not be flushed correctly.\n\n**Fix:** Verify that `write-u8` works correctly on Chez u8vector output ports. If not, adapt the byte writing to use Chez-compatible APIs.\n\n### Issue 5: `printf %b` with `\\044` (dollar sign) outputs empty (test #55)\n\nTest expects `printf '%b' '\\044'` to output `$`. The `\\0` prefix in `\\044` triggers the `\\0NNN` path which reads up to 3 octal digits after the 0: `44` → value 36 → byte 0x24 = `$`. This should work, but the `write-u8` on a u8vector port may fail on Chez.\n\n**Fix:** Same as Issue 4 — verify u8vector port `write-u8` compatibility.\n\n### Issue 6: Invalid UTF-8 byte handling in `printf '%c'` (test #27)\n\nThe test uses `printf '%x' \"'$byte\"` where `$byte` is a raw byte like `\\xce`. On Chez, single-byte characters above 127 may be treated differently. Chez's `char->integer` on invalid UTF-8 gives the Unicode replacement character U+FFFD.\n\n**Fix:** When the `'char` form encounters raw bytes (from `$'\\xNN'` syntax), extract the raw byte value rather than the Unicode code point. This may require checking for Private Use Area encoding used by the shell's raw-byte mechanism.\n\n### Implementation Plan\n\n1. Fix `number->string` lowercase for `%x` (fixes 8 tests — biggest impact)\n2. Fix `%c` to use Chez-compatible byte extraction (fixes 2 tests)\n3. Fix `%b` u8vector port compatibility (fixes 3 tests)\n4. Fix raw byte handling for `'char` printf argument (fixes 1 test)\n\n**Files to modify:**\n- `jerboa-shell/builtins.ss` — printf format handlers\n\n---\n\n## P1: `echo -e` — 9 Regressions\n\n### Root Cause: Raw byte output ordering\n\n**File:** `jerboa-shell/builtins.ss:3283-3375` (`echo-expand-escapes`, `display-raw-bytes`)\n\nAll 9 echo failures show the same pattern: the raw byte appears at the wrong position in the output. For example, `echo -e 'ab\\x63d'` outputs `abcdf\\ne` instead of `abcdef\\n`. The character 'e' (0x65) and 'f' appear swapped with the escaped character.\n\nThe `echo-expand-escapes` function returns a list of segments (strings and fixnums for raw bytes). The segments are accumulated in reverse order using `acc` and then reversed at the end. The issue is in how `flush-buf!` interacts with the accumulator:\n\n```scheme\n(loop j (cons byte (flush-buf! acc)))\n```\n\nThis flushes the current string buffer and then adds the byte. But `flush-buf!` returns `(cons current-string acc)` where `acc` is in reverse order. The byte gets consed onto this, so it ends up in the right position in the reversed list... but there may be a subtle bug in the `flush-buf!` + `cons` sequence when the `get-output-string` side-effects interact with Chez.\n\n**Confirmed Chez-specific issue:** `get-output-string` on Chez does NOT reset the output port. On Gambit, `get-output-string` drains the buffer and resets it. On Chez, subsequent writes to `buf` include old content, causing all raw-byte escapes to produce corrupted output — bytes appear at wrong positions because the string buffer accumulates text from before AND after the byte.\n\nThe `flush-buf!` function at line 3298 calls `get-output-string buf` but doesn't reset the port:\n```scheme\n(define (flush-buf! acc)\n (let ((s (get-output-string buf)))\n (if (string=? s \"\") acc (cons s acc))))\n```\n\nAfter this call, `buf` still contains the old text. When the loop continues and writes more chars to `buf`, they get prepended with the already-flushed content.\n\n**Fix:** After `get-output-string buf`, explicitly reinitialize `buf`:\n```scheme\n(define (flush-buf! acc)\n (let ((s (get-output-string buf)))\n (set! buf (open-output-string)) ;; reset — critical for Chez\n (if (string=? s \"\") acc (cons s acc))))\n```\n\nThis same pattern also affects `printf-escape` and `printf-interpret-b-escapes` which use `write-u8` to a u8vector output port — verify those ports also handle the Chez `get-output-string`/`get-output-u8vector` semantics correctly.\n\n### Implementation Plan\n\n1. Fix `get-output-string` buffer reset in `echo-expand-escapes` (fixes all 9 tests)\n\n**Files to modify:**\n- `jerboa-shell/builtins.ss` — `echo-expand-escapes` function\n- Possibly `src/compat/gambit.sls` — if `get-output-string` needs a compat wrapper\n\n---\n\n## P1: Background Jobs & `wait` — 9 Regressions\n\n### Issue 1: `wait` returns 0 instead of process exit status (tests #8, #16, #17, #21)\n\n**File:** `jerboa-shell/jobs.ss:268-341` (`job-wait`)\n\n`wait $PID` calls `job-table-get` to look up the job by PID. For external processes launched via `ffi-fork-exec`, the PID in the job table is the real PID. But `job-table-get` may not find the job if it was already cleaned up or if the PID format doesn't match (string vs number comparison).\n\nWhen `job-table-get` returns `#f`, the wait builtin returns 127 (line 1952). But tests show status 0, suggesting the job IS found but `job-wait` returns 0.\n\nRoot cause: In `job-wait`, the `ffi-waitpid-pid` call with `WNOHANG` may return 0 (child not yet exited) initially, then on retry the child has already been reaped by SIGCHLD handler. When `ffi-waitpid-pid` returns -1 (error, ECHILD because child already reaped), the code falls to the `else` branch (line 328) which sets `last-exit-code` to 0.\n\n**Fix:** When `waitpid` returns -1 (ECHILD), check if the job's thread has a saved exit status. Alternatively, save exit status in the job-process struct when SIGCHLD is received, so `job-wait` can retrieve it even after the process is reaped.\n\n### Issue 2: Builtins/compound commands in background produce no output (tests #9, #19)\n\n**File:** `jerboa-shell/executor.ss:1104-1120` (`launch-background`)\n\nFor non-simple commands (compound/builtins), `launch-background` spawns a thread:\n```scheme\n(th (spawn (lambda ()\n (parameterize ((*in-subshell* #t))\n (execute-command cmd child-env)))))\n```\n\nThe thread runs in the same process and inherits stdout via closure. However:\n1. **No `force-output` before thread exit** — output is buffered and lost when thread is GC'd\n2. **No explicit port parameterization** — unlike `launch-thread-piped` in `pipeline.ss:295-311` which explicitly creates and parameterizes output ports\n3. The `*in-subshell*` flag may cause output routing issues if it affects how builtins write\n\n**Contrast with correct pattern** in `pipeline.ss:311` which calls `force-output` before closing the port:\n```scheme\n(force-output out-port)\n(close-output-port out-port)\n```\n\n**Fix:**\n- Add `force-output` in the thread after `execute-command` returns (in a `dynamic-wind` cleanup)\n- Ensure background threads explicitly parameterize `current-output-port` to the parent's actual stdout\n- For background for-loops (`control.ss:39-88`), add `force-output` after each loop iteration body\n\n### Issue 3: `wait -n` returns 0 instead of first-finished exit status (test #18)\n\n**File:** `jerboa-shell/jobs.ss:346-417` (`job-wait-any`, `job-check-finished?`)\n\n`job-wait-any` polls running jobs via `job-check-finished?` (lines 377-417) which uses non-blocking `ffi-waitpid-pid ... WNOHANG`. **Two problems:**\n\n1. `job-check-finished?` detects completion but does NOT update the job's status — it only returns `#t`/`#f`\n2. After detecting completion, the code calls `job-wait` which does ANOTHER `waitpid` — but the process was already reaped by the first check, causing `waitpid` to return -1 (ECHILD), which falls to the `else` branch (line 328) that sets `last-exit-code` to 0\n\n**Race condition:** Between `job-check-finished?` (non-blocking check) and `job-wait` (blocking wait), the child is reaped by the first call, so the second call fails and returns 0.\n\n**Fix:** In `job-check-finished?`, when `waitpid` returns > 0, immediately save the exit status in the `job-process` struct. Then `job-wait` should check for the saved status before calling `waitpid` again. Alternatively, make `job-check-finished?` update the job status atomically when it discovers completion.\n\n### Issue 4: Trap not cleared in background subshell (trap test #26)\n\n**File:** `jerboa-shell/executor.ss:1108-1119`\n\nBash clears traps in child processes started with `&`. The thread-based \"subshell\" for compound commands doesn't clear the trap table in the cloned environment.\n\n**Fix:** In `launch-background`, after cloning the environment, clear `*trap-table*` in the child thread's dynamic scope.\n\n### Implementation Plan\n\n1. Fix `waitpid`/ECHILD handling to preserve exit status (fixes 4 tests)\n2. Fix background thread stdout routing (fixes 2 tests)\n3. Fix `wait -n` polling to actually check process status (fixes 1 test)\n4. Clear traps in background subshells (fixes 1 test, also fixes trap test)\n5. Fix `wait` for all background jobs to properly collect statuses (fixes 1 test)\n\n**Files to modify:**\n- `jerboa-shell/jobs.ss` — job-wait, job-wait-any\n- `jerboa-shell/executor.ss` — launch-background\n- `jerboa-shell/signals.ss` — trap clearing for subshells\n\n---\n\n## P2: Redirections — 4 Regressions\n\n### Issue 1: Writing to fd 3/4 multiple times loses first write (tests #17, #18)\n\n**Expected:** Two `echo` writes to `exec 3>file` should produce both lines.\n**Got:** Only the last line appears.\n\n**File:** `jerboa-shell/redirect.ss:173-175, 671-678`\n\nWhen `exec 3>file` is applied, `set-port-for-fd!` (line 671) opens the file and creates a NEW Gambit port. This port is set via `(current-output-port port)` parameterization. When another redirect targets the same fd (e.g., `echo foo >&3`), a new port is created that replaces the previous one. The old port's buffered content is lost.\n\n**Fix:** For persistent redirections (`exec N>file`), open the fd once and track it in the shell's fd table. Subsequent writes to fd N should reuse the existing fd/port rather than reopening the file. Ensure `set-port-for-fd!` checks for existing persistent fds before creating new ports.\n\n### Issue 2: `1>&2-` (move fd) not working (test #27)\n\nThe `N>&M-` syntax means \"dup fd M to fd N, then close M\". This may not be implemented in the Chez redirect layer.\n\n**Fix:** Implement fd-move semantics in the redirect handler: `dup2(M, N)` followed by `close(M)`.\n\n### Issue 3: `<>` read/write mode not preserving position (test #29)\n\n`<> file` opens file for both reading and writing. After reading, the write position should be where the read left off. The Chez port may open separate read/write ports or not handle bidirectional fd correctly.\n\n**Fix:** Use a single fd opened with `O_RDWR` and ensure read/write share the same file offset. May require FFI `open()` with `O_RDWR` flag.\n\n**Files to modify:**\n- `src/gsh/redirect.sls`\n- `jerboa-shell/redirect.ss`\n\n---\n\n## P2: Tilde Expansion — 4 Regressions\n\n### Issue 1: `~nonexistent` expands to $HOME instead of literal (test #6)\n\n**File:** `jerboa-shell/expander.ss:720-724`\n\n```scheme\n(else\n (with-catch\n (lambda (e) (values (substring str i end) end))\n (lambda ()\n (values (user-info-home (user-info prefix)) end))))\n```\n\nWhen `user-info` throws for a nonexistent user, the catch should return the literal `~nonexistent`. But on Chez, `user-info` might not throw — it might return a default or the current user's info. Or the Chez compat `user-info` implementation falls back differently.\n\n**Fix:** Check the `user-info` implementation in the compat layer. Ensure it throws when the user doesn't exist. If it silently returns current user info, add an explicit check.\n\n### Issue 2: `${x//~/~root}` not expanding tilde in replacement (test #8)\n\n**File:** `jerboa-shell/expander.ss`\n\nIn `${var//pattern/replacement}`, tilde in the replacement should expand. The current code may not run tilde expansion on the replacement string.\n\n**Fix:** Apply tilde expansion to the replacement string in `${var//pat/repl}` before substitution.\n\n### Issue 3: `x=foo:~` tilde in colon-separated values (test #9)\n\n**File:** `jerboa-shell/expander.ss:461-470`\n\nIn assignment context, `~` should expand after `:` in values like `foo:~`. The `expand-assignment-value` function should handle this. Test shows `foo:~,` is incorrectly expanding the tilde (expected `foo:~,` to NOT expand because `,` follows `~` without `/`).\n\n**Fix:** Tilde after `:` in assignment context should only expand when followed by `/` or end of string, not arbitrary chars.\n\n### Issue 4: Temp assignment `x=~` with `env` (test #14)\n\nTilde expansion in temp assignments before commands (e.g., `x=~root:~ env`) should expand `~root` to root's home. Related to user-info lookup (Issue 1).\n\n**Fix:** Same as Issue 1 — fix `user-info` compat to correctly resolve other users.\n\n**Files to modify:**\n- `jerboa-shell/expander.ss` — `expand-tilde-in`, `expand-assignment-value`\n- `src/compat/gambit.sls` — `user-info` implementation\n\n---\n\n## P2: `cd` Builtin — 4 Regressions\n\n### Issue 1: `cd` with strict_arg_parse (test #3)\n\n`cd --` should succeed (status 0), but returns 1. The option parser may treat `--` as an error when no directory follows, rather than as \"cd to $HOME\".\n\n**Fix:** In `cd` option parsing, `cd --` (with no further args) should cd to `$HOME`, same as bare `cd`.\n\n### Issue 2: `pwd` in symlinked dir (test #15)\n\nWhen the shell starts in a directory that's a symlink, `pwd` should show the symlink path (logical). The Chez port may resolve symlinks eagerly.\n\n**Fix:** Initialize `$PWD` from the environment's `PWD` variable (if it points to the correct directory) rather than using `current-directory` which may resolve symlinks.\n\n### Issue 3: `cd` with inherited PWD disagreement (tests #25, #26)\n\nWhen `PWD` is inherited but doesn't match the actual directory, `cd` should still work. The Chez port may not handle the case where `$PWD` disagrees with `getcwd()`.\n\n**Fix:** In `cd`, when the inherited `$PWD` disagrees with `getcwd()`, update `$PWD` to reflect the actual directory before attempting relative path resolution.\n\n**Files to modify:**\n- `jerboa-shell/builtins.ss` — cd builtin\n- `jerboa-shell/main.ss` — PWD initialization\n\n---\n\n## P2: `trap` — 3 Regressions\n\n### Issue 1: Traps not cleared in subshell via `&` (test #26)\n\nBackground subshells (`cmd &`) should start with an empty trap table. Currently thread-based subshells inherit the parent's `*trap-table*`.\n\n**Fix:** In `launch-background`, parameterize `*trap-table*` with a fresh hash-table in the child thread.\n\n### Issue 2: trap USR1 + sleep not working non-interactively (test #27)\n\n`trap 'echo usr1' USR1; kill -USR1 $$; sleep 0.1` should print \"usr1\". The signal may not be delivered or the trap handler may not execute during `sleep`.\n\n**Fix:** Ensure `pending-signals!` is checked after `sleep` completes, and that signal flag checking works for USR1 in non-interactive mode.\n\n### Issue 3: trap EXIT + sleep + SIGINT (test #29)\n\nSimilar to Issue 2 — EXIT trap should fire when the shell receives SIGINT during sleep.\n\n**Fix:** Ensure EXIT trap fires on all exit paths including signal-induced exits.\n\n**Files to modify:**\n- `jerboa-shell/signals.ss` — signal delivery in non-interactive mode\n- `jerboa-shell/executor.ss` — launch-background trap clearing\n\n---\n\n## P3: `file-info` Compat Layer — 4 Regressions (across bracket, cd)\n\n### Root Cause: Stubbed-out stat fields\n\n**File:** `src/compat/gambit.sls:282-284`\n\n```scheme\n(make-file-info-rec type (if (< size 0) 0 size) mode\n 0 0 ;; device/inode — use real stat if needed\n 0 0 ;; owner/group\n ...)\n```\n\n`device`, `inode`, `owner`, and `group` are all hardcoded to 0. This breaks:\n- `test -G` (effective group ownership) — always fails (test #37)\n- `test -O` (effective user ownership) — always fails (test #37)\n- `test -ef` (same file by device+inode) — can't compare because both are 0 (test #42)\n\n### Fix\n\nAdd FFI functions to the C shim (`ffi-shim.c`) to extract full stat fields:\n\n```c\nint ffi_file_uid(const char *path) { struct stat st; return stat(path, &st) == 0 ? st.st_uid : -1; }\nint ffi_file_gid(const char *path) { struct stat st; return stat(path, &st) == 0 ? st.st_gid : -1; }\nlong long ffi_file_dev(const char *path) { struct stat st; return stat(path, &st) == 0 ? (long long)st.st_dev : -1; }\nlong long ffi_file_ino(const char *path) { struct stat st; return stat(path, &st) == 0 ? (long long)st.st_ino : -1; }\n```\n\nThen update `file-info` to use them:\n\n```scheme\n(define c-ffi-file-uid (foreign-procedure \"ffi_file_uid\" (string) int))\n(define c-ffi-file-gid (foreign-procedure \"ffi_file_gid\" (string) int))\n(define c-ffi-file-dev (foreign-procedure \"ffi_file_dev\" (string) long-long))\n(define c-ffi-file-ino (foreign-procedure \"ffi_file_ino\" (string) long-long))\n\n(define (file-info path . follow?)\n (let* ((follow (if (pair? follow?) (car follow?) #t))\n (type-int (c-ffi-file-type path (if follow 1 0))))\n (if (= type-int -1)\n (error 'file-info \"cannot stat file\" path)\n (make-file-info-rec\n (file-type-int->symbol type-int)\n (let ((s (c-ffi-file-size path))) (if (< s 0) 0 s))\n (c-ffi-file-mode path)\n (c-ffi-file-dev path)\n (c-ffi-file-ino path)\n (c-ffi-file-uid path)\n (c-ffi-file-gid path)\n (make-time 'time-utc 0 (let ((m (c-ffi-file-mtime path))) (if (< m 0) 0 m)))\n (make-time 'time-utc 0 0)))))\n```\n\n**Files to modify:**\n- `ffi-shim.c` — add uid/gid/dev/ino FFI functions\n- `src/compat/gambit.sls` — update `file-info` to use them\n\n---\n\n## P3: Glob — 2 Regressions\n\n### Issue 1: Unicode char in glob pattern (test #31)\n\n`echo __?__` should match both `__a__` and `__μ__`. The `?` glob should match any single character, including multi-byte UTF-8 characters. The Chez glob implementation may treat `?` as matching a single byte instead of a single character.\n\n**Fix:** In `jerboa-shell/glob.ss`, ensure glob `?` matches a single Unicode character, not a single byte. The regex `[^/]` generated for `?` should be `[^/]` with Unicode mode enabled.\n\n### Issue 2: `shopt -u globskipdots` (test #39)\n\n`shopt -u globskipdots` should make `*` match `.` and `..`. This shopt option may not be implemented.\n\n**Fix:** Add `globskipdots` to the shopt handling. When disabled, glob patterns should include dotfiles including `.` and `..`.\n\n**Files to modify:**\n- `jerboa-shell/glob.ss`\n- `jerboa-shell/builtins.ss` — shopt handler\n\n---\n\n## P3: Brace Expansion — 2 Regressions\n\n### Issue 1: Tilde in brace expansion (test #30)\n\n`echo ~bob/src{,~root}` should expand to `/home/bob/src /root`. Tilde at the start of brace elements should expand.\n\n**Fix:** Apply tilde expansion to each brace-expanded result, not just the original word.\n\n### Issue 2: Side effect ordering in `{a,b,c}` (test #53)\n\n`echo {a,b,c}-$((i++))` should produce `a-0 b-1 c-2` (left-to-right evaluation). Currently produces `a-1 b-2 c-0`, suggesting the arithmetic expression is evaluated first for all expansions, then assigned.\n\n**Fix:** Evaluate `$((i++))` for each brace-expanded word in left-to-right order, not all at once.\n\n**Files to modify:**\n- `jerboa-shell/expander.ss` — brace expansion and tilde interaction\n\n---\n\n## P3: Quote — 1 Regression\n\n### Issue: `$'\\377'` octal in ANSI-C quoting (test #28)\n\n`$'\\377'` should produce byte 0xFF. The output shows the byte appears but at the wrong position, similar to the echo-e issue.\n\n**Fix:** Same root cause as echo-e — `get-output-string` buffer reset issue in the ANSI-C quote expander. Apply the same fix.\n\n**Files to modify:**\n- `jerboa-shell/expander.ss` — ANSI-C quoting handler\n\n---\n\n## P3: `source` Along PATH — 1 Regression\n\n### Issue: Source doesn't skip directories in PATH (test #23)\n\n`source myfile` should search PATH and skip entries that are directories. `find-file-in-path` at `jerboa-shell/util.ss:164` calls `file-directory?` which may not work correctly through the Chez compat layer.\n\n**Fix:** Verify `file-directory?` works correctly on Chez. It may need to use the stat-based FFI rather than Chez's built-in `file-directory?` which might have different semantics.\n\n**Files to modify:**\n- `jerboa-shell/util.ss` — `find-file-in-path`\n- `src/compat/gambit.sls` — verify `file-directory?`\n\n---\n\n## P3: `case` — 1 Regression\n\n### Issue: Matching byte 0xFF against empty string (test #10)\n\n`case $'\\xff' in '') echo a;; *) echo b;; esac` should match `*` (not empty), outputting `b`. Chez may represent the 0xFF byte differently, making the case variable appear empty.\n\n**Fix:** Ensure raw bytes from `$'\\xff'` are preserved through variable assignment and case matching. Check the PUA (Private Use Area) encoding scheme for raw bytes.\n\n**Files to modify:**\n- `jerboa-shell/expander.ss` — case pattern matching with raw bytes\n\n---\n\n## P3: `while` in Pipeline — 1 Regression\n\n### Issue: Variable not visible after while-in-pipe (test #12)\n\n`echo 1 2 3 | while read x; do ((n++)); done; echo $n` — expects `$n` to be 3. In bash with `lastpipe` enabled, the last command in a pipeline runs in the current shell. Without it, pipeline components run in subshells and variable changes are lost.\n\n**Fix:** Check if `lastpipe` shopt is enabled (it should be in this context). If the last pipeline component is a builtin/compound command, run it in the current shell rather than a subshell.\n\n**Files to modify:**\n- `jerboa-shell/executor.ss` — pipeline execution, lastpipe handling\n\n---\n\n## P3: `time` Pipeline — 1 Regression\n\n### Issue: `time` with pipeline returns status 1 (test #4)\n\n`time ls | cat` returns status 1 instead of 0. The `time` keyword wraps a pipeline, but the status may not propagate correctly from the timed pipeline.\n\n**Fix:** In `execute-time-command` (`jerboa-shell/executor.ss:946`), ensure the pipeline's exit status is returned, not an error status from the timing code. Check for exceptions in `fl-` or `cpu-time` on Chez.\n\n**Files to modify:**\n- `jerboa-shell/executor.ss` — `execute-time-command`\n\n---\n\n## Implementation Order (Recommended)\n\n### Phase 1: Quick Wins (26 tests, ~2 days)\n\n1. **printf %x lowercase** — Add `string-downcase` after `number->string` (8 tests)\n2. **echo-e buffer reset** — Fix `get-output-string` in `echo-expand-escapes` (9 tests)\n3. **file-info stat fields** — Add uid/gid/dev/ino FFI and update compat (4 tests)\n4. **printf %c** — Chez-compatible byte extraction (2 tests)\n5. **$'\\377' quoting** — Same buffer reset fix as echo-e (1 test)\n6. **source PATH directories** — Verify file-directory? compat (1 test)\n7. **time pipeline status** — Fix status propagation (1 test)\n\n### Phase 2: Medium Effort (22 tests, ~3 days)\n\n8. **read -n from pipe** — Fix port-or-fd-read-char on Chez (6 tests)\n9. **read IFS splitting** — Rewrite read-ifs-split for bash compat (8 tests)\n10. **read backslash handling** — Fix non-raw mode (4 tests)\n11. **read null delimiter** — Byte-level I/O for -d '' (2 tests)\n12. **read misc** — Zero args status, smooshed opts (2 tests)\n\n### Phase 3: Structural Fixes (12 tests, ~4 days)\n\n13. **Background job wait** — Fix waitpid/ECHILD, save exit status (4 tests)\n14. **Background stdout** — Fix thread output routing (2 tests)\n15. **Tilde expansion** — Fix ~user, assignment context, replacement (4 tests)\n16. **Redirect persistence** — Fix exec N>file fd management (3 tests)\n\n### Phase 4: Edge Cases (6 tests, ~2 days)\n\n17. **cd improvements** — PWD init, symlinks, arg parsing (4 tests)\n18. **wait -n** — Non-blocking poll for process completion (1 test)\n19. **trap in subshells** — Clear traps, signal delivery (3 tests)\n20. **glob unicode** — Fix ? to match chars not bytes (1 test)\n21. **brace+tilde** — Tilde in brace elements (1 test)\n22. **Misc** — case 0xff, while-in-pipe lastpipe, brace side-effects, globskipdots (4 tests)\n\n### Total: ~60 test regressions addressed across ~11 days of work\n\n---\n\n## Verification\n\nAfter each fix, run the comparison:\n\n```bash\npython3 /tmp/compare_compat.py\n```\n\nOr test a single spec:\n\n```bash\npython3 jerboa-shell/test/run_spec.py -v \\\n /home/jafourni/mine/jerboa-shell/_vendor/oils/spec/SPECNAME.test.sh \\\n /home/jafourni/mine/jerboa-shell/gsh\n```\n\nTarget: **1063/1179 (90.2%)** — parity with jerboa-shell.\n\n---\n\n## Improvements to Preserve\n\nJerboa-shell already passes 19 tests that jerboa-shell fails. These should not regress:\n\n| Spec File | Tests | Count |\n|-----------|-------|-------|\n| exit-status | #1, #3, #4, #7, #8 | 5 |\n| redirect-multi | #7, #12, #13 | 3 |\n| builtin-set | #6, #7, #8 | 3 |\n| pipeline | #6, #12, #23 | 3 |\n| builtin-process | #23, #26 | 2 |\n| smoke | #15 | 1 |\n| arith | #14 | 1 |\n| var-op-bash | #19 | 1 |\n\nThese represent areas where the Chez port has better behavior (likely due to different default behaviors in Chez's process handling, signal management, or numeric operations). Guard these with explicit regression tests.\n"} -{"text":";; FILE: jerboa-shell/build-jsh-freebsd.ss\n#!chezscheme\n;;; build-jsh-freebsd.ss — Build a fully static jsh binary on FreeBSD\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:... < build-jsh-freebsd.ss\n;;;\n;;; This script:\n;;; 1. Patches coreutils/awk/sed/ssl for static builds (no dlopen)\n;;; 2. Compiles jsh modules (using stock scheme)\n;;; 3. Creates boot file + optimized program .so\n;;; 4. Generates C files with embedded boot data\n;;; 5. Compiles C with cc (clang) against static Chez's scheme.h\n;;; 6. Links fully static binary with libkernel.a\n;;;\n;;; The resulting jsh-freebsd binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/home/freebsd\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-freebsd.sh so we know the repo root.\n(define vendor-dir\n (let ([script-dir (getenv \"SCRIPT_DIR\")])\n (if script-dir\n (format \"~a/vendor\" script-dir)\n (let ([cwd-vendor \"./vendor\"])\n (if (file-directory? cwd-vendor) cwd-vendor\n (format \"~a/jerboa-shell/vendor\" home-dir))))))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. Callers wrap with (or (getenv \"X\") (dep ...))\n;; to allow env var overrides from the shell script.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jsqlite/src\" home-dir)))\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (dep \"jerboa-coreutils\" \"lib\")))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n(define coreutils-shim\n (let ([upstream (dep-file \"jerboa-coreutils\" \"support/libcoreutils.c\")]\n [local \"patches/libcoreutils.c\"])\n (cond\n [(file-exists? upstream) upstream]\n [(file-exists? local) local]\n [else upstream])))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls).\n;; OpenSSL via load-shared-object cannot work in static builds and rustls is\n;; preferred for security.\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n;; Rust native library — resolve via vendor/ → ~/mine/ → ~/\n(define native-rs-dir\n (let* ([v (format \"~a/jerboa/jerboa-native-rs\" vendor-dir)]\n [m (format \"~a/mine/jerboa/jerboa-native-rs\" home-dir)]\n [h (format \"~a/jerboa/jerboa-native-rs\" home-dir)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n(define native-lib-path\n (format \"~a/target/release/libjerboa_native.a\" native-rs-dir))\n(define native-src-dir\n (format \"~a/src\" native-rs-dir))\n;; Sentinel file written after a successful native build without SQLite.\n;; If absent, the .a was built with default (tls-only) features — must rebuild.\n(define native-features-sentinel\n (format \"~a/target/release/.built-with-tls-crypto-no-sqlite\" native-rs-dir))\n(when (and (file-exists? native-src-dir)\n (or (not (file-exists? native-lib-path))\n ;; Features sentinel absent → stale build (wrong feature set)\n (not (file-exists? native-features-sentinel))\n ;; Check if any .rs file is newer than the .a\n (let ([lib-mtime (file-modification-time native-lib-path)])\n (let check ([files (directory-list native-src-dir)])\n (and (pair? files)\n (let ([f (format \"~a/~a\" native-src-dir (car files))])\n (or (and (> (string-length (car files)) 3)\n (string=? \".rs\" (substring (car files)\n (- (string-length (car files)) 3)\n (string-length (car files))))\n (time>? (file-modification-time f) lib-mtime))\n (check (cdr files)))))))))\n (printf \"~n[0/7] Rebuilding Rust native library (source newer than .a)...~n\")\n (let ([rc (safe-system (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote native-rs-dir)))])\n (unless (= rc 0)\n (fprintf (current-error-port) \"FATAL: cargo build --release --no-default-features --features tls,crypto failed~n\")\n (exit 1)))\n ;; Write sentinel so next build knows the right features were used\n (let ([port (open-output-file native-features-sentinel 'truncate)])\n (display \"tls,crypto,no-sqlite\\n\" port)\n (close-output-port port)))\n(when (and (file-exists? native-lib-path)\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(define has-native-lib? (file-exists? native-lib-path))\n\n;; Rust coreutils static library — check current dir first (container build), then home\n(define rust-coreutils-lib-path\n (let ([local (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" (current-directory))]\n [home-path (format \"~a/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\" home-dir)]\n [mine-path (format \"~a/mine/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\" home-dir)])\n (cond\n [(file-exists? local) local]\n [(file-exists? mine-path) mine-path]\n [else home-path])))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — coreutils builtins will be stubs~n\"))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\"))\n\n;; Chez Scheme static installation\n(define chez-ta6fb\n (or (getenv \"CHEZ_TA6FB\")\n (let ([dirs (directory-list \"/usr/local/lib\")])\n (let ([csv-dir (find (lambda (d) (string-prefix? \"csv\" d)) dirs)])\n (if csv-dir\n (format \"/usr/local/lib/~a/ta6fb\" csv-dir)\n (error 'build \"Cannot find Chez ta6fb directory in /usr/local/lib\"))))))\n\n(define scheme-h-dir chez-ta6fb)\n(define petite-boot-path (format \"~a/petite.boot\" chez-ta6fb))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-ta6fb))\n\n(printf \"Chez static: ~a~n\" chez-ta6fb)\n(printf \"Native lib: ~a~n\" (if has-native-lib? native-lib-path \"not found\"))\n(printf \"~n\")\n\n;; ========== Step 0: Patch coreutils for static builds ==========\n;; Coreutils modules call (load-shared-object #f) at library init time.\n;; In static builds, load-shared-object throws because dlopen is unavailable.\n;; Since FFI symbols are pre-registered via Sforeign_symbol, we patch these out.\n\n;; Detect sed -i syntax: FreeBSD uses `sed -i ''`, GNU sed uses `sed -i`\n(define sed-inplace\n (if (= 0 (system \"sed --version 2>/dev/null | head -1 | grep -q GNU\"))\n \"sed -i\" ;; GNU sed (Linux)\n \"sed -i ''\")) ;; BSD sed (FreeBSD/macOS)\n\n(printf \"[0/7] Patching coreutils for static build (no dlopen)...~n\")\n\n(define coreutils-stage (format \"~a/coreutils-stage\" (current-directory)))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(system (format \"mkdir -p '~a'\" coreutils-stage))\n\n(system (format \"cp -a '~a/jerboa-coreutils' '~a/'\"\n coreutils-dir coreutils-stage))\n;; Patch load-shared-object calls (incompatible with static linking)\n(system (format \"find '~a/jerboa-coreutils' -name '*.sls' -exec ~a 's/(load-shared-object #f)/(void)/g' {} +\"\n coreutils-stage sed-inplace))\n(system (format \"find '~a/jerboa-coreutils' -name '*.so' -delete\"\n coreutils-stage))\n(system (format \"find '~a/jerboa-coreutils' -name '*.wpo' -delete\"\n coreutils-stage))\n\n(printf \" Recompiling patched coreutils...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"common.sls\" \"common/version.sls\" \"common/io.sls\" \"common/security.sls\"))\n (for-each\n (lambda (name)\n (let ([sls (format \"~a/jerboa-coreutils/~a.sls\" coreutils-stage name)])\n (when (file-exists? sls)\n (compile-library sls))))\n '(\"basename\" \"dirname\" \"link\" \"unlink\" \"yes\" \"printenv\"\n \"sleep\" \"whoami\" \"logname\" \"hostname\" \"nproc\" \"tty\" \"sync\" \"hostid\"\n \"cat\" \"head\" \"tail\" \"tac\" \"tee\" \"wc\" \"nl\" \"fold\" \"expand\" \"unexpand\" \"fmt\"\n \"cut\" \"paste\" \"join\" \"comm\" \"sort\" \"uniq\" \"tr\" \"numfmt\"\n \"mkdir\" \"rmdir\" \"mktemp\" \"touch\" \"readlink\" \"realpath\" \"ln\" \"cp\" \"mv\" \"rm\"\n \"install\" \"shred\"\n \"ls\" \"chmod\" \"chown\" \"chgrp\" \"stat\" \"du\" \"df\" \"pathchk\"\n \"date\" \"id\" \"groups\" \"who\" \"users\" \"pinky\" \"uptime\" \"uname\" \"arch\"\n \"seq\" \"expr\" \"basenc\" \"base64\" \"base32\" \"od\"\n \"cksum\" \"md5sum\" \"sha1sum\" \"sha224sum\" \"sha256sum\" \"sha384sum\" \"sha512sum\"\n \"b2sum\" \"sum\"\n \"env\" \"timeout\" \"nice\" \"nohup\" \"chroot\" \"stdbuf\"\n \"truncate\" \"mkfifo\" \"mknod\" \"split\" \"csplit\" \"dd\" \"dircolors\"\n \"tsort\" \"shuf\" \"factor\" \"pr\" \"ptx\" \"stty\"\n \"chcon\" \"runcon\"\n \"dir\" \"vdir\" \"rev\" \"top\")))\n\n;; grep + Rust-backed PCRE2\n(let ([grep-pcre2-patch (format \"~a/patches/grep-pcre2.sls\" (current-directory))])\n (when (file-exists? grep-pcre2-patch)\n (system (format \"mkdir -p '~a/jerboa-coreutils/grep'\" coreutils-stage))\n (system (format \"cp '~a' '~a/jerboa-coreutils/grep/pcre2.sls'\"\n grep-pcre2-patch coreutils-stage))))\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (let ([pcre2-sls (format \"~a/jerboa-coreutils/grep/pcre2.sls\" coreutils-stage)])\n (when (file-exists? pcre2-sls)\n (printf \" Compiling grep/pcre2...~n\")\n (compile-library pcre2-sls)))\n (let ([grep-sls (format \"~a/jerboa-coreutils/grep.sls\" coreutils-stage)])\n (when (file-exists? grep-sls)\n (printf \" Compiling grep...~n\")\n (compile-library grep-sls))))\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: patch pcre2 to use Rust regex\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if has-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when has-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (library-directories))])\n (when has-aws?\n (printf \" Compiling jerboa-aws...~n\")\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0d: Stage jerboa-ssh for static build ==========\n(printf \"[0d/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build\n (system (format \"find '~a' -name '*.sls' -exec ~a 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage sed-inplace))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"~a 's/base64-encode/b64-encode/g' '~a'\" sed-inplace kh))\n (system (format \"~a 's/base64-decode/b64-decode/g' '~a'\" sed-inplace kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0e: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0e/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n(define vault-stage (format \"~a/vault-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" vault-stage vault-stage))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n(when has-jerboa-fuse?\n ;; Copy the jerboa-fuse library tree (chez/fuse/ and chez/vault/)\n (system (format \"mkdir -p '~a/chez/fuse' '~a/chez/vault'\" vault-stage vault-stage))\n ;; FUSE layer\n (system (format \"cp '~a/chez/fuse.sls' '~a/chez/fuse.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/constants.sls' '~a/chez/fuse/constants.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/types.sls' '~a/chez/fuse/types.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/codec.sls' '~a/chez/fuse/codec.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/mount.sls' '~a/chez/fuse/mount.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/access.sls' '~a/chez/fuse/access.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/secmem.sls' '~a/chez/fuse/secmem.sls'\" jerboa-fuse-dir vault-stage))\n ;; Vault layer\n (system (format \"cp '~a/chez/vault.sls' '~a/chez/vault.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/format.sls' '~a/chez/vault/format.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/crypto.sls' '~a/chez/vault/crypto.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/blockstore.sls' '~a/chez/vault/blockstore.sls'\" jerboa-fuse-dir vault-stage))\n ;; Patch out ALL load-shared-object calls (FUSE mount helper + libcrypto + libc)\n ;; Use (if #f #f) instead of (void) since some modules only import (rnrs)\n ;; Simple single-level calls:\n (system (format \"find '~a' -name '*.sls' -exec ~a 's/(load-shared-object[^)]*)/(if #f #f)/g' {} +\" vault-stage sed-inplace))\n ;; fuse.sls and blockstore.sls have multi-line (load-shared-object (case ...)) blocks\n ;; that the simple sed can't handle. Use Scheme to patch them out.\n (let ([str-has? (lambda (haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))])\n (for-each\n (lambda (file-path)\n (when (file-exists? file-path)\n (let* ([content (let ([p (open-input-file file-path)])\n (let loop ([lines '()])\n (let ([l (get-line p)])\n (if (eof-object? l)\n (begin (close-input-port p) (reverse lines))\n (loop (cons l lines))))))]\n [patched\n (let loop ([lines content] [acc '()] [skip 0])\n (if (null? lines)\n (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (or (str-has? line \"(define _libc-loaded\")\n (str-has? line \"(define libc-loaded\")))\n (let ([name (if (str-has? line \"_libc-loaded\")\n \"_libc-loaded\" \"libc-loaded\")])\n (loop (cdr lines)\n (cons (format \" (define ~a #t)\" name) acc)\n 1))]\n [(and (> skip 0) (str-has? line \"#t))\"))\n (loop (cdr lines) acc 0)]\n [(> skip 0)\n (loop (cdr lines) acc skip)]\n [else\n (loop (cdr lines) (cons line acc) 0)]))))])\n (let ([p (open-output-file file-path 'replace)])\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched)\n (close-output-port p)))))\n (list (format \"~a/chez/vault/blockstore.sls\" vault-stage)\n (format \"~a/chez/fuse.sls\" vault-stage)))) ;; close let\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up (format → crypto → secmem → mount → constants → types → codec → access → blockstore → fuse → vault)\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n ;; Format layer (no deps)\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n ;; FUSE foundation\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n ;; Secure memory + access control (depend on mount)\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n ;; Vault crypto (depends on format + libcrypto)\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n ;; Vault blockstore (depends on format + crypto + secmem)\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n ;; FUSE main (depends on all fuse sub-modules)\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n ;; Vault main (depends on everything)\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"~n[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(or (not (file-exists? so))\n (time>? (file-modification-time sls) (file-modification-time so)))\n (printf \" Compiling ~a...~n\" sls)\n (compile-library sls)]\n [else\n (printf \" (up to date) ~a~n\" sls)])))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n ;; Compat layer\n (compile-jsh-module \"../compat/gambit\")\n (for-each compile-jsh-module '(\"ffi\"))\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n (for-each compile-jsh-module '(\"conditions\" \"ast\" \"registry\"))\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-freebsd \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(parameterize ([compile-imported-libraries #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n (compile-program \"jsh-generated.ss\"))\n\n;; Verify jsh-generated.so was created\n(unless (file-exists? \"jsh-generated.so\")\n (fprintf (current-error-port) \"FATAL: jsh-generated.so was not created by compile-program~n\")\n (fprintf (current-error-port) \"Check for compilation errors above.~n\")\n (exit 1))\n\n;; ========== Step 3: Skip WPO ==========\n(printf \"[3/7] Skipping WPO (using jsh-generated.so directly)...~n\")\n(define program-so \"jsh-generated.so\")\n\n;; ========== Step 3.5: Pre-compile boot-file dependencies ==========\n\n(let ([boot-jerboa-modules\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\")])\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 2]\n [generate-inspector-information #f])\n (for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [so (format \"~a/~a.so\" jerboa-dir m)])\n (when (and (file-exists? sls) (not (file-exists? so)))\n (printf \" Pre-compiling ~a~n\" sls)\n (compile-library sls))))\n boot-jerboa-modules)))\n\n;; ========== Step 4: Create libs-only boot file ==========\n\n(printf \"[4/7] Creating libs-only boot file...~n\")\n\n;; Helper to filter existing .so files\n(define (existing-sos dir modules)\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" dir m)) modules)))\n\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (append\n ;; Jerboa runtime + stdlib\n (existing-sos jerboa-dir\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\"\n \"std/regex\"\n \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\"\n \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"))\n ;; Local compat layer\n (list \"src/compat/gambit.so\")\n ;; Additional jerboa stdlib\n (existing-sos jerboa-dir\n '(\"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n ;; Networking: rustls TLS + HTTP/HTTPS client (used by jerboa-aws)\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\"\n \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; jerboa-ssh (agent + client + sub-libraries)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" ssh-stage))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" ssh-stage m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" ssh-stage)))\n '())\n ;; Patched coreutils\n (existing-sos coreutils-stage\n '(\"jerboa-coreutils/common\" \"jerboa-coreutils/common/version\"\n \"jerboa-coreutils/common/security\"\n \"jerboa-coreutils/basename\" \"jerboa-coreutils/dirname\"\n \"jerboa-coreutils/link\" \"jerboa-coreutils/unlink\"\n \"jerboa-coreutils/yes\" \"jerboa-coreutils/printenv\"\n \"jerboa-coreutils/sleep\" \"jerboa-coreutils/whoami\"\n \"jerboa-coreutils/logname\" \"jerboa-coreutils/hostname\"\n \"jerboa-coreutils/nproc\" \"jerboa-coreutils/tty\"\n \"jerboa-coreutils/sync\" \"jerboa-coreutils/hostid\"\n \"jerboa-coreutils/cat\" \"jerboa-coreutils/head\"\n \"jerboa-coreutils/tail\" \"jerboa-coreutils/tac\"\n \"jerboa-coreutils/tee\" \"jerboa-coreutils/wc\"\n \"jerboa-coreutils/nl\" \"jerboa-coreutils/fold\"\n \"jerboa-coreutils/expand\" \"jerboa-coreutils/unexpand\"\n \"jerboa-coreutils/fmt\"\n \"jerboa-coreutils/cut\" \"jerboa-coreutils/paste\"\n \"jerboa-coreutils/join\" \"jerboa-coreutils/comm\"\n \"jerboa-coreutils/sort\" \"jerboa-coreutils/uniq\"\n \"jerboa-coreutils/tr\" \"jerboa-coreutils/numfmt\"\n \"jerboa-coreutils/mkdir\" \"jerboa-coreutils/rmdir\"\n \"jerboa-coreutils/mktemp\" \"jerboa-coreutils/touch\"\n \"jerboa-coreutils/readlink\" \"jerboa-coreutils/realpath\"\n \"jerboa-coreutils/ln\" \"jerboa-coreutils/cp\"\n \"jerboa-coreutils/mv\" \"jerboa-coreutils/rm\"\n \"jerboa-coreutils/install\" \"jerboa-coreutils/shred\"\n \"jerboa-coreutils/ls\" \"jerboa-coreutils/chmod\"\n \"jerboa-coreutils/chown\" \"jerboa-coreutils/chgrp\"\n \"jerboa-coreutils/stat\" \"jerboa-coreutils/du\"\n \"jerboa-coreutils/df\" \"jerboa-coreutils/pathchk\"\n \"jerboa-coreutils/date\" \"jerboa-coreutils/id\"\n \"jerboa-coreutils/groups\" \"jerboa-coreutils/who\"\n \"jerboa-coreutils/users\" \"jerboa-coreutils/pinky\"\n \"jerboa-coreutils/uptime\" \"jerboa-coreutils/uname\"\n \"jerboa-coreutils/arch\"\n \"jerboa-coreutils/seq\" \"jerboa-coreutils/expr\"\n \"jerboa-coreutils/basenc\" \"jerboa-coreutils/base64\"\n \"jerboa-coreutils/base32\" \"jerboa-coreutils/od\"\n \"jerboa-coreutils/cksum\" \"jerboa-coreutils/md5sum\"\n \"jerboa-coreutils/sha1sum\" \"jerboa-coreutils/sha224sum\"\n \"jerboa-coreutils/sha256sum\" \"jerboa-coreutils/sha384sum\"\n \"jerboa-coreutils/sha512sum\" \"jerboa-coreutils/b2sum\"\n \"jerboa-coreutils/sum\"\n \"jerboa-coreutils/env\" \"jerboa-coreutils/timeout\"\n \"jerboa-coreutils/nice\" \"jerboa-coreutils/nohup\"\n \"jerboa-coreutils/chroot\" \"jerboa-coreutils/stdbuf\"\n \"jerboa-coreutils/truncate\" \"jerboa-coreutils/mkfifo\"\n \"jerboa-coreutils/mknod\" \"jerboa-coreutils/split\"\n \"jerboa-coreutils/csplit\" \"jerboa-coreutils/dd\"\n \"jerboa-coreutils/dircolors\"\n \"jerboa-coreutils/tsort\" \"jerboa-coreutils/shuf\"\n \"jerboa-coreutils/factor\" \"jerboa-coreutils/pr\"\n \"jerboa-coreutils/ptx\" \"jerboa-coreutils/stty\"\n \"jerboa-coreutils/chcon\" \"jerboa-coreutils/runcon\"\n \"jerboa-coreutils/dir\" \"jerboa-coreutils/vdir\"\n \"jerboa-coreutils/rev\" \"jerboa-coreutils/top\"\n \"jerboa-coreutils/grep/pcre2\" \"jerboa-coreutils/grep\"))\n ;; jerboa-awk\n (existing-sos awk-stage\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed\n (existing-sos sed-stage\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-ssl + jerboa-https removed — jerboa-aws now uses (std net request) (rustls)\n ;; jerboa-fuse (vault)\n (if has-jerboa-fuse?\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" vault-stage m))\n '(\"chez/vault/format\" \"chez/fuse/constants\" \"chez/fuse/types\"\n \"chez/fuse/mount\" \"chez/fuse/codec\" \"chez/fuse/secmem\" \"chez/fuse/access\"\n \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\")))\n '())\n ;; jerboa-aws (if available)\n (if has-aws?\n (existing-sos aws-stage\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\" \"jerboa-aws/time\"\n \"jerboa-aws/crypto\" \"jerboa-aws/creds\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/request\" \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jsh modules\n (map (lambda (m) (format \"src/jsh/~a.so\" m))\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\" \"config\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"\n \"coreutils\"))))\n\n;; Verify jsh.boot was created\n(unless (file-exists? \"jsh.boot\")\n (fprintf (current-error-port) \"FATAL: jsh.boot was not created by make-boot-file~n\")\n (fprintf (current-error-port) \"Check for compilation/boot errors above.~n\")\n (exit 1))\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-freebsd-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n;; JSH_CROSS_CC overrides the compiler for cross-compilation (e.g. from a container)\n(define gcc (or (getenv \"JSH_CROSS_CC\") \"cc\"))\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" home-dir \"=~\"))\n\n;; Helper: write file as C byte array directly to output port (avoids O(n^2) string-append)\n(define (write-c-array filepath varname out)\n (let* ([bv (call-with-port (open-file-input-port filepath) get-bytevector-all)]\n [len (bytevector-length bv)]\n [hex \"0123456789abcdef\"])\n (fprintf out \"static const unsigned char ~a[] = {~n\" varname)\n (do ([i 0 (+ i 1)])\n ((= i len))\n (when (and (> i 0) (= (mod i 16) 0)) (display \",\\n\" out))\n (when (and (> i 0) (not (= (mod i 16) 0))) (display \",\" out))\n (display \"0x\" out)\n (let ([b (bytevector-u8-ref bv i)])\n (display (string-ref hex (fxsrl b 4)) out)\n (display (string-ref hex (fxand b 15)) out)))\n (fprintf out \"~n};~nstatic const unsigned int ~a_len = ~a;~n\" varname len)))\n\n;; Generate static_boot.c\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n (write-c-array petite-boot-path \"petite_boot\" out) (newline out)\n (write-c-array scheme-boot-path \"scheme_boot\" out) (newline out)\n (write-c-array \"jsh.boot\" \"jsh_boot\" out) (newline out)\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", jsh_boot, jsh_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n;; FFI symbol whitelist — auto-generated from ffi-shim.c plus a small set of\n;; non-ffi_ helpers (Cage/Landlock wrappers, Rust-native shims).\n(define ffi-shim-symbols\n (append (read-symbol-list \"ffi-shim-symbols.list\")\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\")))\n\n(define native-symbols\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\"\n \"jerboa_x509_cert_fingerprint\"\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n\n(define native-int-symbols\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n\n;; High-level jsh_* coreutils commands (from Rust jerboa-coreutils).\n;; On FreeBSD these are stubbed out — the Rust coreutils lib is not yet built.\n(define jsh-coreutils-commands\n '(\"jsh_arch\" \"jsh_b2sum\" \"jsh_base32\" \"jsh_base64\" \"jsh_basename\" \"jsh_basenc\"\n \"jsh_cat\" \"jsh_chgrp\" \"jsh_chmod\" \"jsh_chown\" \"jsh_chroot\" \"jsh_cksum\"\n \"jsh_comm\" \"jsh_cp\" \"jsh_csplit\" \"jsh_cu_realpath\" \"jsh_cut\" \"jsh_date\"\n \"jsh_dd\" \"jsh_df\" \"jsh_dir\" \"jsh_dircolors\" \"jsh_dirname\" \"jsh_du\"\n \"jsh_echo\" \"jsh_env\" \"jsh_expand\" \"jsh_expr\" \"jsh_factor\" \"jsh_fmt\"\n \"jsh_fold\" \"jsh_grep\" \"jsh_groups\" \"jsh_head\" \"jsh_hostid\" \"jsh_hostname\"\n \"jsh_id\" \"jsh_install\" \"jsh_join\" \"jsh_kill\" \"jsh_link\" \"jsh_ln\"\n \"jsh_logname\" \"jsh_ls\" \"jsh_md5sum\" \"jsh_mkdir\" \"jsh_mkfifo\" \"jsh_mknod\"\n \"jsh_mktemp\" \"jsh_mv\" \"jsh_nice\" \"jsh_nl\" \"jsh_nohup\" \"jsh_nproc\"\n \"jsh_numfmt\" \"jsh_od\" \"jsh_paste\" \"jsh_pathchk\" \"jsh_pinky\" \"jsh_pr\"\n \"jsh_printenv\" \"jsh_printf\" \"jsh_ptx\" \"jsh_pwd\" \"jsh_readlink\" \"jsh_rm\"\n \"jsh_rmdir\" \"jsh_seq\" \"jsh_sha1sum\" \"jsh_sha224sum\" \"jsh_sha256sum\"\n \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_shred\" \"jsh_shuf\" \"jsh_sleep\"\n \"jsh_sort\" \"jsh_split\" \"jsh_stat\" \"jsh_stty\" \"jsh_sum\" \"jsh_sync\"\n \"jsh_tac\" \"jsh_tail\" \"jsh_tee\" \"jsh_test\" \"jsh_timeout\" \"jsh_touch\"\n \"jsh_tr\" \"jsh_truncate\" \"jsh_tsort\" \"jsh_tty\" \"jsh_uname\" \"jsh_unexpand\"\n \"jsh_uniq\" \"jsh_unlink\" \"jsh_uptime\" \"jsh_users\" \"jsh_vdir\" \"jsh_wc\"\n \"jsh_who\" \"jsh_whoami\" \"jsh_yes\"))\n\n(define coreutils-symbols\n '(\"coreutils_chmod\" \"coreutils_lstat_mode\" \"coreutils_stat_isdir\"\n \"coreutils_chown\" \"coreutils_lchown\"\n \"coreutils_getpwnam_uid\" \"coreutils_getgrnam_gid\"\n \"coreutils_stat_call\" \"coreutils_stat_get\"\n \"coreutils_uid_to_name\" \"coreutils_gid_to_name\"\n \"coreutils_du_stat\" \"coreutils_statvfs\" \"coreutils_statvfs_get\"\n \"coreutils_test_access\" \"coreutils_test_stat\"\n \"coreutils_ls_lstat\" \"coreutils_ls_stat_get\" \"coreutils_ls_readlink\"\n \"coreutils_isatty\" \"coreutils_time_format\"\n \"coreutils_terminal_width\" \"coreutils_terminal_height\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_cp_lstat\" \"coreutils_cp_stat_get\" \"coreutils_cp_readlink\"\n \"coreutils_symlink\" \"coreutils_link\" \"coreutils_utime\"\n \"coreutils_mkdir\" \"coreutils_lstat_type\"\n \"coreutils_unlink\" \"coreutils_rmdir\" \"coreutils_access_w\"\n \"coreutils_rename\" \"coreutils_stat_get_mode\"\n \"coreutils_stat_atime\" \"coreutils_stat_mtime\"\n \"coreutils_file_size\" \"coreutils_fsync\"\n \"coreutils_chgrp_chown\" \"coreutils_chgrp_lchown\"\n \"coreutils_mkstemp\" \"coreutils_mkstemp_get_path\"\n \"coreutils_mkdtemp\" \"coreutils_readlink\" \"coreutils_realpath\"\n \"coreutils_stat_size\" \"coreutils_fsync_path\"))\n\n(define ssh-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n\n;; jerboa_ssh_crypto.c symbols (used by ssh/transport sub-library)\n(define ssh-crypto-symbols\n '(\"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\" \"jerboa_ssh_sha512\"\n \"jerboa_ssh_hmac_sha256\" \"jerboa_ssh_hmac_sha512\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_aes256_ctr_init\" \"jerboa_ssh_aes256_ctr_process\" \"jerboa_ssh_aes256_ctr_free\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_ed25519_sign\" \"jerboa_ssh_ed25519_derive_pubkey\"\n \"jerboa_ssh_tcp_connect\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_write\"\n \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_set_nodelay\"))\n\n;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n(define vault-fuse-symbols\n '(;; Secure memory\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n ;; Process tree\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n ;; FUSE device + mount\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n\n;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n;; jerboa_aead_seal, jerboa_aead_open — all in libjerboa_native (ring). No libcrypto needed.\n;; POSIX symbols needed by vault code (pread/pwrite for file I/O, fsync, uid/gid)\n(define vault-crypto-symbols\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\"))\n\n;; Generate jsh_main_freebsd.c\n(define program-c (format \"~a/jsh_main_freebsd.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include <dlfcn.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n\n ;; Embed program .so\n (write-c-array program-so \"jsh_program_data\" out)\n (newline out)\n\n ;; Declare static_boot_init\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n\n ;; Declare FFI symbols\n (display \"/* FFI symbols from ffi-shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n\n ;; Rust native symbols\n (when has-native-lib?\n (display \"\\n#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n native-symbols)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n native-int-symbols)\n (display \"#endif\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n\n ;; Coreutils FFI\n (display \"\\n/* FFI symbols from libcoreutils.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n coreutils-symbols)\n\n ;; High-level jsh_* coreutils commands (from Rust libjsh_coreutils.a)\n (display \"\\n/* jsh_* coreutils commands */\\n\" out)\n (if has-rust-coreutils?\n (begin\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n jsh-coreutils-commands))\n (begin\n (display \"/* Stubs — Rust coreutils not built */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"int ~a(int ac, const char **av) { return 127; }\\n\" name))\n jsh-coreutils-commands)\n (display \"void jsh_coreutils_init(int a, char **b) { }\\n\" out)))\n\n ;; jerboa-ssh (shim only; crypto symbols resolved lazily)\n (display \"\\n/* FFI symbols from jerboa_ssh_shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ssh-symbols)\n\n ;; jerboa-fuse vault (crypto symbols now from libjerboa_native via ring)\n (display \"/* FFI symbols for vault (from ffi-shim.c + libjerboa_native) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n vault-fuse-symbols)\n (newline out)\n\n ;; POSIX wrappers\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n\n ;; FreeBSD errno compatibility — __errno_location doesn't exist on FreeBSD\n (display \"/* FreeBSD errno compatibility */\\n\" out)\n (display \"static int *freebsd_errno_location(void) { return &errno; }\\n\\n\" out)\n\n ;; Stubs for symbols not available in FreeBSD native lib\n ;; (regex extended, epoll, inotify, landlock, seccomp)\n (display \"/* Stubs for Linux-only / missing native symbols */\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n (display \"void *jerboa_regex_compile_ex(const char *p, int f) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_find_at(void *r, const char *s, int o, int *ms, int *me) { return 0; }\\n\" out)\n (display \"char *jerboa_regex_captures(void *r, const char *s, int n) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_group_count(void *r) { return 0; }\\n\" out)\n (display \"int jerboa_epoll_create(void) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_ctl(int e, int o, int f, int ev) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_wait(int e, void *ev, int m, int t) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_close(int e) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_init(void) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_add_watch(int f, const char *p, int m) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_rm_watch(int f, int w) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_read(int f, void *b, int s) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_close(int f) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_create_ruleset(void) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_path_rule(int r, const char *p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_net_rule(int r, int p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_enforce(int r) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_available(void) { return 0; }\\n\" out)\n (display \"int jerboa_seccomp_lock(void) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_lock_strict(void) { return -1; }\\n\\n\" out)\n\n ;; register_ffi_symbols\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Rust native\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append native-symbols native-int-symbols))\n (display \"#endif\\n\" out))\n ;; POSIX\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"readlink\" \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"\n ;; vault blockstore\n \"flock\" \"pread\" \"pwrite\" \"fsync\"\n ;; top builtin\n \"setpriority\"))\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\" \"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; FreeBSD: __errno_location and __error → our FreeBSD wrapper\n ;; __errno_location is Linux glibc; __error is FreeBSD libc\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)freebsd_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)freebsd_errno_location);\\n\" out)\n ;; Register stub symbols for Linux-only / missing native functionality\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n ;; coreutils\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n coreutils-symbols)\n ;; jsh_* coreutils commands (stubs)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n jsh-coreutils-commands)\n (fprintf out \" Sforeign_symbol(\\\"jsh_coreutils_init\\\", (void*)jsh_coreutils_init);\\n\")\n ;; jerboa-ssh (shim symbols only; crypto symbols resolved lazily at runtime)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ssh-symbols)\n ;; jerboa-fuse vault\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-fuse-symbols)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-crypto-symbols)\n ;; Sockets\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"htons\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"))\n (display \"}\\n\\n\" out)\n\n ;; Custom main — FreeBSD\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; FreeBSD: sysctl for exe path\n (display \" /* Resolve exe path via sysctl (FreeBSD) */\\n\" out)\n (display \" {\\n\" out)\n (display \" int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" size_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (sysctl(mib, 4, exe_buf, &exe_len, NULL, 0) == 0) {\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; FreeBSD: Always use tmpfile since fdescfs (/dev/fd/) may not be mounted.\n ;; memfd_create exists on FreeBSD 13+ but /dev/fd/N requires fdescfs.\n (display \" /* FreeBSD: extract program .so to tmpfile */\\n\" out)\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-program-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program_data, 1, jsh_program_data_len, fp) != jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"fwrite tmpfile\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C ==========\n\n(printf \"[6/7] Compiling C with cc (clang)...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-freebsd \"Command failed\" cmd)))\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_freebsd.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_freebsd.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; landlock-shim.c — Landlock is Linux-only; always use stub on FreeBSD\n(begin\n (printf \" Landlock is Linux-only, generating stub for FreeBSD~n\")\n (system (format \"echo 'int ffi_landlock_abi_version(void) { return -1; } int ffi_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int ffi_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, int p) { return -1; } int ffi_landlock_create_ruleset(void) { return -1; } int ffi_landlock_add_path_rule(int a, const char *b, int c) { return -1; } int ffi_landlock_add_net_rule(int a, int b, int c) { return -1; } int ffi_landlock_enforce(int a) { return -1; } int jerboa_landlock_abi_version(void) { return -1; } int jerboa_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int jerboa_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, unsigned long long p) { return -1; }' | ~a -c -x c ~a -o '~a/landlock-shim.o' -\"\n gcc harden-cflags build-dir)))\n\n;; coreutils FFI shim\n(if (file-exists? coreutils-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir coreutils-shim))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; jerboa-ssh shim\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Use standalone ed25519 backend (Rust libjerboa_native provides the symbols)\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c — no longer compiled (OpenSSL removed);\n ;; SSH crypto symbols resolved lazily at runtime if SSH is used.\n ;; Generate empty .o placeholder for the linker.\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir)))\n\n (begin\n (printf \" Warning: jerboa-ssh shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-ssl shim — no longer compiled; TLS now via jerboa_tls_* (rustls)\n(printf \" [skip] jerboa-ssl shim — replaced by jerboa_tls_* (rustls)~n\")\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking static jsh-freebsd binary...~n\")\n\n;; FreeBSD static link compat: Rust coreutils references readdir_r@FBSD_1.5\n;; (versioned symbol from shared libc) but libc.a only has the unversioned symbol.\n;; Generate a small compat .o that provides the versioned symbol.\n(let ([compat-c (format \"~a/fbsd_compat.c\" build-dir)]\n [compat-o (format \"~a/fbsd_compat.o\" build-dir)])\n (call-with-output-file compat-c\n (lambda (out)\n (display \"#include <dirent.h>\\n\" out)\n (display \"__asm__(\\\".symver readdir_r_impl, readdir_r@FBSD_1.5\\\");\\n\" out)\n (display \"int readdir_r_impl(DIR *dirp, struct dirent *entry, struct dirent **result) {\\n\" out)\n (display \" return readdir_r(dirp, entry, result);\\n\" out)\n (display \"}\\n\" out)))\n (run-cmd (format \"~a -c -O2 -w -o '~a' '~a'\" gcc compat-o compat-c)))\n\n(let* ([objs (format \"~a/jsh_main_freebsd.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/coreutils-ffi.o ~a/landlock-shim.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o ~a/fbsd_compat.o\"\n build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir)]\n [native-flag (if has-native-lib? (format \" ~a\" native-lib-path) \"\")]\n [coreutils-flag (if has-rust-coreutils? (format \" ~a\" rust-coreutils-lib-path) \"\")]\n ;; Cross-compilation sysroot: when JSH_CROSS_CC is set, libraries are\n ;; under /freebsd/usr/lib/ instead of /usr/lib/\n [syslib (if (getenv \"JSH_CROSS_CC\") \"/freebsd/usr/lib\" \"/usr/lib\")]\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n [cxx-libs (if has-native-lib?\n (format \" ~a/libc++.a ~a/libcxxrt.a\" syslib syslib)\n \"\")]\n [link-libs (format \"-L~a -L~a -L/usr/local/lib -lkernel -lz -lm -lthr -liconv -lncursesw -luuid -llz4 -lutil\"\n chez-ta6fb syslib)]\n ;; Static link: libgcc_s has no .a — use libgcc.a + libgcc_eh.a instead\n ;; On cross-build with clang, these may not exist — clang uses compiler-rt\n [gcc-static (let ([gcc-a (format \"~a/libgcc.a\" syslib)])\n (if (file-exists? gcc-a)\n (format \" ~a/libgcc.a ~a/libgcc_eh.a\" syslib syslib)\n \"\"))]\n [link-cmd (format \"~a -static -o jsh-freebsd ~a~a~a~a ~a~a -Wl,--allow-multiple-definition\"\n gcc objs native-flag coreutils-flag cxx-libs link-libs gcc-static)])\n (printf \" ~a~n\" link-cmd)\n (run-cmd link-cmd))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? \"jsh-freebsd\")\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-freebsd\"))])\n (run-cmd \"strip jsh-freebsd\")\n (let ([post-size (file-length (open-file-input-port \"jsh-freebsd\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n ;; Compute SHA-256 integrity hash\n (printf \"[harden] Computing integrity hash...~n\")\n ;; FreeBSD uses sha256 -q (not sha256sum)\n (system \"sha256 -q jsh-freebsd | tr -d '\\\\n' > /tmp/_jsh_hash.txt 2>/dev/null || sha256sum jsh-freebsd | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\")\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-freebsd.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-freebsd.sha256 (32 bytes)~n\")))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n;; ssl-stage removed — jerboa-ssl/jerboa-https no longer used (rustls replaces them)\n(when has-aws? (system (format \"rm -rf '~a'\" aws-stage)))\n(system (format \"rm -rf '~a'\" awk-stage))\n(system (format \"rm -rf '~a'\" sed-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Static binary created: jsh-freebsd~n~n\")\n(system \"ls -lh jsh-freebsd\")\n(printf \"~n\")\n(system \"file jsh-freebsd\")\n(printf \"~nTest: ./jsh-freebsd -c 'echo Hello from static jsh'~n\")\n"} +{"text":";; FILE: jerboa-shell/build-jsh-freebsd.ss\n#!chezscheme\n;;; build-jsh-freebsd.ss — Build a fully static jsh binary on FreeBSD\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:... < build-jsh-freebsd.ss\n;;;\n;;; This script:\n;;; 1. Patches coreutils/awk/sed/ssl for static builds (no dlopen)\n;;; 2. Compiles jsh modules (using stock scheme)\n;;; 3. Creates boot file + optimized program .so\n;;; 4. Generates C files with embedded boot data\n;;; 5. Compiles C with cc (clang) against static Chez's scheme.h\n;;; 6. Links fully static binary with libkernel.a\n;;;\n;;; The resulting jsh-freebsd binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/home/freebsd\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-freebsd.sh so we know the repo root.\n(define vendor-dir\n (let ([script-dir (getenv \"SCRIPT_DIR\")])\n (if script-dir\n (format \"~a/vendor\" script-dir)\n (let ([cwd-vendor \"./vendor\"])\n (if (file-directory? cwd-vendor) cwd-vendor\n (format \"~a/jerboa-shell/vendor\" home-dir))))))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. Callers wrap with (or (getenv \"X\") (dep ...))\n;; to allow env var overrides from the shell script.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jerboa-sqlite/src\" home-dir)))\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (dep \"jerboa-coreutils\" \"lib\")))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n(define coreutils-shim\n (let ([upstream (dep-file \"jerboa-coreutils\" \"support/libcoreutils.c\")]\n [local \"patches/libcoreutils.c\"])\n (cond\n [(file-exists? upstream) upstream]\n [(file-exists? local) local]\n [else upstream])))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls).\n;; OpenSSL via load-shared-object cannot work in static builds and rustls is\n;; preferred for security.\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n;; Rust native library — resolve via vendor/ → ~/mine/ → ~/\n(define native-rs-dir\n (let* ([v (format \"~a/jerboa/jerboa-native-rs\" vendor-dir)]\n [m (format \"~a/mine/jerboa/jerboa-native-rs\" home-dir)]\n [h (format \"~a/jerboa/jerboa-native-rs\" home-dir)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n(define native-lib-path\n (format \"~a/target/release/libjerboa_native.a\" native-rs-dir))\n(define native-src-dir\n (format \"~a/src\" native-rs-dir))\n;; Sentinel file written after a successful native build without SQLite.\n;; If absent, the .a was built with default (tls-only) features — must rebuild.\n(define native-features-sentinel\n (format \"~a/target/release/.built-with-tls-crypto-no-sqlite\" native-rs-dir))\n(when (and (file-exists? native-src-dir)\n (or (not (file-exists? native-lib-path))\n ;; Features sentinel absent → stale build (wrong feature set)\n (not (file-exists? native-features-sentinel))\n ;; Check if any .rs file is newer than the .a\n (let ([lib-mtime (file-modification-time native-lib-path)])\n (let check ([files (directory-list native-src-dir)])\n (and (pair? files)\n (let ([f (format \"~a/~a\" native-src-dir (car files))])\n (or (and (> (string-length (car files)) 3)\n (string=? \".rs\" (substring (car files)\n (- (string-length (car files)) 3)\n (string-length (car files))))\n (time>? (file-modification-time f) lib-mtime))\n (check (cdr files)))))))))\n (printf \"~n[0/7] Rebuilding Rust native library (source newer than .a)...~n\")\n (let ([rc (safe-system (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote native-rs-dir)))])\n (unless (= rc 0)\n (fprintf (current-error-port) \"FATAL: cargo build --release --no-default-features --features tls,crypto failed~n\")\n (exit 1)))\n ;; Write sentinel so next build knows the right features were used\n (let ([port (open-output-file native-features-sentinel 'truncate)])\n (display \"tls,crypto,no-sqlite\\n\" port)\n (close-output-port port)))\n(when (and (file-exists? native-lib-path)\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(define has-native-lib? (file-exists? native-lib-path))\n\n;; Rust coreutils static library — check current dir first (container build), then home\n(define rust-coreutils-lib-path\n (let ([local (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" (current-directory))]\n [home-path (format \"~a/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\" home-dir)]\n [mine-path (format \"~a/mine/jerboa-shell/rust-coreutils/target/release/libjsh_coreutils.a\" home-dir)])\n (cond\n [(file-exists? local) local]\n [(file-exists? mine-path) mine-path]\n [else home-path])))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — coreutils builtins will be stubs~n\"))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\"))\n\n;; Chez Scheme static installation\n(define chez-ta6fb\n (or (getenv \"CHEZ_TA6FB\")\n (let ([dirs (directory-list \"/usr/local/lib\")])\n (let ([csv-dir (find (lambda (d) (string-prefix? \"csv\" d)) dirs)])\n (if csv-dir\n (format \"/usr/local/lib/~a/ta6fb\" csv-dir)\n (error 'build \"Cannot find Chez ta6fb directory in /usr/local/lib\"))))))\n\n(define scheme-h-dir chez-ta6fb)\n(define petite-boot-path (format \"~a/petite.boot\" chez-ta6fb))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-ta6fb))\n\n(printf \"Chez static: ~a~n\" chez-ta6fb)\n(printf \"Native lib: ~a~n\" (if has-native-lib? native-lib-path \"not found\"))\n(printf \"~n\")\n\n;; ========== Step 0: Patch coreutils for static builds ==========\n;; Coreutils modules call (load-shared-object #f) at library init time.\n;; In static builds, load-shared-object throws because dlopen is unavailable.\n;; Since FFI symbols are pre-registered via Sforeign_symbol, we patch these out.\n\n;; Detect sed -i syntax: FreeBSD uses `sed -i ''`, GNU sed uses `sed -i`\n(define sed-inplace\n (if (= 0 (system \"sed --version 2>/dev/null | head -1 | grep -q GNU\"))\n \"sed -i\" ;; GNU sed (Linux)\n \"sed -i ''\")) ;; BSD sed (FreeBSD/macOS)\n\n(printf \"[0/7] Patching coreutils for static build (no dlopen)...~n\")\n\n(define coreutils-stage (format \"~a/coreutils-stage\" (current-directory)))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(system (format \"mkdir -p '~a'\" coreutils-stage))\n\n(system (format \"cp -a '~a/jerboa-coreutils' '~a/'\"\n coreutils-dir coreutils-stage))\n;; Patch load-shared-object calls (incompatible with static linking)\n(system (format \"find '~a/jerboa-coreutils' -name '*.sls' -exec ~a 's/(load-shared-object #f)/(void)/g' {} +\"\n coreutils-stage sed-inplace))\n(system (format \"find '~a/jerboa-coreutils' -name '*.so' -delete\"\n coreutils-stage))\n(system (format \"find '~a/jerboa-coreutils' -name '*.wpo' -delete\"\n coreutils-stage))\n\n(printf \" Recompiling patched coreutils...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"common.sls\" \"common/version.sls\" \"common/io.sls\" \"common/security.sls\"))\n (for-each\n (lambda (name)\n (let ([sls (format \"~a/jerboa-coreutils/~a.sls\" coreutils-stage name)])\n (when (file-exists? sls)\n (compile-library sls))))\n '(\"basename\" \"dirname\" \"link\" \"unlink\" \"yes\" \"printenv\"\n \"sleep\" \"whoami\" \"logname\" \"hostname\" \"nproc\" \"tty\" \"sync\" \"hostid\"\n \"cat\" \"head\" \"tail\" \"tac\" \"tee\" \"wc\" \"nl\" \"fold\" \"expand\" \"unexpand\" \"fmt\"\n \"cut\" \"paste\" \"join\" \"comm\" \"sort\" \"uniq\" \"tr\" \"numfmt\"\n \"mkdir\" \"rmdir\" \"mktemp\" \"touch\" \"readlink\" \"realpath\" \"ln\" \"cp\" \"mv\" \"rm\"\n \"install\" \"shred\"\n \"ls\" \"chmod\" \"chown\" \"chgrp\" \"stat\" \"du\" \"df\" \"pathchk\"\n \"date\" \"id\" \"groups\" \"who\" \"users\" \"pinky\" \"uptime\" \"uname\" \"arch\"\n \"seq\" \"expr\" \"basenc\" \"base64\" \"base32\" \"od\"\n \"cksum\" \"md5sum\" \"sha1sum\" \"sha224sum\" \"sha256sum\" \"sha384sum\" \"sha512sum\"\n \"b2sum\" \"sum\"\n \"env\" \"timeout\" \"nice\" \"nohup\" \"chroot\" \"stdbuf\"\n \"truncate\" \"mkfifo\" \"mknod\" \"split\" \"csplit\" \"dd\" \"dircolors\"\n \"tsort\" \"shuf\" \"factor\" \"pr\" \"ptx\" \"stty\"\n \"chcon\" \"runcon\"\n \"dir\" \"vdir\" \"rev\" \"top\")))\n\n;; grep + Rust-backed PCRE2\n(let ([grep-pcre2-patch (format \"~a/patches/grep-pcre2.sls\" (current-directory))])\n (when (file-exists? grep-pcre2-patch)\n (system (format \"mkdir -p '~a/jerboa-coreutils/grep'\" coreutils-stage))\n (system (format \"cp '~a' '~a/jerboa-coreutils/grep/pcre2.sls'\"\n grep-pcre2-patch coreutils-stage))))\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (let ([pcre2-sls (format \"~a/jerboa-coreutils/grep/pcre2.sls\" coreutils-stage)])\n (when (file-exists? pcre2-sls)\n (printf \" Compiling grep/pcre2...~n\")\n (compile-library pcre2-sls)))\n (let ([grep-sls (format \"~a/jerboa-coreutils/grep.sls\" coreutils-stage)])\n (when (file-exists? grep-sls)\n (printf \" Compiling grep...~n\")\n (compile-library grep-sls))))\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: patch pcre2 to use Rust regex\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if has-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when has-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (library-directories))])\n (when has-aws?\n (printf \" Compiling jerboa-aws...~n\")\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0d: Stage jerboa-ssh for static build ==========\n(printf \"[0d/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build\n (system (format \"find '~a' -name '*.sls' -exec ~a 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage sed-inplace))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"~a 's/base64-encode/b64-encode/g' '~a'\" sed-inplace kh))\n (system (format \"~a 's/base64-decode/b64-decode/g' '~a'\" sed-inplace kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0e: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0e/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n(define vault-stage (format \"~a/vault-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" vault-stage vault-stage))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n(when has-jerboa-fuse?\n ;; Copy the jerboa-fuse library tree (chez/fuse/ and chez/vault/)\n (system (format \"mkdir -p '~a/chez/fuse' '~a/chez/vault'\" vault-stage vault-stage))\n ;; FUSE layer\n (system (format \"cp '~a/chez/fuse.sls' '~a/chez/fuse.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/constants.sls' '~a/chez/fuse/constants.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/types.sls' '~a/chez/fuse/types.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/codec.sls' '~a/chez/fuse/codec.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/mount.sls' '~a/chez/fuse/mount.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/access.sls' '~a/chez/fuse/access.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/secmem.sls' '~a/chez/fuse/secmem.sls'\" jerboa-fuse-dir vault-stage))\n ;; Vault layer\n (system (format \"cp '~a/chez/vault.sls' '~a/chez/vault.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/format.sls' '~a/chez/vault/format.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/crypto.sls' '~a/chez/vault/crypto.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/blockstore.sls' '~a/chez/vault/blockstore.sls'\" jerboa-fuse-dir vault-stage))\n ;; Patch out ALL load-shared-object calls (FUSE mount helper + libcrypto + libc)\n ;; Use (if #f #f) instead of (void) since some modules only import (rnrs)\n ;; Simple single-level calls:\n (system (format \"find '~a' -name '*.sls' -exec ~a 's/(load-shared-object[^)]*)/(if #f #f)/g' {} +\" vault-stage sed-inplace))\n ;; fuse.sls and blockstore.sls have multi-line (load-shared-object (case ...)) blocks\n ;; that the simple sed can't handle. Use Scheme to patch them out.\n (let ([str-has? (lambda (haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))])\n (for-each\n (lambda (file-path)\n (when (file-exists? file-path)\n (let* ([content (let ([p (open-input-file file-path)])\n (let loop ([lines '()])\n (let ([l (get-line p)])\n (if (eof-object? l)\n (begin (close-input-port p) (reverse lines))\n (loop (cons l lines))))))]\n [patched\n (let loop ([lines content] [acc '()] [skip 0])\n (if (null? lines)\n (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (or (str-has? line \"(define _libc-loaded\")\n (str-has? line \"(define libc-loaded\")))\n (let ([name (if (str-has? line \"_libc-loaded\")\n \"_libc-loaded\" \"libc-loaded\")])\n (loop (cdr lines)\n (cons (format \" (define ~a #t)\" name) acc)\n 1))]\n [(and (> skip 0) (str-has? line \"#t))\"))\n (loop (cdr lines) acc 0)]\n [(> skip 0)\n (loop (cdr lines) acc skip)]\n [else\n (loop (cdr lines) (cons line acc) 0)]))))])\n (let ([p (open-output-file file-path 'replace)])\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched)\n (close-output-port p)))))\n (list (format \"~a/chez/vault/blockstore.sls\" vault-stage)\n (format \"~a/chez/fuse.sls\" vault-stage)))) ;; close let\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up (format → crypto → secmem → mount → constants → types → codec → access → blockstore → fuse → vault)\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n ;; Format layer (no deps)\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n ;; FUSE foundation\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n ;; Secure memory + access control (depend on mount)\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n ;; Vault crypto (depends on format + libcrypto)\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n ;; Vault blockstore (depends on format + crypto + secmem)\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n ;; FUSE main (depends on all fuse sub-modules)\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n ;; Vault main (depends on everything)\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"~n[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(or (not (file-exists? so))\n (time>? (file-modification-time sls) (file-modification-time so)))\n (printf \" Compiling ~a...~n\" sls)\n (compile-library sls)]\n [else\n (printf \" (up to date) ~a~n\" sls)])))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n ;; Compat layer\n (compile-jsh-module \"../compat/gambit\")\n (for-each compile-jsh-module '(\"ffi\"))\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n (for-each compile-jsh-module '(\"conditions\" \"ast\" \"registry\"))\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-freebsd \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(parameterize ([compile-imported-libraries #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (append\n (if has-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n (compile-program \"jsh-generated.ss\"))\n\n;; Verify jsh-generated.so was created\n(unless (file-exists? \"jsh-generated.so\")\n (fprintf (current-error-port) \"FATAL: jsh-generated.so was not created by compile-program~n\")\n (fprintf (current-error-port) \"Check for compilation errors above.~n\")\n (exit 1))\n\n;; ========== Step 3: Skip WPO ==========\n(printf \"[3/7] Skipping WPO (using jsh-generated.so directly)...~n\")\n(define program-so \"jsh-generated.so\")\n\n;; ========== Step 3.5: Pre-compile boot-file dependencies ==========\n\n(let ([boot-jerboa-modules\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\")])\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 2]\n [generate-inspector-information #f])\n (for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [so (format \"~a/~a.so\" jerboa-dir m)])\n (when (and (file-exists? sls) (not (file-exists? so)))\n (printf \" Pre-compiling ~a~n\" sls)\n (compile-library sls))))\n boot-jerboa-modules)))\n\n;; ========== Step 4: Create libs-only boot file ==========\n\n(printf \"[4/7] Creating libs-only boot file...~n\")\n\n;; Helper to filter existing .so files\n(define (existing-sos dir modules)\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" dir m)) modules)))\n\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (append\n ;; Jerboa runtime + stdlib\n (existing-sos jerboa-dir\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\"\n \"std/regex\"\n \"std/match2\" \"std/sugar\"\n \"std/misc/string\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\"\n \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"))\n ;; Local compat layer\n (list \"src/compat/gambit.so\")\n ;; Additional jerboa stdlib\n (existing-sos jerboa-dir\n '(\"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n ;; Networking: rustls TLS + HTTP/HTTPS client (used by jerboa-aws)\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\"\n \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; jerboa-ssh (agent + client + sub-libraries)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" ssh-stage))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" ssh-stage m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" ssh-stage)))\n '())\n ;; Patched coreutils\n (existing-sos coreutils-stage\n '(\"jerboa-coreutils/common\" \"jerboa-coreutils/common/version\"\n \"jerboa-coreutils/common/security\"\n \"jerboa-coreutils/basename\" \"jerboa-coreutils/dirname\"\n \"jerboa-coreutils/link\" \"jerboa-coreutils/unlink\"\n \"jerboa-coreutils/yes\" \"jerboa-coreutils/printenv\"\n \"jerboa-coreutils/sleep\" \"jerboa-coreutils/whoami\"\n \"jerboa-coreutils/logname\" \"jerboa-coreutils/hostname\"\n \"jerboa-coreutils/nproc\" \"jerboa-coreutils/tty\"\n \"jerboa-coreutils/sync\" \"jerboa-coreutils/hostid\"\n \"jerboa-coreutils/cat\" \"jerboa-coreutils/head\"\n \"jerboa-coreutils/tail\" \"jerboa-coreutils/tac\"\n \"jerboa-coreutils/tee\" \"jerboa-coreutils/wc\"\n \"jerboa-coreutils/nl\" \"jerboa-coreutils/fold\"\n \"jerboa-coreutils/expand\" \"jerboa-coreutils/unexpand\"\n \"jerboa-coreutils/fmt\"\n \"jerboa-coreutils/cut\" \"jerboa-coreutils/paste\"\n \"jerboa-coreutils/join\" \"jerboa-coreutils/comm\"\n \"jerboa-coreutils/sort\" \"jerboa-coreutils/uniq\"\n \"jerboa-coreutils/tr\" \"jerboa-coreutils/numfmt\"\n \"jerboa-coreutils/mkdir\" \"jerboa-coreutils/rmdir\"\n \"jerboa-coreutils/mktemp\" \"jerboa-coreutils/touch\"\n \"jerboa-coreutils/readlink\" \"jerboa-coreutils/realpath\"\n \"jerboa-coreutils/ln\" \"jerboa-coreutils/cp\"\n \"jerboa-coreutils/mv\" \"jerboa-coreutils/rm\"\n \"jerboa-coreutils/install\" \"jerboa-coreutils/shred\"\n \"jerboa-coreutils/ls\" \"jerboa-coreutils/chmod\"\n \"jerboa-coreutils/chown\" \"jerboa-coreutils/chgrp\"\n \"jerboa-coreutils/stat\" \"jerboa-coreutils/du\"\n \"jerboa-coreutils/df\" \"jerboa-coreutils/pathchk\"\n \"jerboa-coreutils/date\" \"jerboa-coreutils/id\"\n \"jerboa-coreutils/groups\" \"jerboa-coreutils/who\"\n \"jerboa-coreutils/users\" \"jerboa-coreutils/pinky\"\n \"jerboa-coreutils/uptime\" \"jerboa-coreutils/uname\"\n \"jerboa-coreutils/arch\"\n \"jerboa-coreutils/seq\" \"jerboa-coreutils/expr\"\n \"jerboa-coreutils/basenc\" \"jerboa-coreutils/base64\"\n \"jerboa-coreutils/base32\" \"jerboa-coreutils/od\"\n \"jerboa-coreutils/cksum\" \"jerboa-coreutils/md5sum\"\n \"jerboa-coreutils/sha1sum\" \"jerboa-coreutils/sha224sum\"\n \"jerboa-coreutils/sha256sum\" \"jerboa-coreutils/sha384sum\"\n \"jerboa-coreutils/sha512sum\" \"jerboa-coreutils/b2sum\"\n \"jerboa-coreutils/sum\"\n \"jerboa-coreutils/env\" \"jerboa-coreutils/timeout\"\n \"jerboa-coreutils/nice\" \"jerboa-coreutils/nohup\"\n \"jerboa-coreutils/chroot\" \"jerboa-coreutils/stdbuf\"\n \"jerboa-coreutils/truncate\" \"jerboa-coreutils/mkfifo\"\n \"jerboa-coreutils/mknod\" \"jerboa-coreutils/split\"\n \"jerboa-coreutils/csplit\" \"jerboa-coreutils/dd\"\n \"jerboa-coreutils/dircolors\"\n \"jerboa-coreutils/tsort\" \"jerboa-coreutils/shuf\"\n \"jerboa-coreutils/factor\" \"jerboa-coreutils/pr\"\n \"jerboa-coreutils/ptx\" \"jerboa-coreutils/stty\"\n \"jerboa-coreutils/chcon\" \"jerboa-coreutils/runcon\"\n \"jerboa-coreutils/dir\" \"jerboa-coreutils/vdir\"\n \"jerboa-coreutils/rev\" \"jerboa-coreutils/top\"\n \"jerboa-coreutils/grep/pcre2\" \"jerboa-coreutils/grep\"))\n ;; jerboa-awk\n (existing-sos awk-stage\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed\n (existing-sos sed-stage\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-ssl + jerboa-https removed — jerboa-aws now uses (std net request) (rustls)\n ;; jerboa-fuse (vault)\n (if has-jerboa-fuse?\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" vault-stage m))\n '(\"chez/vault/format\" \"chez/fuse/constants\" \"chez/fuse/types\"\n \"chez/fuse/mount\" \"chez/fuse/codec\" \"chez/fuse/secmem\" \"chez/fuse/access\"\n \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\")))\n '())\n ;; jerboa-aws (if available)\n (if has-aws?\n (existing-sos aws-stage\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\" \"jerboa-aws/time\"\n \"jerboa-aws/crypto\" \"jerboa-aws/creds\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/request\" \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jsh modules\n (map (lambda (m) (format \"src/jsh/~a.so\" m))\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\" \"config\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"\n \"coreutils\"))))\n\n;; Verify jsh.boot was created\n(unless (file-exists? \"jsh.boot\")\n (fprintf (current-error-port) \"FATAL: jsh.boot was not created by make-boot-file~n\")\n (fprintf (current-error-port) \"Check for compilation/boot errors above.~n\")\n (exit 1))\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-freebsd-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n;; JSH_CROSS_CC overrides the compiler for cross-compilation (e.g. from a container)\n(define gcc (or (getenv \"JSH_CROSS_CC\") \"cc\"))\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" home-dir \"=~\"))\n\n;; Helper: write file as C byte array directly to output port (avoids O(n^2) string-append)\n(define (write-c-array filepath varname out)\n (let* ([bv (call-with-port (open-file-input-port filepath) get-bytevector-all)]\n [len (bytevector-length bv)]\n [hex \"0123456789abcdef\"])\n (fprintf out \"static const unsigned char ~a[] = {~n\" varname)\n (do ([i 0 (+ i 1)])\n ((= i len))\n (when (and (> i 0) (= (mod i 16) 0)) (display \",\\n\" out))\n (when (and (> i 0) (not (= (mod i 16) 0))) (display \",\" out))\n (display \"0x\" out)\n (let ([b (bytevector-u8-ref bv i)])\n (display (string-ref hex (fxsrl b 4)) out)\n (display (string-ref hex (fxand b 15)) out)))\n (fprintf out \"~n};~nstatic const unsigned int ~a_len = ~a;~n\" varname len)))\n\n;; Generate static_boot.c\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n (write-c-array petite-boot-path \"petite_boot\" out) (newline out)\n (write-c-array scheme-boot-path \"scheme_boot\" out) (newline out)\n (write-c-array \"jsh.boot\" \"jsh_boot\" out) (newline out)\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", jsh_boot, jsh_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n;; FFI symbol whitelist — auto-generated from ffi-shim.c plus a small set of\n;; non-ffi_ helpers (Cage/Landlock wrappers, Rust-native shims).\n(define ffi-shim-symbols\n (append (read-symbol-list \"ffi-shim-symbols.list\")\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\")))\n\n(define native-symbols\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\"\n \"jerboa_x509_cert_fingerprint\"\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n\n(define native-int-symbols\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n\n;; High-level jsh_* coreutils commands (from Rust jerboa-coreutils).\n;; On FreeBSD these are stubbed out — the Rust coreutils lib is not yet built.\n(define jsh-coreutils-commands\n '(\"jsh_arch\" \"jsh_b2sum\" \"jsh_base32\" \"jsh_base64\" \"jsh_basename\" \"jsh_basenc\"\n \"jsh_cat\" \"jsh_chgrp\" \"jsh_chmod\" \"jsh_chown\" \"jsh_chroot\" \"jsh_cksum\"\n \"jsh_comm\" \"jsh_cp\" \"jsh_csplit\" \"jsh_cu_realpath\" \"jsh_cut\" \"jsh_date\"\n \"jsh_dd\" \"jsh_df\" \"jsh_dir\" \"jsh_dircolors\" \"jsh_dirname\" \"jsh_du\"\n \"jsh_echo\" \"jsh_env\" \"jsh_expand\" \"jsh_expr\" \"jsh_factor\" \"jsh_fmt\"\n \"jsh_fold\" \"jsh_grep\" \"jsh_groups\" \"jsh_head\" \"jsh_hostid\" \"jsh_hostname\"\n \"jsh_id\" \"jsh_install\" \"jsh_join\" \"jsh_kill\" \"jsh_link\" \"jsh_ln\"\n \"jsh_logname\" \"jsh_ls\" \"jsh_md5sum\" \"jsh_mkdir\" \"jsh_mkfifo\" \"jsh_mknod\"\n \"jsh_mktemp\" \"jsh_mv\" \"jsh_nice\" \"jsh_nl\" \"jsh_nohup\" \"jsh_nproc\"\n \"jsh_numfmt\" \"jsh_od\" \"jsh_paste\" \"jsh_pathchk\" \"jsh_pinky\" \"jsh_pr\"\n \"jsh_printenv\" \"jsh_printf\" \"jsh_ptx\" \"jsh_pwd\" \"jsh_readlink\" \"jsh_rm\"\n \"jsh_rmdir\" \"jsh_seq\" \"jsh_sha1sum\" \"jsh_sha224sum\" \"jsh_sha256sum\"\n \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_shred\" \"jsh_shuf\" \"jsh_sleep\"\n \"jsh_sort\" \"jsh_split\" \"jsh_stat\" \"jsh_stty\" \"jsh_sum\" \"jsh_sync\"\n \"jsh_tac\" \"jsh_tail\" \"jsh_tee\" \"jsh_test\" \"jsh_timeout\" \"jsh_touch\"\n \"jsh_tr\" \"jsh_truncate\" \"jsh_tsort\" \"jsh_tty\" \"jsh_uname\" \"jsh_unexpand\"\n \"jsh_uniq\" \"jsh_unlink\" \"jsh_uptime\" \"jsh_users\" \"jsh_vdir\" \"jsh_wc\"\n \"jsh_who\" \"jsh_whoami\" \"jsh_yes\"))\n\n(define coreutils-symbols\n '(\"coreutils_chmod\" \"coreutils_lstat_mode\" \"coreutils_stat_isdir\"\n \"coreutils_chown\" \"coreutils_lchown\"\n \"coreutils_getpwnam_uid\" \"coreutils_getgrnam_gid\"\n \"coreutils_stat_call\" \"coreutils_stat_get\"\n \"coreutils_uid_to_name\" \"coreutils_gid_to_name\"\n \"coreutils_du_stat\" \"coreutils_statvfs\" \"coreutils_statvfs_get\"\n \"coreutils_test_access\" \"coreutils_test_stat\"\n \"coreutils_ls_lstat\" \"coreutils_ls_stat_get\" \"coreutils_ls_readlink\"\n \"coreutils_isatty\" \"coreutils_time_format\"\n \"coreutils_terminal_width\" \"coreutils_terminal_height\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_cp_lstat\" \"coreutils_cp_stat_get\" \"coreutils_cp_readlink\"\n \"coreutils_symlink\" \"coreutils_link\" \"coreutils_utime\"\n \"coreutils_mkdir\" \"coreutils_lstat_type\"\n \"coreutils_unlink\" \"coreutils_rmdir\" \"coreutils_access_w\"\n \"coreutils_rename\" \"coreutils_stat_get_mode\"\n \"coreutils_stat_atime\" \"coreutils_stat_mtime\"\n \"coreutils_file_size\" \"coreutils_fsync\"\n \"coreutils_chgrp_chown\" \"coreutils_chgrp_lchown\"\n \"coreutils_mkstemp\" \"coreutils_mkstemp_get_path\"\n \"coreutils_mkdtemp\" \"coreutils_readlink\" \"coreutils_realpath\"\n \"coreutils_stat_size\" \"coreutils_fsync_path\"))\n\n(define ssh-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n\n;; jerboa_ssh_crypto.c symbols (used by ssh/transport sub-library)\n(define ssh-crypto-symbols\n '(\"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\" \"jerboa_ssh_sha512\"\n \"jerboa_ssh_hmac_sha256\" \"jerboa_ssh_hmac_sha512\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_aes256_ctr_init\" \"jerboa_ssh_aes256_ctr_process\" \"jerboa_ssh_aes256_ctr_free\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_ed25519_sign\" \"jerboa_ssh_ed25519_derive_pubkey\"\n \"jerboa_ssh_tcp_connect\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_write\"\n \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_set_nodelay\"))\n\n;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n(define vault-fuse-symbols\n '(;; Secure memory\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n ;; Process tree\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n ;; FUSE device + mount\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n\n;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n;; jerboa_aead_seal, jerboa_aead_open — all in libjerboa_native (ring). No libcrypto needed.\n;; POSIX symbols needed by vault code (pread/pwrite for file I/O, fsync, uid/gid)\n(define vault-crypto-symbols\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\"))\n\n;; Generate jsh_main_freebsd.c\n(define program-c (format \"~a/jsh_main_freebsd.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include <dlfcn.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n\n ;; Embed program .so\n (write-c-array program-so \"jsh_program_data\" out)\n (newline out)\n\n ;; Declare static_boot_init\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n\n ;; Declare FFI symbols\n (display \"/* FFI symbols from ffi-shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n\n ;; Rust native symbols\n (when has-native-lib?\n (display \"\\n#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n native-symbols)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n native-int-symbols)\n (display \"#endif\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n\n ;; Coreutils FFI\n (display \"\\n/* FFI symbols from libcoreutils.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n coreutils-symbols)\n\n ;; High-level jsh_* coreutils commands (from Rust libjsh_coreutils.a)\n (display \"\\n/* jsh_* coreutils commands */\\n\" out)\n (if has-rust-coreutils?\n (begin\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n jsh-coreutils-commands))\n (begin\n (display \"/* Stubs — Rust coreutils not built */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"int ~a(int ac, const char **av) { return 127; }\\n\" name))\n jsh-coreutils-commands)\n (display \"void jsh_coreutils_init(int a, char **b) { }\\n\" out)))\n\n ;; jerboa-ssh (shim only; crypto symbols resolved lazily)\n (display \"\\n/* FFI symbols from jerboa_ssh_shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ssh-symbols)\n\n ;; jerboa-fuse vault (crypto symbols now from libjerboa_native via ring)\n (display \"/* FFI symbols for vault (from ffi-shim.c + libjerboa_native) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n vault-fuse-symbols)\n (newline out)\n\n ;; POSIX wrappers\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n\n ;; FreeBSD errno compatibility — __errno_location doesn't exist on FreeBSD\n (display \"/* FreeBSD errno compatibility */\\n\" out)\n (display \"static int *freebsd_errno_location(void) { return &errno; }\\n\\n\" out)\n\n ;; Stubs for symbols not available in FreeBSD native lib\n ;; (regex extended, epoll, inotify, landlock, seccomp)\n (display \"/* Stubs for Linux-only / missing native symbols */\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n (display \"void *jerboa_regex_compile_ex(const char *p, int f) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_find_at(void *r, const char *s, int o, int *ms, int *me) { return 0; }\\n\" out)\n (display \"char *jerboa_regex_captures(void *r, const char *s, int n) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_group_count(void *r) { return 0; }\\n\" out)\n (display \"int jerboa_epoll_create(void) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_ctl(int e, int o, int f, int ev) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_wait(int e, void *ev, int m, int t) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_close(int e) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_init(void) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_add_watch(int f, const char *p, int m) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_rm_watch(int f, int w) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_read(int f, void *b, int s) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_close(int f) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_create_ruleset(void) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_path_rule(int r, const char *p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_net_rule(int r, int p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_enforce(int r) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_available(void) { return 0; }\\n\" out)\n (display \"int jerboa_seccomp_lock(void) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_lock_strict(void) { return -1; }\\n\\n\" out)\n\n ;; register_ffi_symbols\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Rust native\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append native-symbols native-int-symbols))\n (display \"#endif\\n\" out))\n ;; POSIX\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"readlink\" \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"\n ;; vault blockstore\n \"flock\" \"pread\" \"pwrite\" \"fsync\"\n ;; top builtin\n \"setpriority\"))\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\" \"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; FreeBSD: __errno_location and __error → our FreeBSD wrapper\n ;; __errno_location is Linux glibc; __error is FreeBSD libc\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)freebsd_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)freebsd_errno_location);\\n\" out)\n ;; Register stub symbols for Linux-only / missing native functionality\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n ;; coreutils\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n coreutils-symbols)\n ;; jsh_* coreutils commands (stubs)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n jsh-coreutils-commands)\n (fprintf out \" Sforeign_symbol(\\\"jsh_coreutils_init\\\", (void*)jsh_coreutils_init);\\n\")\n ;; jerboa-ssh (shim symbols only; crypto symbols resolved lazily at runtime)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ssh-symbols)\n ;; jerboa-fuse vault\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-fuse-symbols)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-crypto-symbols)\n ;; Sockets\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"htons\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"))\n (display \"}\\n\\n\" out)\n\n ;; Custom main — FreeBSD\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; FreeBSD: sysctl for exe path\n (display \" /* Resolve exe path via sysctl (FreeBSD) */\\n\" out)\n (display \" {\\n\" out)\n (display \" int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" size_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (sysctl(mib, 4, exe_buf, &exe_len, NULL, 0) == 0) {\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; FreeBSD: Always use tmpfile since fdescfs (/dev/fd/) may not be mounted.\n ;; memfd_create exists on FreeBSD 13+ but /dev/fd/N requires fdescfs.\n (display \" /* FreeBSD: extract program .so to tmpfile */\\n\" out)\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-program-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program_data, 1, jsh_program_data_len, fp) != jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"fwrite tmpfile\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C ==========\n\n(printf \"[6/7] Compiling C with cc (clang)...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-freebsd \"Command failed\" cmd)))\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_freebsd.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_freebsd.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; landlock-shim.c — Landlock is Linux-only; always use stub on FreeBSD\n(begin\n (printf \" Landlock is Linux-only, generating stub for FreeBSD~n\")\n (system (format \"echo 'int ffi_landlock_abi_version(void) { return -1; } int ffi_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int ffi_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, int p) { return -1; } int ffi_landlock_create_ruleset(void) { return -1; } int ffi_landlock_add_path_rule(int a, const char *b, int c) { return -1; } int ffi_landlock_add_net_rule(int a, int b, int c) { return -1; } int ffi_landlock_enforce(int a) { return -1; } int jerboa_landlock_abi_version(void) { return -1; } int jerboa_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int jerboa_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, unsigned long long p) { return -1; }' | ~a -c -x c ~a -o '~a/landlock-shim.o' -\"\n gcc harden-cflags build-dir)))\n\n;; coreutils FFI shim\n(if (file-exists? coreutils-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir coreutils-shim))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; jerboa-ssh shim\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Use standalone ed25519 backend (Rust libjerboa_native provides the symbols)\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c — no longer compiled (OpenSSL removed);\n ;; SSH crypto symbols resolved lazily at runtime if SSH is used.\n ;; Generate empty .o placeholder for the linker.\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir)))\n\n (begin\n (printf \" Warning: jerboa-ssh shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-ssl shim — no longer compiled; TLS now via jerboa_tls_* (rustls)\n(printf \" [skip] jerboa-ssl shim — replaced by jerboa_tls_* (rustls)~n\")\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking static jsh-freebsd binary...~n\")\n\n;; FreeBSD static link compat: Rust coreutils references readdir_r@FBSD_1.5\n;; (versioned symbol from shared libc) but libc.a only has the unversioned symbol.\n;; Generate a small compat .o that provides the versioned symbol.\n(let ([compat-c (format \"~a/fbsd_compat.c\" build-dir)]\n [compat-o (format \"~a/fbsd_compat.o\" build-dir)])\n (call-with-output-file compat-c\n (lambda (out)\n (display \"#include <dirent.h>\\n\" out)\n (display \"__asm__(\\\".symver readdir_r_impl, readdir_r@FBSD_1.5\\\");\\n\" out)\n (display \"int readdir_r_impl(DIR *dirp, struct dirent *entry, struct dirent **result) {\\n\" out)\n (display \" return readdir_r(dirp, entry, result);\\n\" out)\n (display \"}\\n\" out)))\n (run-cmd (format \"~a -c -O2 -w -o '~a' '~a'\" gcc compat-o compat-c)))\n\n(let* ([objs (format \"~a/jsh_main_freebsd.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/coreutils-ffi.o ~a/landlock-shim.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o ~a/fbsd_compat.o\"\n build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir)]\n [native-flag (if has-native-lib? (format \" ~a\" native-lib-path) \"\")]\n [coreutils-flag (if has-rust-coreutils? (format \" ~a\" rust-coreutils-lib-path) \"\")]\n ;; Cross-compilation sysroot: when JSH_CROSS_CC is set, libraries are\n ;; under /freebsd/usr/lib/ instead of /usr/lib/\n [syslib (if (getenv \"JSH_CROSS_CC\") \"/freebsd/usr/lib\" \"/usr/lib\")]\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n [cxx-libs (if has-native-lib?\n (format \" ~a/libc++.a ~a/libcxxrt.a\" syslib syslib)\n \"\")]\n [link-libs (format \"-L~a -L~a -L/usr/local/lib -lkernel -lz -lm -lthr -liconv -lncursesw -luuid -llz4 -lutil\"\n chez-ta6fb syslib)]\n ;; Static link: libgcc_s has no .a — use libgcc.a + libgcc_eh.a instead\n ;; On cross-build with clang, these may not exist — clang uses compiler-rt\n [gcc-static (let ([gcc-a (format \"~a/libgcc.a\" syslib)])\n (if (file-exists? gcc-a)\n (format \" ~a/libgcc.a ~a/libgcc_eh.a\" syslib syslib)\n \"\"))]\n [link-cmd (format \"~a -static -o jsh-freebsd ~a~a~a~a ~a~a -Wl,--allow-multiple-definition\"\n gcc objs native-flag coreutils-flag cxx-libs link-libs gcc-static)])\n (printf \" ~a~n\" link-cmd)\n (run-cmd link-cmd))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? \"jsh-freebsd\")\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-freebsd\"))])\n (run-cmd \"strip jsh-freebsd\")\n (let ([post-size (file-length (open-file-input-port \"jsh-freebsd\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n ;; Compute SHA-256 integrity hash\n (printf \"[harden] Computing integrity hash...~n\")\n ;; FreeBSD uses sha256 -q (not sha256sum)\n (system \"sha256 -q jsh-freebsd | tr -d '\\\\n' > /tmp/_jsh_hash.txt 2>/dev/null || sha256sum jsh-freebsd | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\")\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-freebsd.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-freebsd.sha256 (32 bytes)~n\")))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n;; ssl-stage removed — jerboa-ssl/jerboa-https no longer used (rustls replaces them)\n(when has-aws? (system (format \"rm -rf '~a'\" aws-stage)))\n(system (format \"rm -rf '~a'\" awk-stage))\n(system (format \"rm -rf '~a'\" sed-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Static binary created: jsh-freebsd~n~n\")\n(system \"ls -lh jsh-freebsd\")\n(printf \"~n\")\n(system \"file jsh-freebsd\")\n(printf \"~nTest: ./jsh-freebsd -c 'echo Hello from static jsh'~n\")\n"} {"text":";; FILE: jerboa-shell/codex-findings.md\n# Codex Security Findings\n\nScope: manual source review plus targeted static scans of the `jerboa-shell` repository. I focused on the active shell, mux, embed, FFI, history, config, sandbox, redirection, and pipeline code paths. I did not include vendored or generated noise unless it is used by the runtime build.\n\nThis report intentionally references embedded sensitive file names but does not copy secret contents.\n\n## Critical Findings\n\n### 1. Private keys and operational secrets are embedded into generated source and likely the binary\n\nEvidence:\n- `src/jsh/embed-data.sls:800` embeds `.ssh/id_rsa`\n- `src/jsh/embed-data.sls:801` embeds `.ssh/id_rsa.pub`\n- `src/jsh/embed-data.sls:802` embeds `.ssh/known_hosts`\n- `src/jsh/embed-data.sls:806` embeds `keys/cert.pem`\n- `src/jsh/embed-data.sls:807` embeds `keys/key.pem`\n- `src/jsh/embed-data.sls:809` embeds `mullvad_account.txt`\n- `src/jsh/embed-data.sls:810` embeds `mullvad_wireguard_linux_cy_nic.zip`\n- `src/jsh/embed-data.sls:811` embeds `record.key`\n\nImpact: anyone who can read the generated source, build artifacts, crash dumps, backups, or final binary can recover private SSH keys, TLS key material, VPN credentials, and application keys. This is immediate credential compromise, not just a defense-in-depth issue.\n\nRecommendation: remove all private material from embed generation, rotate every exposed credential, and make embed packaging fail closed on sensitive path patterns such as `.ssh/`, `id_rsa`, `key.pem`, `record.key`, account files, and VPN archives. Public assets can remain embedded, but private runtime secrets should be provisioned separately through OS keychains, restricted config files, or explicit user-supplied paths.\n\n### 2. The mux mTLS trust model embeds the signing key used to mint trusted certificates\n\nEvidence:\n- `src/jsh/mux-transport.sls:527` reads embedded CA certificate data.\n- `src/jsh/mux-transport.sls:528` reads embedded CA key data.\n- `src/jsh/mux-transport.sls:533` to `src/jsh/mux-transport.sls:535` generates an ephemeral leaf certificate from that embedded CA key and connects using the embedded CA.\n- `src/jsh/mux-transport.sls:586` to `src/jsh/mux-transport.sls:600` documents the design: clients generate per-instance leaves signed by the embedded CA.\n\nImpact: possession of one binary containing the embedded CA private key is enough to mint certificates trusted by every peer using the same embedded CA. mTLS authenticates only \"has a copy of the binary/key,\" not a unique local identity.\n\nRecommendation: never ship the CA private key in the client. Generate per-install identities locally, store the private key with strict permissions, pin peer certificates or trust a local CA generated during first setup, and rotate the current CA. If mux is local-only, consider replacing this with Unix socket permissions plus explicit auth tokens instead of self-signing with a shared embedded key.\n\n## High Findings\n\n### 3. Non-Linux startup extracts executable code to a predictable temp path\n\nEvidence:\n- `jsh-main.c:129` to `jsh-main.c:142` builds `$TMPDIR/.jsh-program-<pid>.so` and writes it with `fopen`.\n- `jsh-main.c:162` unlinks the path after loading.\n\nImpact: on fallback platforms, a predictable filename plus `fopen(\"wb\")` follows symlinks and can race with another same-user process. Depending on directory ownership and permissions, this can overwrite unintended files or allow replacement of the extracted code before load.\n\nRecommendation: use `mkstemp` or `open` with `O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC`, mode `0600`, and operate through the file descriptor. Prefer unlinking immediately after opening where the platform supports it. Verify ownership and mode of the temporary directory before use.\n\n### 4. Mux server names are not validated before being used in socket and pidfile paths\n\nEvidence:\n- `jsh.ss:1131` to `jsh.ss:1134` accepts `--name` for remote mux invocation.\n- `jsh.ss:3320` to `jsh.ss:3321` accepts `--name` for mux server startup.\n- `src/jsh/mux-server.sls:65` to `src/jsh/mux-server.sls:69` constructs socket and pidfile paths with raw `name`.\n- `src/jsh/mux-server.sls:338` to `src/jsh/mux-server.sls:340` writes the pidfile and listens on the computed socket path.\n\nImpact: names containing `/`, `..`, control characters, or odd path syntax can escape the intended mux directory or collide with unintended files. This amplifies the socket unlink and pidfile issues below.\n\nRecommendation: centralize mux-name validation and reject anything except a small portable set such as `[A-Za-z0-9_.-]+`. Reject empty names, path separators, `.`/`..`, leading dashes if names are later passed to commands, and overly long names. Apply validation before computing any path.\n\n### 5. Unix socket listener unconditionally unlinks the requested path\n\nEvidence:\n- `ffi-shim.c:2204` defines `ffi_stream_listen_unix`.\n- `ffi-shim.c:2205` calls `unlink(path)` before binding.\n\nImpact: with unvalidated mux names, this can remove arbitrary user-writable paths reachable through path traversal. Even with validation, blindly unlinking is risky if the mux directory is compromised or if a stale path is not actually a socket.\n\nRecommendation: validate mux names first. Before unlinking a stale path, `lstat` it and require that it is a Unix socket owned by the current uid. Do not unlink regular files, symlinks, directories, or files owned by another uid. Use `O_NOFOLLOW`/`lstat` style checks consistently for companion files.\n\n### 6. Mux passwords are passed through the process environment\n\nEvidence:\n- `jsh.ss:3278` to `jsh.ss:3287` stores the password in `_JSH_MUX_PW`, forks/execs the child, then clears the parent environment.\n\nImpact: environment variables can be exposed through process inspection, crash reports, debugging tools, shell wrappers, and child process inheritance. Clearing the parent after fork does not remove the secret from the child environment.\n\nRecommendation: pass secrets over an inherited pipe, socketpair, or other fd-based channel. Mark unrelated fds close-on-exec and scrub buffers after use. If an environment fallback remains, warn clearly and make it opt-in.\n\n### 7. Embedded certificate and key material can be written back to disk with predictable names\n\nEvidence:\n- `jsh.ss:1071` to `jsh.ss:1096` defines `mux-resolve-embed-to-file`.\n- `jsh.ss:1087` constructs a hidden output path from the embedded file name.\n- `jsh.ss:1089` to `jsh.ss:1094` writes embedded bytes to that path without exclusive creation, permission setting, or cleanup.\n\nImpact: private certificate or key material can land on disk under predictable names such as a hidden file in the mux directory. The code relies on surrounding directory permissions but does not itself enforce safe file creation semantics.\n\nRecommendation: avoid writing private embedded material to disk. If a library requires paths, write to a unique `0600` file created with exclusive/no-follow semantics and delete it reliably. Prefer fd-based APIs or in-memory TLS configuration where possible.\n\n### 8. Sandbox safe-eval reads input before disabling reader features\n\nEvidence:\n- `src/jsh/sandbox.sls:252` to `src/jsh/sandbox.sls:255` parses untrusted expression text with `read` and then evaluates it in `*safe-eval-bindings*`.\n\nImpact: the restricted evaluation environment only applies after reading. If read-time evaluation or unsafe reader extensions are enabled, code may execute or allocate unexpectedly before the sandbox binding set is used.\n\nRecommendation: wrap reads of untrusted expressions with `(parameterize ([read-eval #f]) ...)` if available in this runtime, or use a safe reader/parser that only accepts the intended expression subset. Apply the same hardening to any helper that reads user-provided Scheme text before validation.\n\n## Medium Findings\n\n### 9. Interactive Scheme eval is intentionally powerful but should be treated as unsandboxed\n\nEvidence:\n- `src/jsh/script.sls:100` to `src/jsh/script.sls:118` reads and evaluates comma-prefixed Scheme input.\n\nImpact: this appears to be an intentional shell feature. It is safe only when the input is fully trusted. It should not be reachable through scripts, mux commands, or automation channels that handle untrusted input.\n\nRecommendation: document this as trusted-local-code execution. If the feature can be invoked remotely or through mux, gate it behind an explicit unsafe mode or route it through the hardened safe evaluator.\n\n### 10. Blocking FFI calls are declared without collect-safe wrappers\n\nEvidence:\n- `src/jsh/mux-transport.sls:110` to `src/jsh/mux-transport.sls:126` declares blocking TLS connect, accept, read, and write calls as plain `foreign-procedure`.\n- `src/jsh/ffi.sls:696` to `src/jsh/ffi.sls:700` declares bytevector write/send/recv wrappers as plain `foreign-procedure`.\n- `src/jsh/rl.sls:18` to `src/jsh/rl.sls:21` declares `waitpid`, `read`, and `write` as plain `foreign-procedure`.\n\nImpact: long-running or peer-controlled blocking calls can pin the Chez runtime and interfere with GC or other fibers/threads. For mux and network-facing code, slow clients can become a denial-of-service vector.\n\nRecommendation: use collect-safe foreign calls for blocking C APIs where supported, or move blocking operations to nonblocking/evented wrappers. Add timeouts around peer-controlled TLS and stream operations.\n\n### 11. PID files are written with permissive mode and symlink-following semantics\n\nEvidence:\n- `ffi-shim.c:2812` to `ffi-shim.c:2822` opens pidfile paths with `O_WRONLY | O_CREAT | O_TRUNC` and mode `0644`.\n\nImpact: pidfiles may disclose process information and can be clobbered through symlink/path manipulation if the containing directory or mux name is unsafe. This also compounds the unvalidated mux-name issue.\n\nRecommendation: use `0600`, `O_NOFOLLOW`, `O_CLOEXEC`, and preferably atomic write-plus-rename. Validate the containing directory owner and permissions before writing.\n\n### 12. Existing mux runtime directories are not validated before use\n\nEvidence:\n- `src/jsh/mux-server.sls:58` to `src/jsh/mux-server.sls:63` derives the mux socket directory from `XDG_RUNTIME_DIR` or `HOME`.\n- `src/jsh/mux-server.sls:336` creates the directory with mode `0700`, but this does not prove an already-existing directory is safe.\n\nImpact: if the directory already exists with unsafe ownership or permissions, socket and pidfile operations may happen in a location controlled or observable by another user/process.\n\nRecommendation: after creating or discovering the directory, `stat` it and require directory type, current uid ownership, and no group/world permissions. Fail closed if the checks do not pass.\n\n### 13. Command history can persist secrets typed as arguments\n\nEvidence:\n- `jsh.ss:2533` to `jsh.ss:2534` parses `ssh --password`.\n- `jsh.ss:2744` to `jsh.ss:2745` parses `scp --password`.\n- `src/jsh/history.sls:548` to `src/jsh/history.sls:568` persists history entries to the configured history file.\n\nImpact: commands containing passwords, tokens, API keys, or one-off secrets may be stored in plaintext history. The history file is intended to be chmodded to `0600`, but persistence still expands the lifetime of secrets and exposes them to backups and local compromise.\n\nRecommendation: add history redaction and ignore rules for secret-bearing flags and environment assignments. Prefer prompting for passwords instead of accepting them on the command line. Do not store entries containing `--password`, `token=`, `AWS_SECRET_ACCESS_KEY=`, private key material, or similar patterns.\n\n### 14. Sandbox fallback appears to continue without sandboxing when platform support is missing\n\nEvidence:\n- `ffi-shim.c:1371` to `ffi-shim.c:1379` attempts to apply Landlock and logs that unsupported kernels continue without sandbox enforcement.\n\nImpact: a caller requesting sandboxing may receive an unsandboxed process on unsupported platforms or kernels. That is a policy bypass if users or higher-level code rely on sandbox success for safety.\n\nRecommendation: fail closed by default when sandbox setup is requested but unavailable. Provide an explicit option such as `--allow-unsandboxed-fallback` for compatibility workflows.\n\n## Low Findings and Hardening Items\n\n### 15. Debug logging can write to an environment-selected path\n\nEvidence:\n- `src/jsh/mux-server.sls:37` to `src/jsh/mux-server.sls:46` opens the path from `MUX_SERVER_DEBUG` for append logging.\n\nImpact: this is mostly user-controlled behavior, but it can leak mux metadata to unintended files and follows normal path semantics, including symlinks.\n\nRecommendation: treat this as a development-only feature. If retained, create logs with safe file-open flags where possible and avoid logging secrets or authentication material.\n\n### 16. Redirection and pipeline code should use exception-safe cleanup uniformly\n\nEvidence:\n- `redirect.ss:249` to `redirect.ss:253` opens redirection ports and mutates current ports.\n- `redirect.ss:263` to `redirect.ss:266` restores current ports after executing the command.\n- `redirect.ss:735` to `redirect.ss:740` and `redirect.ss:750` to `redirect.ss:752` contain similar open/restore patterns.\n- `pipeline.ss:123` to `pipeline.ss:143` and `pipeline.ss:288` to `pipeline.ss:318` open `/dev/fd/N` ports and close them after command execution.\n\nImpact: many paths close ports explicitly, but not all mutations are visibly protected by `dynamic-wind`, `unwind-protect`, or `with-resource` at the same abstraction level. Exceptions during setup or command execution can leave ports/fds open or current ports temporarily wrong.\n\nRecommendation: wrap every fd/port acquisition and current-port mutation in a single exception-safe helper. Jerboa already has `unwind-protect` and `with-resource`; using those consistently would make leaks and restoration bugs easier to audit.\n\n### 17. Config file loading silently ignores all errors\n\nEvidence:\n- `src/jsh/config.sls:33` to `src/jsh/config.sls:39` loads `~/.jsh/config` under a broad guard that suppresses every condition.\n\nImpact: malformed config, permission problems, or unsafe config behavior can fail silently. Security-relevant config may appear to be active when it was ignored.\n\nRecommendation: distinguish \"file absent\" from parse/runtime errors. Warn on malformed config and consider rejecting group/world-writable config files before loading.\n\n## Suggested Remediation Order\n\n1. Remove and rotate embedded private material immediately. Redesign mux identity so no shared CA private key ships in the binary.\n2. Validate mux names and harden all socket, pidfile, and temp-file creation before any more mux features are added.\n3. Replace environment-based password passing with fd-based transfer.\n4. Harden all untrusted Scheme reads with read-time-eval disabled or a restricted parser.\n5. Add collect-safe or nonblocking wrappers for blocking FFI calls used by mux and stream operations.\n6. Add history redaction for secret-bearing commands and make password prompts the preferred path.\n7. Normalize fd/port cleanup with `with-resource`, `unwind-protect`, or a local helper.\n\n## Notes\n\n- `src/jsh/embed-data.sls` is generated-style Scheme, but it is part of the runtime source tree and currently contains sensitive bytevectors. The correct fix is upstream in the embed generation inputs and filters, not manual editing of only the generated file.\n- The comma Scheme evaluator appears intentional. I counted it as a trust-boundary warning rather than a standalone vulnerability.\n- Several issues compound: unvalidated mux names make unlink, pidfile, and embedded-file extraction behaviors materially more dangerous than they would be in a strictly private, validated runtime directory.\n"} {"text":";; FILE: jerboa-shell/prompt.ss\n;;; prompt.ss — Prompt expansion (PS1/PS2/PS4) for gsh\n\n(export #t)\n(import :std/sugar\n :std/format\n :jsh/ffi\n :jsh/util\n (only-in :jsh/expander find-matching-paren))\n\n;;; --- Git branch helper (reads .git/HEAD directly, no external commands) ---\n\n(def (git-branch-name)\n (let loop ([dir (current-directory)])\n (let ([head-path (string-append dir \"/.git/HEAD\")])\n (if (file-exists? head-path)\n (with-catch\n (lambda (e) #f)\n (lambda ()\n (let* ([content (call-with-input-file head-path read-line)]\n [prefix \"ref: refs/heads/\"])\n (if (and (string? content)\n (>= (string-length content) (string-length prefix))\n (string=? prefix (substring content 0 (string-length prefix))))\n (substring content (string-length prefix) (string-length content))\n ;; Detached HEAD — show short hash\n (if (and (string? content) (>= (string-length content) 7))\n (substring content 0 7)\n #f)))))\n ;; Walk up to parent directory\n (let ([parent (path-directory dir)])\n (if (or (not parent) (string=? parent dir) (string=? parent \"/\"))\n #f\n (loop parent)))))))\n\n;;; --- Public interface ---\n\n;; Expand prompt escape sequences in a PS string\n;; env-get: (lambda (name) -> string or #f)\n;; job-count: number of active jobs\n;; cmd-number: command number\n;; history-number: history number\n;; cmd-exec-fn: optional (lambda (cmd-string) -> output-string) for $(...) expansion\n(def (expand-prompt ps-string env-get\n (job-count 0)\n (cmd-number 0)\n (history-number 0)\n (cmd-exec-fn #f)\n (ssh-key-count 0))\n (let ([len (string-length ps-string)]\n [out (open-output-string)])\n (let loop ([i 0])\n (cond\n ((>= i len)\n (get-output-string out))\n ;; Command substitution $(...)\n ((and cmd-exec-fn\n (char=? (string-ref ps-string i) #\\$)\n (< (+ i 1) len)\n (char=? (string-ref ps-string (+ i 1)) #\\())\n (let ([close (find-matching-paren ps-string (+ i 2))])\n (if close\n (let* ([cmd-str (substring ps-string (+ i 2) close)]\n [output (with-catch\n (lambda (e) \"\") ;; Silently ignore errors in prompt commands\n (lambda () (cmd-exec-fn cmd-str)))])\n (display output out)\n (loop (+ close 1)))\n ;; No matching ) - output literally\n (begin\n (display \"$(\" out)\n (loop (+ i 2))))))\n ;; Backslash escape sequence\n ((and (char=? (string-ref ps-string i) #\\\\)\n (< (+ i 1) len))\n (let ([ch (string-ref ps-string (+ i 1))])\n (case ch\n ;; Username\n ((#\\u)\n (display (or (env-get \"USER\") (user-name)) out)\n (loop (+ i 2)))\n ;; Hostname (short)\n ((#\\h)\n (let* ([host (or (env-get \"HOSTNAME\")\n (with-catch (lambda (e) \"localhost\")\n (lambda () (hostname-short))))]\n [dot (string-index host #\\.)])\n (display (if dot (substring host 0 dot) host) out)\n (loop (+ i 2))))\n ;; Hostname (full)\n ((#\\H)\n (display (or (env-get \"HOSTNAME\")\n (with-catch (lambda (e) \"localhost\")\n (lambda () (hostname-short))))\n out)\n (loop (+ i 2)))\n ;; Working directory with ~ for home\n ((#\\w)\n (let* ([pwd (or (env-get \"PWD\") (current-directory))]\n [home (or (env-get \"HOME\") \"\")]\n [display-pwd (if (and (> (string-length home) 0)\n (string-prefix? home pwd))\n (string-append \"~\" (substring pwd (string-length home)\n (string-length pwd)))\n pwd)])\n (display display-pwd out)\n (loop (+ i 2))))\n ;; Basename of working directory\n ((#\\W)\n (let* ([pwd (or (env-get \"PWD\") (current-directory))]\n [home (or (env-get \"HOME\") \"\")])\n (if (string=? pwd home)\n (display \"~\" out)\n (display (path-basename pwd) out))\n (loop (+ i 2))))\n ;; Date\n ((#\\d)\n ;; Simplified: just show date\n (display (date-string) out)\n (loop (+ i 2)))\n ;; Time formats\n ((#\\t) ;; 24h HH:MM:SS\n (display (time-string-24h) out)\n (loop (+ i 2)))\n ((#\\T) ;; 12h HH:MM:SS\n (display (time-string-12h) out)\n (loop (+ i 2)))\n ((#\\@) ;; 12h am/pm\n (display (time-string-ampm) out)\n (loop (+ i 2)))\n ((#\\A) ;; 24h HH:MM\n (display (time-string-hhmm) out)\n (loop (+ i 2)))\n ;; Newline / carriage return\n ((#\\n) (display \"\\n\" out) (loop (+ i 2)))\n ((#\\r) (display \"\\r\" out) (loop (+ i 2)))\n ;; Shell name\n ((#\\s)\n (display \"jsh\" out)\n (loop (+ i 2)))\n ;; Shell version\n ((#\\v)\n (display (or (env-get \"JSH_VERSION_SHORT\") \"0.2\") out)\n (loop (+ i 2)))\n ((#\\V)\n (display (or (env-get \"JSH_VERSION\") \"0.2.0\") out)\n (loop (+ i 2)))\n ;; Number of jobs\n ((#\\j)\n (display (number->string job-count) out)\n (loop (+ i 2)))\n ;; Terminal basename\n ((#\\l)\n (display \"tty\" out)\n (loop (+ i 2)))\n ;; Command number\n ((#\\#)\n (display (number->string cmd-number) out)\n (loop (+ i 2)))\n ;; History number\n ((#\\!)\n (display (number->string history-number) out)\n (loop (+ i 2)))\n ;; $ or # (root check)\n ((#\\$)\n (display (if (= (ffi-geteuid) 0) \"#\" \"$\") out)\n (loop (+ i 2)))\n ;; Bell\n ((#\\a)\n (display \"\\007\" out)\n (loop (+ i 2)))\n ;; Literal backslash\n ((#\\\\)\n (display \"\\\\\" out)\n (loop (+ i 2)))\n ;; strftime format \\D{format}\n ((#\\D)\n (if (and (< (+ i 2) len) (char=? (string-ref ps-string (+ i 2)) #\\{))\n (let ([close (string-index-from ps-string #\\} (+ i 3))])\n (if close\n (begin\n ;; Simplified: just show ISO date\n (display (date-string) out)\n (loop (+ close 1)))\n (begin\n (display \"\\\\D\" out)\n (loop (+ i 2)))))\n (begin\n (display \"\\\\D\" out)\n (loop (+ i 2)))))\n ((#\\]) (loop (+ i 2)))\n ;; \\g = bare branch name, \\G = \" (branch)\" with decoration\n ((#\\g)\n (let ([branch (git-branch-name)])\n (when branch (display branch out)))\n (loop (+ i 2)))\n ((#\\G)\n (let ([branch (git-branch-name)])\n (when branch\n (display \" (\" out)\n (display branch out)\n (display \")\" out)))\n (loop (+ i 2)))\n ;; SSH key count (\\K) — N when N>0 keys in agent, empty string when 0\n ((#\\K)\n (when (> ssh-key-count 0)\n (display (number->string ssh-key-count) out))\n (loop (+ i 2)))\n ;; Non-printing delimiters (bash \\[ \\]) — skip\n ((#\\[) (loop (+ i 2)))\n ((#\\]) (loop (+ i 2)))\n ;; Unknown escape: output literally\n (else\n (display \"\\\\\" out)\n (display (string ch) out)\n (loop (+ i 2))))))\n ;; Regular character\n (else\n (display (string (string-ref ps-string i)) out)\n (loop (+ i 1)))))))\n\n;; Calculate visible width of a prompt (excluding \\(...\\) non-printing sequences)\n(def (prompt-width prompt-string)\n (let ([len (string-length prompt-string)])\n (let loop ([i 0] [width 0] [in-escape? #f])\n (cond\n ((>= i len) width)\n ;; ANSI escape sequence: \\e(...m\n ((and (not in-escape?)\n (char=? (string-ref prompt-string i) #\\escape))\n (loop (+ i 1) width #t))\n (in-escape?\n (if (char-alphabetic? (string-ref prompt-string i))\n (loop (+ i 1) width #f) ;; end of escape\n (loop (+ i 1) width #t)))\n (else\n (loop (+ i 1) (+ width 1) #f))))))\n\n;;; --- Time/date helpers ---\n\n(def (current-time-values)\n ;; Returns (seconds minutes hours day month year weekday)\n ;; Using Gambit's time->seconds and manual calculation\n (let* ([t (time->seconds (current-time))]\n [secs (inexact->exact (floor t))])\n ;; Simple approach: use process to get date components\n ;; For now, return approximate values\n (values (modulo secs 60)\n (modulo (quotient secs 60) 60)\n (modulo (quotient secs 3600) 24)\n 0 0 0 0)))\n\n(def (time-string-24h)\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (format \"~2,'0d:~2,'0d:~2,'0d\" h m s)))\n\n(def (time-string-12h)\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (let ([h12 (cond ((= h 0) 12) ((> h 12) (- h 12)) (else h))])\n (format \"~2,'0d:~2,'0d:~2,'0d\" h12 m s))))\n\n(def (time-string-ampm)\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (let ([h12 (cond ((= h 0) 12) ((> h 12) (- h 12)) (else h))]\n [ampm (if (>= h 12) \"PM\" \"AM\")])\n (format \"~2,'0d:~2,'0d ~a\" h12 m ampm))))\n\n(def (time-string-hhmm)\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (format \"~2,'0d:~2,'0d\" h m)))\n\n(def (date-string)\n ;; Simplified date string\n (let-values ([(s m h d mo y wd) (current-time-values)])\n (format \"~a\" (seconds->date-string (time->seconds (current-time))))))\n\n(def (seconds->date-string secs)\n ;; Very simplified - will be improved later with proper date library\n (with-catch\n (lambda (e) \"???\")\n (lambda ()\n (let ([port (open-input-process (list path: \"/bin/date\" arguments: (list \"+%a %b %d\")))])\n (let ([result (read-line port)])\n (close-port port)\n (if (string? result) result \"???\"))))))\n\n(def (hostname-short)\n (with-catch\n (lambda (e) \"localhost\")\n (lambda ()\n (let ([port (open-input-process (list path: \"/bin/hostname\"))])\n (let ([result (read-line port)])\n (close-port port)\n (if (string? result) result \"localhost\"))))))\n\n(def (path-basename path)\n (let loop ([i (- (string-length path) 1)])\n (cond\n ((< i 0) path)\n ((char=? (string-ref path i) #\\/)\n (substring path (+ i 1) (string-length path)))\n (else (loop (- i 1))))))\n\n(def (string-prefix? prefix str)\n (and (>= (string-length str) (string-length prefix))\n (string=? (substring str 0 (string-length prefix)) prefix)))\n\n(def (string-index-from str ch start)\n (let loop ([i start])\n (cond\n ((>= i (string-length str)) #f)\n ((char=? (string-ref str i) ch) i)\n (else (loop (+ i 1))))))\n"} {"text":";; FILE: jerboa-shell/asciinema.md\n# Jerboa Shell Session Recording — Implementation Plan\n\n## Overview\n\nIntegrate asciicast v2-compatible terminal session recording directly into jerboa-shell,\neliminating the need for external tools like asciinema or goasciinema. The shell itself\nbecomes the recorder — it already owns the REPL, the PTY, and the executor pipeline.\n\n## Why Build It In?\n\nExternal recorders wrap the shell in a PTY and capture raw bytes flowing through it.\nThey have no knowledge of what the shell is doing — they cannot distinguish a prompt\nfrom command output, or tag events with the command that produced them. Jerboa already\nhas structured access to:\n\n- **Input lines** (after line-edit, before parse)\n- **Expanded commands** (after parameter/glob expansion)\n- **Redirections and pipelines** (fd graph)\n- **Exit status** (per-command and PIPESTATUS)\n- **Timestamps and CWD** (history entries)\n- **Terminal dimensions** (SIGWINCH handling)\n\nA built-in recorder can produce **enriched asciicast** files with semantic annotations\nthat no external tool can provide.\n\n---\n\n## Format: Asciicast v2 + Extensions\n\nBase format is standard asciicast v2 for compatibility with asciinema players:\n\n```\n{\"version\":2,\"width\":120,\"height\":40,\"timestamp\":1710806400,\"env\":{\"SHELL\":\"/usr/bin/jsh\",\"TERM\":\"xterm-256color\"}}\n[0.0, \"o\", \"$ \"]\n[0.5, \"i\", \"ls -la\\r\"]\n[0.6, \"o\", \"total 42\\r\\ndrwxr-xr-x ...\\r\\n\"]\n[1.2, \"o\", \"$ \"]\n```\n\n### Extended Event Types (jerboa-specific)\n\nStandard players ignore unknown event types, so we add:\n\n| Type | Meaning | Data |\n|------|---------|------|\n| `\"o\"` | stdout output | raw bytes (standard) |\n| `\"i\"` | stdin input | raw bytes (standard) |\n| `\"r\"` | terminal resize | `\"COLSxROWS\"` (standard) |\n| `\"m\"` | marker | label string (standard) |\n| `\"c\"` | command | `{\"cmd\":\"ls -la\",\"cwd\":\"/home/user\",\"expanded\":\"ls -la\"}` |\n| `\"x\"` | exit status | `{\"status\":0,\"pipestatus\":[0]}` |\n| `\"e\"` | env change | `{\"var\":\"PATH\",\"op\":\"set\",\"val\":\"/usr/bin:...\"}` |\n| `\"d\"` | duration | `{\"cmd\":\"ls -la\",\"wall_ms\":42,\"user_ms\":12,\"sys_ms\":8}` |\n\nThis means recordings are playable in any asciicast v2 player (extra events ignored)\nwhile jerboa-aware tools can reconstruct full session semantics.\n\n---\n\n## Architecture\n\n### Phase 1: Core Recording (Tap the I/O Layer)\n\n**Goal**: Record sessions to `~/console-logs/` in asciicast v2 format.\n\n**Implementation**: Insert recording hooks at three points in the existing I/O path:\n\n```\n┌──────────────────────────────────────────────────────────┐\n│ LINEEDIT (raw terminal bytes) │\n│ ┌──────────┐ │\n│ │ keystroke │──→ [TAP: \"i\" event] ──→ line buffer │\n│ └──────────┘ │\n└──────────────────────────────────────────────────────────┘\n │ completed line\n ▼\n┌──────────────────────────────────────────────────────────┐\n│ EXECUTOR │\n│ ┌──────────┐ │\n│ │ dispatch │──→ [TAP: \"c\" event with cmd/cwd/expanded] │\n│ └──────────┘ │\n│ ┌──────────┐ │\n│ │ complete │──→ [TAP: \"x\" event with exit status] │\n│ └──────────┘ │\n└──────────────────────────────────────────────────────────┘\n │ output bytes\n ▼\n┌──────────────────────────────────────────────────────────┐\n│ OUTPUT (write to real fd 1/2) │\n│ ┌──────────┐ │\n│ │ write() │──→ [TAP: \"o\" event] ──→ terminal │\n│ └──────────┘ │\n└──────────────────────────────────────────────────────────┘\n```\n\n#### New Files\n\n| File | Purpose |\n|------|---------|\n| `jerboa-shell/recorder.ss` | Recording state, event buffering, file writer |\n| `ffi-shim.c` additions | `ffi-clock-monotonic-ns` for high-resolution timestamps |\n\n#### recorder.ss API\n\n```scheme\n;; State\n(define *recording?* (make-parameter #f))\n(define *recorder* (make-parameter #f))\n\n;; Control\n(recorder-start! filename) ; open file, write header, set *recording?*\n(recorder-stop!) ; flush, close file, clear state\n(recorder-toggle!) ; pause/resume\n\n;; Event emission (called from existing modules)\n(recorder-emit! type data) ; generic: writes [elapsed, type, data] line\n(recorder-output! bytes) ; shorthand for \"o\" event\n(recorder-input! bytes) ; shorthand for \"i\" event\n(recorder-command! cmd-info) ; shorthand for \"c\" event\n(recorder-exit-status! status) ; shorthand for \"x\" event\n(recorder-resize! cols rows) ; shorthand for \"r\" event\n```\n\n#### Integration Points (minimal patches)\n\n1. **lineedit.ss**: After each raw byte read, `(when (*recording?*) (recorder-input! byte))`\n2. **executor.ss**: Before dispatch, emit \"c\" event. After completion, emit \"x\" event.\n3. **pipeline.ss / redirect.ss**: Wrap `ffi-write` (or the write path to fd 1/2) to tee bytes to recorder.\n4. **signals.ss**: On SIGWINCH, emit \"r\" event.\n\n#### Output Tapping Strategy\n\nThe tricky part is capturing output from external commands. Two approaches:\n\n**Option A — PTY Wrapper (like asciinema)**:\nSpawn a PTY for the session and capture master-side output. This is what external\nrecorders do. Downside: adds PTY overhead and complexity.\n\n**Option B — fd-level tee via splice/tee(2) or write interception**:\nAdd `ffi-tee-fd` that duplicates output bytes to a pipe before they reach the terminal.\nOn Linux, `tee(2)` syscall can do this in-kernel with zero-copy. Alternatively, replace\ndirect fd writes with a wrapper that also feeds the recorder.\n\n**Recommendation**: Option B. Jerboa already manages all fd writes through its\nredirect/pipeline layer. We add a thin wrapper:\n\n```c\n// ffi-shim.c addition\nssize_t ffi_write_tee(int fd, const char *buf, size_t len, int record_fd) {\n if (record_fd >= 0) {\n // Write to recording pipe (non-blocking, drop on EAGAIN)\n write(record_fd, buf, len);\n }\n return write(fd, buf, len);\n}\n```\n\nThe record_fd is a pipe whose read end is consumed by the recorder thread/fiber.\n\n#### Storage\n\n```\n~/console-logs/\n├── 2026-03-19_14-30-00.cast # asciicast v2 format\n├── 2026-03-19_14-30-00.meta # optional: enriched metadata (JSON)\n└── index.db # optional: SQLite index (Phase 3)\n```\n\nFilename format: `YYYY-MM-DD_HH-MM-SS.cast` (matches goasciinema convention).\n\n#### Shell Builtins\n\n```bash\nrecord start [filename] # begin recording (default: auto-named in ~/console-logs/)\nrecord stop # end recording\nrecord pause # toggle pause\nrecord mark \"label\" # insert marker event\nrecord status # show recording state, elapsed time, file size\n```\n\nAlternatively, use `set -o recording` / `set +o recording` to fit shell conventions.\n\n---\n\n### Phase 2: Playback & Search\n\n**Goal**: Play back recordings and search across sessions.\n\n#### Playback (`play` builtin)\n\n```bash\nplay ~/console-logs/2026-03-19_14-30-00.cast # replay with timing\nplay --speed 2.0 last # 2x speed, most recent\nplay --cat last # dump output without timing\nplay --commands last # show only commands + exit status\n```\n\nImplementation: Read asciicast v2 events, sleep between timestamps, write \"o\" events\nto stdout. Use `jerboa-pcre2` for pattern filtering. The playback engine is simple —\n~100 lines of Scheme.\n\n#### Search (leveraging jsqlite)\n\n```bash\nrecord search \"podman build\" # search commands, cwd, and output\nrecord search --failed \"make\" # commands that exited non-zero\nrecord search --cwd /home/user/project \"test\" # scoped to directory\nrecord list # list all recordings with metadata\nrecord stats # total sessions, hours, commands\n```\n\nUses pure Jerboa `jsqlite` to maintain an index database:\n\n```sql\nCREATE TABLE sessions (\n id INTEGER PRIMARY KEY,\n filename TEXT UNIQUE,\n started_at INTEGER, -- unix timestamp\n duration_ms INTEGER,\n cols INTEGER,\n rows INTEGER,\n command_count INTEGER,\n cwd_at_start TEXT\n);\n\nCREATE TABLE commands (\n id INTEGER PRIMARY KEY,\n session_id INTEGER REFERENCES sessions(id),\n timestamp_ms INTEGER, -- offset from session start\n command TEXT,\n expanded TEXT,\n cwd TEXT,\n exit_status INTEGER,\n duration_ms INTEGER\n);\n\nCREATE VIRTUAL TABLE command_fts USING fts5(command, expanded, cwd);\n```\n\n---\n\n### Phase 3: Real-Time Consolidation via Chez Threads\n\n**Goal**: Decouple recording from the main shell loop for zero-overhead capture.\n\nJerboa runs on Chez Scheme which has native OS threads. The recording system uses\na **producer-consumer architecture**:\n\n```\nMain Thread (shell REPL) Recording Thread\n┌─────────────────────┐ ┌──────────────────────┐\n│ lineedit / executor │ │ event consumer │\n│ │ ring │ │\n│ recorder-emit! ──────┼──buffer──┼──→ write to .cast │\n│ │ │ update SQLite index │\n│ │ │ flush periodically │\n└─────────────────────┘ └──────────────────────┘\n```\n\n#### Ring Buffer (lock-free)\n\nUse a fixed-size bytevector ring buffer with atomic CAS for the write cursor.\nEvents are length-prefixed: `[u16:len][bytes:payload]`. The recorder thread\nreads from the tail, writes to disk, and advances the read cursor.\n\n```scheme\n(define-record-type ring-buffer\n (fields\n (mutable data) ; bytevector\n (mutable write-pos) ; atomic\n (mutable read-pos) ; atomic\n (mutable capacity)))\n```\n\nIf the ring fills up (recorder can't keep up), events are dropped — recording\nfidelity degrades gracefully rather than blocking the shell.\n\n#### Why Not Actors?\n\nChez Scheme (jerboa's runtime) doesn't have a built-in actor system like the old runtime did.\nWhile we could build one, it's unnecessary overhead for this use case. A single\ndedicated thread with a ring buffer is simpler, faster, and sufficient. The recording\nthread is a pure consumer — no bidirectional messaging needed.\n\nIf jerboa later adopts an actor/fiber system, the recorder thread can be trivially\nwrapped as an actor that receives event messages.\n\n---\n\n### Phase 4: Network Streaming & Collaboration (IMPLEMENTED)\n\n**Goal**: Stream sessions to remote observers or aggregation servers.\n\n**Status**: Implemented using jerboa's native actor transport with cookie-authenticated\nTCP and FASL serialization. No TLS — uses plain TCP via `(std net tcp-raw)` (POSIX\nsockets, no external library dependency). AES encryption can be layered via\n`(std crypto cipher)` for sensitive deployments.\n\n#### Architecture\n\n```\n┌─────────────┐ ┌──────────────┐ ┌─────────────────┐\n│ jerboa shell │────→│ recorder.ss │────→│ local .cast file │\n│ (session 1) │ │ (actor node) │ └─────────────────┘\n└─────────────┘ │ │────→│ actor send │\n └──────────────┘ └────────┬──────────┘\n │ (list 'events file lines)\n┌─────────────┐ ┌──────────────┐ │\n│ jerboa shell │────→│ recorder.ss │────→──────────┤\n│ (session 2) │ │ (actor node) │ │\n└─────────────┘ └──────────────┘ │\n ▼\n ┌─────────────────┐\n │ aggregator actor │\n │ (id 1, remote │\n │ node at host:port)│\n └─────────────────┘\n │\n ┌─────────────────┐\n │ viewer actors │\n │ (play --live) │\n │ spawn-actor with │\n │ display behavior │\n └─────────────────┘\n```\n\n#### Transport: Actor Model\n\nUses jerboa's `(std actor core)` + `(std actor transport)`:\n\n- **Cookie authentication**: FNV-1a hash handshake on connect\n- **FASL serialization**: Native Chez Scheme binary format for messages\n- **TCP via `(std net tcp-raw)`**: Pure POSIX sockets, no jerboa-ssl dependency\n- **Fire-and-forget**: Recorder sends events without blocking on response\n- **Ephemeral ports**: Shell nodes bind to port 0 (OS-assigned)\n\nURL format: `node://host:port/cookie` (cookie defaults to `jsh-default-cookie`)\n\n#### Actor Messages\n\n| Message | Direction | Purpose |\n|---------|-----------|---------|\n| `(session-start file cols rows epoch)` | recorder → aggregator | New recording session |\n| `(events file lines)` | recorder → aggregator | Batch of event lines |\n| `(session-end file)` | recorder → aggregator | Recording stopped |\n| `(subscribe viewer-ref)` | viewer → aggregator | Subscribe to live events |\n| `(event line)` | aggregator → viewer | Forward event to viewer |\n\n#### Live Tailing\n\n```bash\n# On machine A (recording)\nrecord start --stream node://aggregator:9876/my-secret-cookie\n\n# On machine B (watching)\nplay --live node://aggregator:9876/my-secret-cookie\n# or\nplay --follow ~/console-logs/2026-03-19_14-30-00.cast # tail -f equivalent\n```\n\n#### Implementation Files\n\n| File | Changes |\n|------|---------|\n| `recorder.ss` | Actor streaming: `parse-stream-url`, `stream-connect!`, `stream-disconnect!`, `stream-send-events!` |\n| `player.ss` | Actor viewer: `play-live` spawns local viewer actor, subscribes to aggregator |\n| `jerboa/lib/std/net/tcp-raw.sls` | New: fd-based plain TCP, jerboa-ssl-compatible API, no external deps |\n| `jerboa/lib/std/actor/transport.sls` | Changed: imports `(std net tcp-raw)` instead of `(std net ssl)` |\n| `build-binary-jsh.ss` | Added actor/mpsc/tcp-raw modules to boot file |\n| `build-jsh-musl.ss` | Added actor modules to boot file + POSIX socket symbol registration |\n| `Makefile` | Added `JERBOA_SSL` variable to LIBDIRS_JSH |\n\n---\n\n### Phase 5: Enriched Session Intelligence\n\n**Goal**: Leverage the shell's semantic knowledge for advanced features.\n\n#### Command-Aware Recording\n\nSince jerboa owns the parser, we can annotate recordings with:\n\n- **Command boundaries**: Know exactly where each command's output starts/ends\n- **Variable mutations**: Track `export`, `unset`, assignment side effects\n- **Function definitions**: Record when shell functions are defined/redefined\n- **Pipeline topology**: Which commands piped to which, with individual timings\n- **Error context**: Stderr output tagged to the specific command that produced it\n\n#### Session Analytics\n\n```bash\nrecord report last # summary: commands run, error rate, time distribution\nrecord report --slow last # commands that took > 1s\nrecord report --errors last # all non-zero exits with context\n```\n\n#### Reproducibility\n\nBecause we capture expanded commands with their CWD and environment:\n\n```bash\nrecord replay --execute last # re-execute all commands from a recording\nrecord replay --dry-run last # show what would be executed\nrecord export --script last # generate a shell script from the recording\n```\n\n---\n\n## Implementation Order\n\n| Phase | Scope | Dependencies | Status |\n|-------|-------|--------------|--------|\n| **1a** | `recorder.ss` + file writer + `record start/stop` builtins | None | DONE |\n| **1b** | Output tapping (fd-level tee in ffi-shim.c) | 1a | DONE |\n| **1c** | Input tapping (lineedit.ss hooks) | 1a | DONE |\n| **1d** | Command/status events (executor.ss hooks) | 1a | DONE |\n| **1e** | SIGWINCH resize events | 1a | DONE |\n| **2a** | `play` builtin (timing-aware playback) | 1a-1e | DONE |\n| **2b** | SQLite-compatible index + `record search/list` | `jsqlite` | DONE |\n| **3** | Buffered recording (in-memory event buffer, batch flush) | 1a-1e | DONE |\n| **4** | Actor-based network streaming (`std actor transport` + `std net tcp-raw`) | 3 | DONE |\n| **5** | Enriched analytics (env change events, duration, session report, export) | 2b | DONE |\n\n**Phase 1 is the critical path** — everything else builds on it. Phase 1a-1e can be\ndone in a single session. The recording format is designed so that Phase 1 recordings\nremain compatible as features are added in later phases.\n\n---\n\n## Libraries Used\n\n| Library | Phase | Purpose |\n|---------|-------|---------|\n| `jsqlite` | 2+ | Session index, command/output search, analytics |\n| `(std actor core)` | 4 | Actor spawn, send, lifecycle |\n| `(std actor transport)` | 4 | Cookie-authenticated TCP transport, FASL framing |\n| `(std net tcp-raw)` | 4 | Plain POSIX TCP sockets (no jerboa-ssl dependency) |\n| `(std actor mpsc)` | 4 | Lock-free mailbox for actors |\n\n---\n\n## Configuration\n\n```bash\n# ~/.jshrc or environment variables\nJSH_RECORD=1 # auto-record all sessions\nJSH_RECORD_DIR=~/console-logs # recording directory\nJSH_RECORD_STDIN=1 # include input events\nJSH_RECORD_IDLE_LIMIT=5 # cap idle time to 5s\nJSH_RECORD_STREAM=tcp://host:9876 # stream to aggregator\nJSH_RECORD_COMMANDS_ONLY=0 # if 1, only emit c/x events (compact)\n```\n\nOr via shell options:\n\n```bash\nset -o recording # enable\nset +o recording # disable\nshopt -s record_stdin # include input\nshopt -s record_commands # include enriched command events\n```\n\n---\n\n## Comparison with External Recorders\n\n| Feature | asciinema | goasciinema | jerboa (this plan) |\n|---------|-----------|-------------|-------------------|\n| Format | asciicast v2 | asciicast v2 | asciicast v2 + extensions |\n| Recording | PTY wrapper | PTY wrapper | Native (no PTY overhead) |\n| Input capture | Optional | Optional | Optional |\n| Command boundaries | No | No | **Yes** |\n| Exit status per cmd | No | No | **Yes** |\n| CWD tracking | No | No | **Yes** |\n| Variable tracking | No | No | **Yes** (Phase 5) |\n| Pipeline topology | No | No | **Yes** (Phase 5) |\n| Search | Basic (goasciinema) | SQLite FTS | SQLite FTS + semantic |\n| Streaming | Upload after | Upload after | **Real-time** (Phase 4) |\n| Always-on recording | Possible but awkward | Possible | **Native** (`set -o recording`) |\n| Playback | Separate tool | Separate tool | **Built-in** |\n| Binary overhead | Separate install | Separate install | **Zero** (compiled in) |\n| Format compatible | Yes | Yes | Yes (extended events ignored) |\n"} @@ -254,12 +254,12 @@ {"text":";; FILE: jerboa-shell/master-router.md\n# Centralized Mux Router\n\n## Concept\n\n```\nPhone (client) VPS (router) MacBook (mux)\n───────────── ───────────── ──────────────\n,mux attach uap.tf:443 ──TLS──> Router Master <──TLS── mux-server (registered)\n │\n ← session menu ←────────────────────┘\n → select \"macbook\" ──────────────────> relay messages ──────> mux-server\n (transparent pipe)\n```\n\n## Three New Components\n\n### 1. `mux-router.ss` — runs on the VPS\n\n- Listens on 443 with TLS (reuses existing `mux-transport` + HTTP camouflage)\n- Maintains a registry of connected mux servers (name, host, capabilities)\n- When a **mux server** connects, it sends a `MSG-REGISTER` identifying itself — the router holds this connection open as a **control channel**\n- When a **client** connects with `MSG-ATTACH`, the router sends back a `MSG-SESSION-LIST` (new message type) — a menu of registered muxes\n- After the client picks one, the router either:\n - **Option A (simple relay):** Bridges the client transport to the mux server's transport — every byte forwarded transparently. The router becomes a dumb pipe after handoff.\n - **Option B (multiplexed control channel):** The router tells the mux server \"open a new data channel back to me for client X.\" The mux server dials a second TLS connection tagged with a session token. The router pairs them.\n\nOption B is better — it means the mux server only needs one persistent outbound connection (the control channel), and data channels are opened on demand. This is how ngrok/Cloudflare Tunnel work.\n\n### 2. `mux-register.ss` — logic in the mux server\n\n- On `,mux server --router uap.tf:443 --name macbook`, the mux server:\n 1. Dials TLS to `uap.tf:443`\n 2. Sends `MSG-REGISTER` with its name, available sessions\n 3. Keeps the connection alive (ping/pong already exists)\n 4. When it receives `MSG-OPEN-CHANNEL {token}` from the router, it dials a **new** TLS connection to the router, sends `MSG-CHANNEL-READY {token}`, and that connection becomes a normal mux client transport\n\n### 3. New protocol messages\n\n```\nMSG-REGISTER (0x30) mux→router {name, host-label, session-count}\nMSG-REGISTERED (0x31) router→mux {ack}\nMSG-SESSION-LIST (0x32) router→client {list of (name, host, sessions)}\nMSG-SELECT-MUX (0x33) client→router {mux-name}\nMSG-OPEN-CHANNEL (0x34) router→mux {token}\nMSG-CHANNEL-READY(0x35) mux→router {token}\nMSG-ROUTE-OK (0x36) router→client {ack, proceed with normal mux proto}\n```\n\n## Connection Flow\n\n```\n1. MacBook boots jsh:\n ,mux server --router uap.tf:443 --name macbook --password hunter2\n\n2. MacBook → VPS: TLS connect, MSG-REGISTER {name:\"macbook\"}\n VPS → MacBook: MSG-REGISTERED {ok}\n (control channel stays open, ping/pong keeps it alive)\n\n3. Phone:\n ,mux attach uap.tf:443\n\n4. Phone → VPS: TLS connect, MSG-ATTACH\n VPS → Phone: MSG-SESSION-LIST [{name:\"macbook\", host:\"home\", sessions:2},\n {name:\"workbox\", host:\"office\", sessions:1}]\n\n5. Phone → VPS: MSG-SELECT-MUX {name:\"macbook\"}\n\n6. VPS → MacBook: MSG-OPEN-CHANNEL {token:\"abc123\"}\n MacBook → VPS: new TLS conn, MSG-CHANNEL-READY {token:\"abc123\"}\n\n7. VPS pairs Phone↔MacBook data channel\n VPS → Phone: MSG-ROUTE-OK\n\n8. From here on, Phone↔MacBook talk normal mux protocol.\n VPS just copies bytes between the two TLS connections.\n```\n\n## Where It Fits in the Codebase\n\n| New/Modified | File | What |\n|---|---|---|\n| **New** | `src/jsh/mux-router.ss` | Router server (runs on VPS) |\n| **New** | `src/jsh/mux-relay.ss` | Relay/bridge logic — pairing two transports |\n| **Modify** | `src/jsh/mux-proto.ss` | Add message types 0x30-0x36 |\n| **Modify** | `src/jsh/mux-server.ss` | Add `--router` flag, registration logic, `MSG-OPEN-CHANNEL` handler |\n| **Modify** | `src/jsh/mux-client.ss` | Handle `MSG-SESSION-LIST` menu, `MSG-SELECT-MUX` |\n| **Modify** | `src/jsh/main.ss` | Add `,mux router` command, `--router` flag to `,mux server` |\n\n## Authentication Model\n\n- **Mux→Router auth:** The mux server authenticates to the router using the existing challenge-response (scrypt + HMAC). The router has a config of allowed mux names + password hashes.\n- **Client→Router auth:** Client authenticates to the router first (proves they're allowed to see the menu). Then after routing, they authenticate to the actual mux server through the relay (the existing auth handshake flows through transparently).\n- Two layers: router-level access control + per-mux passwords. A client needs both.\n\n## Key Design Decisions\n\n### 1. Router as dumb relay vs. smart proxy\n\nRecommendation: **dumb relay**. After pairing, the router just copies bytes. This means encryption between client and mux is end-to-end (the router can't read session content even if compromised). The router only sees the initial handshake to route.\n\n### 2. Heartbeat/reconnect\n\nWhen the MacBook's control channel drops (laptop sleep, network change), it should auto-reconnect and re-register. The router marks it offline until it reconnects.\n\n### 3. NAT traversal\n\nSince the mux server initiates all connections outward, no port forwarding needed on the home network. This is the whole point.\n\n### 4. Multiple routers\n\nA mux server could register with multiple routers for redundancy. Each registration is just an outbound TLS connection.\n\n---\n\n## Implementation Guide — Step by Step\n\nThis section is a complete, ordered implementation plan. Each step produces a buildable, testable increment. Follow the order exactly — later steps depend on earlier ones.\n\n### Prerequisites\n\n- Read and understand these files before starting:\n - `src/jsh/mux-proto.sls` — wire protocol (message framing, encode/decode)\n - `src/jsh/mux-transport.sls` — transport abstraction (FD, TLS, WebSocket)\n - `src/jsh/mux-auth.sls` — challenge-response auth + encryption\n - `src/jsh/mux-server.sls` — server event loop, client handling\n - `src/jsh/mux-client.sls` — client attach, relay loop\n - `src/jsh/mux-session.sls` — session/window/pane data model\n - `build-jerboa.ss` — build system (.ss → .sls compilation)\n\n- Remember: **only edit `.ss` source files**. The `.sls` files are generated by `build-jerboa.ss`. If a `.ss` file doesn't exist yet for a module, that means the `.sls` IS the source (legacy). For new files, create `.ss` files.\n\n---\n\n### Step 1: Add Router Protocol Messages to `mux-proto`\n\n**File:** `src/jsh/mux-proto.sls` (this one is edited directly — it has no `.ss` source)\n\n**What to do:**\n\n1. Add these constants after the existing `MSG-ENCRYPTED` definition (line 45):\n\n```scheme\n;; Router protocol messages\n(define MSG-REGISTER #x30) ;; mux→router: register this mux server\n(define MSG-REGISTERED #x31) ;; router→mux: registration acknowledged\n(define MSG-ROUTER-LIST #x32) ;; router→client: list of available muxes\n(define MSG-SELECT-MUX #x33) ;; client→router: select a mux by name\n(define MSG-OPEN-CHANNEL #x34) ;; router→mux: open a data channel (token)\n(define MSG-CHANNEL-READY #x35) ;; mux→router: data channel connected (token)\n(define MSG-ROUTE-OK #x36) ;; router→client: routing established, proceed\n(define MSG-ROUTER-AUTH #x37) ;; router→mux/client: router-level auth challenge\n```\n\n2. Add all new names to the `(export ...)` list at the top (lines 8-19).\n\n**Note:** `MSG-SESSION-LIST` (#x0C) already exists in the protocol and is used for intra-mux session listing. The router uses `MSG-ROUTER-LIST` (#x32) to avoid collision — the payload format is different (mux names vs session IDs).\n\n**Verify:** `make jsh-musl` builds clean.\n\n---\n\n### Step 2: Create the Relay/Bridge Module — `src/jsh/mux-relay.ss`\n\n**File:** New file `src/jsh/mux-relay.ss`\n\n**Purpose:** A bidirectional byte copier that bridges two transports. After the router pairs a client with a mux data channel, it hands both transports to this relay. The relay copies bytes in both directions until one side disconnects.\n\n**What to implement:**\n\n```scheme\n(import (jerboa prelude))\n;; This becomes (library (jsh mux-relay) ...) after build-jerboa.ss processes it\n\n;; The relay needs access to:\n;; - transport-read, transport-write, transport-close, transport-ready?\n;; from (jsh mux-transport)\n;; - ffi-nanosleep-us from (jsh ffi)\n```\n\n**Core function — `relay-bridge`:**\n\n```\n(relay-bridge transport-a transport-b) → void (returns when either side disconnects)\n```\n\nThe function runs a poll loop (matching the style of `server-event-loop` in `mux-server.sls:474`):\n\n```\nloop:\n if transport-a ready?:\n read from a into buf (4096 bytes, non-blocking style)\n if read <= 0: break (a disconnected)\n write all bytes to b\n if write fails: break (b disconnected)\n if transport-b ready?:\n read from b into buf\n if read <= 0: break\n write all bytes to a\n if write fails: break\n sleep 500µs (matches relay-loop sleep at mux-client.sls:585)\n loop\n```\n\n**Implementation details:**\n\n- Use `transport-read-fn` / `transport-write-fn` / `transport-id` — same pattern as `mux-client.sls:251-252`.\n- Write must handle partial writes (loop until all bytes sent), same pattern as `mux-write-message` in `mux-proto.sls:87-95`.\n- On exit, close both transports via `transport-close`.\n- Use `(ffi-nanosleep-us 500)` for the sleep — same as `mux-client.sls:585`.\n- Wrap the entire loop in `(guard (e [#t ...])` so transport errors don't crash the router.\n\n**Also export:** `relay-bridge-async` — a variant that spawns the relay in a Chez `fork-thread` so the router event loop isn't blocked:\n\n```scheme\n(define (relay-bridge-async tr-a tr-b on-done)\n (fork-thread\n (lambda ()\n (guard (e [#t (void)])\n (relay-bridge tr-a tr-b))\n (when on-done (on-done)))))\n```\n\n**Add to build chain:** Add `(jsh mux-relay)` to the module list in `build-jsh.ss` (look for where other `jsh mux-*` modules are listed, around lines 35-57).\n\n**Verify:** `make jsh-musl` builds clean.\n\n---\n\n### Step 3: Create the Router Server — `src/jsh/mux-router.ss`\n\n**File:** New file `src/jsh/mux-router.ss`\n\nThis is the largest new component. Model it closely on `mux-server.sls` — it's structurally the same (listen, accept, event loop) but instead of managing PTYs, it manages a registry of mux servers and routes clients to them.\n\n#### 3a. Define Router Data Structures\n\n**Registered mux record:**\n\n```scheme\n(define-record-type registered-mux\n (fields\n [immutable name] ;; string: \"macbook\", \"workbox\", etc.\n [immutable host-label] ;; string: human-readable origin label\n [immutable transport] ;; transport: the control channel to this mux\n [mutable session-count] ;; fixnum: how many sessions available\n [mutable alive?] ;; boolean: control channel still up\n [mutable last-ping] ;; fixnum: epoch seconds of last ping/pong\n ))\n```\n\n**Pending channel record** (for in-flight client↔mux pairing):\n\n```scheme\n(define-record-type pending-channel\n (fields\n [immutable token] ;; bytevector: 32-byte random token\n [immutable client-tr] ;; transport: the waiting client\n [immutable mux-name] ;; string: which mux this is for\n [immutable created-at] ;; fixnum: epoch seconds (for timeout)\n ))\n```\n\n**Router client record** (someone who connected but hasn't been routed yet):\n\n```scheme\n(define-record-type router-client\n (fields\n [immutable transport]\n [mutable state] ;; 'pending-auth | 'pending-select | 'routed | 'failed\n [mutable auth-challenge] ;; bytevector or #f\n [mutable fail-count]\n ))\n```\n\n**Router state:**\n\n```scheme\n(define-record-type router-state\n (fields\n [immutable listen-fds]\n [mutable muxes] ;; list of registered-mux\n [mutable clients] ;; list of router-client\n [mutable pending-channels] ;; list of pending-channel\n [mutable running?]\n [immutable tls-ctx]\n [immutable password-hash] ;; router-level auth (optional)\n [immutable password-salt]\n ))\n```\n\n#### 3b. Implement the Router Event Loop\n\nModel on `server-event-loop` (`mux-server.sls:474-506`):\n\n```\nrouter-event-loop:\n check-signals! (SIGTERM, SIGCHLD not needed here)\n accept-new-connections!\n handle-mux-control-channels! ;; read from registered muxes\n handle-client-input! ;; read from unrouted clients\n expire-pending-channels! ;; timeout stale pairings (30s)\n cleanup-dead-muxes! ;; remove disconnected muxes\n cleanup-dead-clients! ;; remove disconnected clients\n sleep 1ms\n loop\n```\n\n#### 3c. Accept and Classify Connections\n\nWhen a new TLS connection arrives, the router doesn't know yet if it's a mux registering or a client attaching. Use the same HTTP detection pattern as `mux-server.sls:579-617`:\n\n1. Accept TCP fd, TLS handshake (reuse `wrap-accepted-fd` pattern)\n2. If HTTP Upgrade → WebSocket (for browser clients, future)\n3. If HTTP probe → nginx camouflage (copy `handle-http-probe!`)\n4. If raw mux protocol → read first message:\n - `MSG-REGISTER` → it's a mux server registering (go to 3d)\n - `MSG-ATTACH` → it's a client wanting to connect (go to 3e)\n - `MSG-CHANNEL-READY` → it's a mux opening a data channel (go to 3f)\n\n**Critical subtlety:** The router must do a **non-blocking first-message read** to classify. But unlike the existing server (which speaks first with `MSG-AUTH-REQUIRED`), here **both** muxes and clients speak first. So after TLS + WebSocket detection, do a blocking read of the first mux message — the connecting party always sends its intent immediately.\n\n#### 3d. Handle MSG-REGISTER (Mux Server Registration)\n\nWhen a mux sends `MSG-REGISTER`:\n\n1. **Parse payload:** The payload format should be a simple length-prefixed structure:\n ```\n [name-len:2B big-endian][name:UTF-8][host-len:2B][host-label:UTF-8][session-count:2B]\n ```\n\n2. **Authenticate the mux** (if router has a password configured):\n - Send `MSG-ROUTER-AUTH` with salt + challenge (same format as `MSG-AUTH-REQUIRED`, payload = salt(32) || challenge(32))\n - Wait for `MSG-AUTH-RESPONSE` (same proof computation as existing auth)\n - Verify with `mux-auth-verify-proof`\n - On failure: send `MSG-AUTH-FAIL`, close\n\n3. **Register:**\n - Create `registered-mux` record\n - Add to `router-state-muxes`\n - Send `MSG-REGISTERED` (empty payload = success)\n - Keep transport open as control channel\n\n4. **Control channel maintenance:**\n - In the event loop, periodically send `MSG-PING` on each control channel\n - If no `MSG-PONG` within 30s, mark mux as dead\n - If mux sends `MSG-REGISTER` again (re-register after reconnect), update the record\n\n#### 3e. Handle Client Connection (MSG-ATTACH from Client)\n\nWhen a client sends `MSG-ATTACH`:\n\n1. **Router-level auth** (if configured):\n - Same challenge-response as 3d. Send `MSG-ROUTER-AUTH`, wait for `MSG-AUTH-RESPONSE`.\n - This proves the client is allowed to use the router at all.\n\n2. **Build mux menu:**\n - Collect all `registered-mux` records where `alive?` is `#t`\n - Encode as `MSG-ROUTER-LIST` payload:\n ```\n [count:2B]\n For each mux:\n [name-len:2B][name:UTF-8][host-len:2B][host-label:UTF-8][session-count:2B][online:1B]\n ```\n\n3. **Send menu:** `mux-write-message ... MSG-ROUTER-LIST payload`\n\n4. **Wait for selection:** Read `MSG-SELECT-MUX` from client:\n - Payload: `[name-len:2B][name:UTF-8]`\n - Look up in `router-state-muxes`\n - If not found or dead: send `MSG-ERROR` with \"mux not available\", close\n - If found: proceed to channel opening (3f)\n\n#### 3f. Open Data Channel (The Routing)\n\nAfter client selects a mux:\n\n1. **Generate token:** `(rust-random-bytes 32)` — 32-byte random token\n\n2. **Create pending-channel record:**\n ```scheme\n (make-pending-channel token client-transport mux-name (current-epoch))\n ```\n Add to `router-state-pending-channels`.\n\n3. **Send MSG-OPEN-CHANNEL to mux** (via control channel):\n - Payload: the 32-byte token\n - `mux-write-message` on the registered-mux's transport\n\n4. **Mux receives MSG-OPEN-CHANNEL** (handled in Step 5):\n - Mux dials a NEW TLS connection to the router\n - Sends `MSG-CHANNEL-READY` with the same 32-byte token as payload\n\n5. **Router receives MSG-CHANNEL-READY** on a new connection:\n - Extract token from payload\n - Find matching `pending-channel` by token comparison (`rust-timing-safe-equal?` from `(std crypto native-rust)` — prevent timing attacks)\n - If found:\n - Remove from pending list\n - Send `MSG-ROUTE-OK` to the client (empty payload)\n - Spawn `relay-bridge-async` with client-transport and mux-data-transport\n - The relay copies bytes bidirectionally — client↔mux talk normal mux protocol from here\n - If not found (stale/invalid token): close the connection\n\n6. **Timeout:** In the event loop, expire any `pending-channel` older than 30 seconds. Send `MSG-ERROR` to the waiting client, close both.\n\n#### 3g. Entry Point — `mux-router-start`\n\n```scheme\n(define (mux-router-start port cert-bv key-bv password)\n ;; Same TLS setup as mux-server-start-tcp-impl (mux-server.sls:368-455):\n ;; 1. Hash password if provided\n ;; 2. Create TLS server context from cert-bv/key-bv PEM\n ;; 3. Create TCP listener on port\n ;; 4. Set up signal handlers (SIGHUP ignore, SIGPIPE ignore, SIGTERM flag)\n ;; 5. Enter router-event-loop\n ;; 6. Cleanup on exit\n ...)\n```\n\n**Add to build chain:** Add `(jsh mux-router)` to `build-jsh.ss`.\n\n**Verify:** `make jsh-musl` builds clean. Router doesn't need PTY/session code — it never spawns shells.\n\n---\n\n### Step 4: Add `,mux router` Command to main\n\n**File:** `src/jsh/main.sls` (or the `.ss` source that generates it — check `build-jerboa.ss`)\n\n**What to do:**\n\n1. Add `(jsh mux-router)` to the imports.\n\n2. In the `,mux` command dispatch (around line 439-582 in main.sls), add a new subcommand:\n\n```\n,mux router -p PORT [--password PW]\n```\n\nThis calls `mux-router-start`. The router always needs:\n- A port (required — it's internet-facing)\n- TLS cert/key from embed (same as `,mux server -p`)\n- Optional password for router-level access control\n\n3. Parse the arguments using the same flag-parsing pattern as the existing `,mux server` command (lines 503-541 in main.sls). The flags are:\n - `-p PORT` — required, TCP port\n - `--password PW` — optional, router-level password\n - No `--name` needed (the router is singular)\n\n4. The router should daemonize the same way `,mux server` does (via `ffi-fork-exec` — see how the server forks around line 1020-1031 in main.sls).\n\n**Verify:** `make jsh-musl && ./jsh-musl -c ',mux router -p 8443 --password test'` starts and listens.\n\n---\n\n### Step 5: Add `--router` Flag to Mux Server\n\n**File:** `src/jsh/mux-server.sls`\n\n**What to add:**\n\nA new exported function and supporting logic for registering with a remote router.\n\n#### 5a. New Export\n\n```scheme\nmux-server-register-router ;; (name router-host router-port password) → void\n```\n\nAdd to the `(export ...)` block.\n\n#### 5b. Registration Logic\n\nImplement `mux-server-register-router`:\n\n```scheme\n(define (mux-server-register-router name router-host router-port router-password)\n ;; 1. Connect to router via TLS\n ;; Use tls-connect-pinned with empty pin (accept self-signed)\n ;; Same as mux-client.sls:122\n (let ([tr (tls-connect-pinned router-host router-port (make-bytevector 0))])\n\n ;; 2. If router requires auth, handle challenge-response\n ;; Read first message — if MSG-ROUTER-AUTH, do auth handshake\n ;; (same flow as do-auth-handshake in mux-client.sls:295-327,\n ;; but using router-password)\n\n ;; 3. Send MSG-REGISTER\n ;; Encode: name-len(2) || name || host-label-len(2) || host-label || session-count(2)\n ;; host-label = (machine-type) or hostname from (getenv \"HOSTNAME\")\n (let ([payload (encode-register-payload name host-label session-count)])\n (mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-REGISTER payload))\n\n ;; 4. Read MSG-REGISTERED ack\n ;; 5. Return the transport (control channel) — caller stores it\n tr))\n```\n\n#### 5c. Integrate into Server Event Loop\n\nIn `server-event-loop` (`mux-server.sls:474`), add a step to monitor the router control channel:\n\n```\n;; In the event loop, after existing steps:\nhandle-router-control! ;; check for MSG-OPEN-CHANNEL, MSG-PING\n```\n\nImplement `handle-router-control!`:\n\n```scheme\n(define (handle-router-control! state)\n (let ([router-tr (server-state-router-transport state)]) ;; new field\n (when (and router-tr (transport-ready? router-tr))\n (let-values ([(type payload)\n (mux-read-message (transport-read-fn router-tr)\n (transport-id router-tr))])\n (cond\n [(not type)\n ;; Router disconnected — schedule reconnect\n (server-debug \"ROUTER: control channel lost, will reconnect\")\n (server-state-router-transport-set! state #f)\n (schedule-router-reconnect! state)]\n\n [(= type MSG-OPEN-CHANNEL)\n ;; Router wants us to open a data channel for a client\n (let ([token payload]) ;; payload IS the 32-byte token\n (server-debug \"ROUTER: opening data channel\")\n (fork-thread\n (lambda ()\n (open-data-channel-to-router! state token))))]\n\n [(= type MSG-PING)\n (mux-write-message (transport-write-fn router-tr)\n (transport-id router-tr) MSG-PONG (make-bytevector 0))]\n\n [else (void)])))))\n```\n\n#### 5d. Open Data Channel Back to Router\n\n```scheme\n(define (open-data-channel-to-router! state token)\n ;; 1. Dial new TLS connection to router\n (let ([tr (tls-connect-pinned\n (server-state-router-host state)\n (server-state-router-port state)\n (make-bytevector 0))])\n\n ;; 2. Send MSG-CHANNEL-READY with token\n (mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-CHANNEL-READY token)\n\n ;; 3. Now this transport IS a client connection.\n ;; Hand it to accept-mux-client! — from here on it's identical to\n ;; a direct client connecting over TCP.\n (accept-mux-client! state tr)))\n```\n\n**This is the key insight:** once the data channel is established, the mux server treats it exactly like any other client connection. The existing `accept-mux-client!` sends `MSG-AUTH-REQUIRED` or `MSG-STATUS`, and the client on the other end of the relay goes through the normal auth handshake. The router just copies bytes.\n\n#### 5e. Add Router Fields to server-state\n\nAdd these fields to the `server-state` record (`mux-server.sls:113`):\n\n```scheme\n[mutable router-transport] ;; transport or #f — control channel to router\n[mutable router-host] ;; string or #f\n[mutable router-port] ;; fixnum or #f\n[mutable router-password] ;; string or #f\n[mutable router-reconnect-at] ;; epoch seconds or #f — when to retry\n```\n\nInitialize all to `#f` in `make-server-state` calls. When `--router` is passed, populate them and call `mux-server-register-router` after the event loop starts.\n\n#### 5f. Auto-Reconnect\n\nIn the event loop, after `handle-router-control!`:\n\n```scheme\n(when (and (server-state-router-host state)\n (not (server-state-router-transport state)))\n (let ([now (ffi-clock-realtime-sec)]\n [at (server-state-router-reconnect-at state)])\n (when (and at (>= now at))\n (guard (e [#t\n ;; Reconnect failed — try again in 10s\n (server-state-router-reconnect-at-set! state (+ now 10))])\n (let ([tr (mux-server-register-router\n (server-state-name state)\n (server-state-router-host state)\n (server-state-router-port state)\n (server-state-router-password state))])\n (server-state-router-transport-set! state tr)\n (server-state-router-reconnect-at-set! state #f))))))\n```\n\n**Verify:** `make jsh-musl` builds clean.\n\n---\n\n### Step 6: Add `--router` Flag to `,mux server` Command\n\n**File:** Where `,mux server` arguments are parsed (main.sls ~503-541)\n\nAdd `--router HOST:PORT` flag parsing:\n\n```scheme\n;; In the flag loop, add:\n[(string=? arg \"--router\")\n (set! router-addr (next-arg))] ;; \"uap.tf:443\"\n```\n\nAfter the server starts (but before entering the event loop), if `router-addr` is set:\n\n```scheme\n(when router-addr\n (let-values ([(host port) (parse-host-port router-addr)])\n (server-state-router-host-set! state host)\n (server-state-router-port-set! state port)\n (server-state-router-password-set! state password) ;; reuse server password or add --router-password\n (guard (e [#t\n (fprintf (current-error-port) \"jsh: router registration failed, will retry~n\")\n (server-state-router-reconnect-at-set! state\n (+ (ffi-clock-realtime-sec) 5))])\n (let ([tr (mux-server-register-router name host port password)])\n (server-state-router-transport-set! state tr)\n (fprintf (current-error-port) \" router: registered with ~a:~a~n\" host port)))))\n```\n\n**Verify:** `make jsh-musl && ./jsh-musl -c ',mux server --name macbook --router localhost:8443 --password test'` registers with the router.\n\n---\n\n### Step 7: Modify Client to Handle Router Menu\n\n**File:** `src/jsh/mux-client.sls`\n\n**What changes:**\n\nWhen a client connects to a router (instead of a direct mux server), the first message back will be `MSG-ROUTER-LIST` instead of `MSG-AUTH-REQUIRED` or `MSG-STATUS`.\n\n#### 7a. Modify `attach-with-transport`\n\nIn `attach-with-transport` (`mux-client.sls:250-290`), add a new case in the initial message dispatch:\n\n```scheme\n[(= type MSG-ROUTER-LIST)\n ;; Connected to a router — show mux selection menu\n (handle-router-menu tr payload password)]\n```\n\nThis goes after the existing `MSG-AUTH-REQUIRED` and `MSG-STATUS` cases.\n\nBut wait — the client sends `MSG-ATTACH` first (the router needs to know it's a client, not a mux registering). So **before** reading the initial message, the client must send `MSG-ATTACH`:\n\n```scheme\n;; At the start of attach-with-transport, before reading:\n(mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-ATTACH (make-bytevector 0))\n```\n\n**However**, this changes behavior for direct connections too. The existing mux server doesn't expect `MSG-ATTACH` as the first client message — the server speaks first. Two options:\n\n**Option A (recommended):** Only send `MSG-ATTACH` when connecting to a known router. Add a `router?` parameter to `attach-with-transport`. When the user does `,mux attach uap.tf:443`, detect that it's a router by either:\n- A `--router` flag: `,mux attach --router uap.tf:443`\n- Or: always send `MSG-ATTACH` first, and modify `mux-server.sls` to handle it gracefully (ignore it if unexpected). This is the more resilient approach.\n\n**Option B (simpler):** Add a `--via` flag for router connections:\n\n```\n,mux attach --via uap.tf:443 # connect through router\n,mux attach myhost:443 # direct connect (existing)\n```\n\nGo with **Option A** — add a `--via` or `--router` flag so the intent is explicit. Create a new function:\n\n```scheme\n(define (mux-client-attach-via-router host port password)\n ;; 1. TLS connect to router\n (let ([tr (tls-connect-pinned host port (make-bytevector 0))])\n ;; 2. Send MSG-ATTACH (tells router \"I'm a client\")\n (mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-ATTACH (make-bytevector 0))\n ;; 3. Handle router auth if needed (MSG-ROUTER-AUTH)\n ;; 4. Receive MSG-ROUTER-LIST\n ;; 5. Display menu, get selection\n ;; 6. Send MSG-SELECT-MUX\n ;; 7. Receive MSG-ROUTE-OK\n ;; 8. From here, call attach-with-transport — normal mux flow\n ))\n```\n\n#### 7b. Implement the Mux Selection Menu\n\n```scheme\n(define (handle-router-menu tr router-list-payload password)\n ;; 1. Decode payload:\n ;; [count:2B] then for each: [name-len:2B][name][host-len:2B][host][sessions:2B][online:1B]\n (let ([muxes (decode-router-list router-list-payload)])\n\n ;; 2. Display menu on stderr (raw terminal):\n ;; jsh router — select a server:\n ;; 1) macbook (home) — 2 sessions\n ;; 2) workbox (office) — 1 session\n ;; >\n (fprintf (current-error-port) \"~njsh router — select a server:~n\")\n (let loop ([i 0] [muxes muxes])\n (when (pair? muxes)\n (let ([m (car muxes)])\n (fprintf (current-error-port) \" ~a) ~a (~a) — ~a session~a~n\"\n (+ i 1)\n (mux-entry-name m)\n (mux-entry-host m)\n (mux-entry-sessions m)\n (if (= (mux-entry-sessions m) 1) \"\" \"s\")))\n (loop (+ i 1) (cdr muxes))))\n (fprintf (current-error-port) \"> \")\n (flush-output-port (current-error-port))\n\n ;; 3. Read selection (number or name)\n ;; Use ffi-embed-read-passphrase or just (get-line (current-input-port))\n ;; since we're not in raw mode yet\n (let* ([input (get-line (current-input-port))]\n [selected (resolve-selection input muxes)])\n\n (unless selected\n (fprintf (current-error-port) \"jsh: invalid selection~n\")\n (transport-close tr)\n (error 'mux-client \"invalid router selection\"))\n\n ;; 4. Send MSG-SELECT-MUX\n (let ([name-bv (string->utf8 (mux-entry-name selected))])\n (mux-write-message (transport-write-fn tr) (transport-id tr)\n MSG-SELECT-MUX name-bv))\n\n ;; 5. Wait for MSG-ROUTE-OK\n (let-values ([(type payload)\n (mux-read-message (transport-read-fn tr) (transport-id tr))])\n (cond\n [(and type (= type MSG-ROUTE-OK))\n (fprintf (current-error-port)\n \"jsh: routed to ~a~n\" (mux-entry-name selected))\n ;; 6. Now the transport is bridged to the mux server.\n ;; Call attach-with-transport — the mux server's auth\n ;; handshake flows through transparently.\n (attach-with-transport tr (mux-entry-name selected) password)]\n [(and type (= type MSG-ERROR))\n (fprintf (current-error-port)\n \"jsh: router error: ~a~n\" (utf8->string payload))\n (transport-close tr)]\n [else\n (fprintf (current-error-port) \"jsh: unexpected router response~n\")\n (transport-close tr)])))))\n```\n\n#### 7c. Wire Up the Command\n\nIn main.sls, add handling for `,mux attach --via HOST:PORT`:\n\n```scheme\n[(string=? arg \"--via\")\n (set! via-router (next-arg))]\n```\n\nThen dispatch:\n\n```scheme\n(if via-router\n (let-values ([(host port) (parse-host-port via-router)])\n (mux-client-attach-via-router host port password))\n ;; ... existing direct-connect logic\n )\n```\n\n**Verify:** Full flow test:\n1. Start router: `./jsh-musl -c ',mux router -p 8443 --password test'`\n2. Start mux server: `./jsh-musl -c ',mux server --name macbook --router localhost:8443 --password test'`\n3. Attach via router: `./jsh-musl -c ',mux attach --via localhost:8443 --password test'`\n4. Should see menu with \"macbook\", select it, get a shell.\n\n---\n\n### Step 8: Payload Encoding/Decoding Helpers\n\nThese are used by Steps 3-7. Put them in `mux-router.ss` (router-side) and duplicate the decoder in `mux-client.sls` (client-side), or create a shared `mux-router-proto.ss` module.\n\n**Recommended:** Add to `mux-proto.sls` since it's the shared protocol module.\n\n```scheme\n;; Encode MSG-REGISTER payload\n(define (encode-register-payload name host-label session-count)\n ;; [name-len:2B BE][name:UTF-8][host-len:2B BE][host:UTF-8][session-count:2B BE]\n (let* ([name-bv (string->utf8 name)]\n [host-bv (string->utf8 host-label)]\n [nlen (bytevector-length name-bv)]\n [hlen (bytevector-length host-bv)]\n [buf (make-bytevector (+ 2 nlen 2 hlen 2))])\n (bytevector-u16-set! buf 0 nlen (endianness big))\n (bytevector-copy! name-bv 0 buf 2 nlen)\n (bytevector-u16-set! buf (+ 2 nlen) hlen (endianness big))\n (bytevector-copy! host-bv 0 buf (+ 4 nlen) hlen)\n (bytevector-u16-set! buf (+ 4 nlen hlen) session-count (endianness big))\n buf))\n\n;; Decode MSG-REGISTER payload → (values name host-label session-count)\n(define (decode-register-payload bv)\n (let* ([nlen (bytevector-u16-ref bv 0 (endianness big))]\n [name (utf8->string (subbytevector bv 2 (+ 2 nlen)))]\n [hlen (bytevector-u16-ref bv (+ 2 nlen) (endianness big))]\n [host (utf8->string (subbytevector bv (+ 4 nlen) (+ 4 nlen hlen)))]\n [sessions (bytevector-u16-ref bv (+ 4 nlen hlen) (endianness big))])\n (values name host sessions)))\n\n;; Encode MSG-ROUTER-LIST payload\n(define (encode-router-list muxes)\n ;; muxes: list of (name host-label session-count online?)\n ;; [count:2B] then for each: [name-len:2B][name][host-len:2B][host][sessions:2B][online:1B]\n ...)\n\n;; Decode MSG-ROUTER-LIST payload → list of alist entries\n(define (decode-router-list bv)\n ...)\n\n;; Helper: extract sub-bytevector\n(define (subbytevector bv start end)\n (let ([out (make-bytevector (- end start))])\n (bytevector-copy! bv start out 0 (- end start))\n out))\n```\n\n---\n\n### Step 9: Keepalive and Health\n\n#### Router Side\n\nIn `router-event-loop`, every 15 seconds:\n\n```scheme\n(for-each\n (lambda (mux)\n (when (registered-mux-alive? mux)\n (let ([ok (mux-write-message\n (transport-write-fn (registered-mux-transport mux))\n (transport-id (registered-mux-transport mux))\n MSG-PING (make-bytevector 0))])\n (unless ok\n (registered-mux-alive?-set! mux #f)))))\n (router-state-muxes state))\n```\n\nHandle `MSG-PONG` in the mux control channel reader — update `last-ping`. If `last-ping` is older than 45s, mark dead.\n\n#### Mux Server Side\n\nIn `handle-router-control!`, handle `MSG-PING`:\n\n```scheme\n[(= type MSG-PING)\n (mux-write-message (transport-write-fn router-tr)\n (transport-id router-tr) MSG-PONG (make-bytevector 0))]\n```\n\nAlready shown in Step 5c.\n\n---\n\n### Step 10: Testing Plan\n\n**Unit test — protocol encoding:**\n- Encode/decode `MSG-REGISTER`, `MSG-ROUTER-LIST` payloads round-trip\n\n**Integration test — local loopback:**\n1. Start router on port 18443\n2. Start mux-server with `--router localhost:18443`\n3. Verify registration (router logs \"macbook registered\")\n4. Client attaches via `--via localhost:18443`\n5. Client sees menu, selects macbook\n6. Client gets a working shell (type `echo hello`, see output)\n7. Client detaches (Ctrl-b d)\n8. Kill mux-server, verify router marks it offline\n9. Restart mux-server, verify it re-registers\n10. Client attaches again, verify it works\n\n**Stress test — reconnect:**\n1. Start router + mux-server\n2. Kill router (SIGTERM)\n3. Verify mux-server logs reconnect attempts\n4. Restart router\n5. Verify mux-server re-registers within 10s\n6. Client can attach\n\n**Security test:**\n1. Wrong router password → rejected\n2. Wrong mux password (through relay) → rejected\n3. Invalid token in `MSG-CHANNEL-READY` → connection closed\n4. Expired pending channel (wait 31s) → client gets error\n\n---\n\n### Build Order Summary\n\n```\nStep 1: mux-proto.sls ← add message constants (5 min)\nStep 2: mux-relay.ss ← new file, ~60 lines (30 min)\nStep 8: payload helpers ← add to mux-proto.sls (30 min)\nStep 3: mux-router.ss ← new file, ~400 lines (the big one) (4 hrs)\nStep 4: main.sls ← add ,mux router command (30 min)\nStep 5: mux-server.sls ← --router registration + data channel (2 hrs)\nStep 6: main.sls ← --router flag for ,mux server (15 min)\nStep 7: mux-client.sls ← router menu + --via flag (1.5 hrs)\nStep 9: keepalive ← ping/pong in router + server (30 min)\nStep 10: testing ← integration tests (1 hr)\n```\n\nBuild and smoke-test (`make jsh-musl && ./jsh-musl -c 'echo ok'`) after EVERY step. Do not batch steps.\n"} {"text":";; FILE: jerboa-shell/README.md\n# jerboa-shell\n\nA POSIX-compatible shell (`jsh`) built with [Jerboa](https://git.sr.ht/~lisp/jerboa) — a Chez Scheme standard library.\n\n**Status:** 1056/1179 (90%) Oils POSIX compat tests passing · 66/66 unit tests passing · 56/56 binary functional tests passing\n\n## Architecture\n\n`jsh` runs shell source code (originally from [jerboa-shell](https://github.com/ober/jerboa-shell)) translated to R6RS `.sls` libraries that run natively on Chez Scheme using:\n\n1. **Jerboa** — provides `(std ...)` and `(jerboa ...)` modules (sort, format, transducers, logging, threads, pattern matching, etc.)\n2. **Gherkin runtime** — MOP/class system and Jerboa compiler support for in-shell `eval`\n3. **C FFI shim** — POSIX system calls (fork, exec, signals, termios, etc.)\n4. **Self-contained binary** — boot files (petite, scheme, jsh) embedded as C byte arrays; fully portable single ELF\n\n### Enhancements over jerboa-shell\n\n| Feature | Implementation |\n|---------|---------------|\n| History search | Transducer pipeline `(std transducer)`: prefix filter + seen-set dedup + take |\n| Debug logging | Structured log via `(std log)`, activated by `JSH_DEBUG=1` |\n| Binary portability | memfd program loading (avoids Chez boot-file thread limitation) |\n| Embedded files | Virtual `//embed/` filesystem compiled into the binary with optional ChaCha20-Poly1305 encryption |\n| Coreutils builtins | 90+ GNU coreutils commands run in-process (busybox-style) via [jerboa-coreutils](https://git.sr.ht/~lisp/jerboa-coreutils) |\n| Pipeline threading | Builtin pipeline stages run as real POSIX threads — `ls \\| sort \\| wc` uses 3 CPU cores |\n| Background jobs | `,run CMD` runs detached with stdout+stderr captured to a log; `,run ls`/`log ID`/`kill ID` |\n| Mux resume + snapshot | per-pane output ring → reconnect with `,mux attach --from N` to replay missed output; Ctrl-b W/E save/restore session layout + scrollback |\n| Wormhole transfers | Optional `worm` feature exposes `,worm send`, `,worm receive`, relay, transit-relay, and SSH-key transfer commands via `jerboa-wormhole` |\n\n## Prerequisites\n\nOur Chez Scheme 10.4 (see the Chez note below). Normal builds use the\nself-contained `jerbuild` tool from Jerboa. The Makefile prefers local tools in\nthis order: `./jerbuild`, `.jerboa/bin/jerbuild`, `../jerboa/dist/jerbuild`,\nthen `jerbuild` on `PATH`. If none are available, it downloads the Jerboa release\nartifact for the host into `.jerboa/bin`. The supported artifact targets are\n`macos-arm64`, `linux-amd64`, `linux-arm64`, and `freebsd-amd64`; set\n`JERBOA_VERSION` to a Jerboa tag that has those artifacts attached.\n\nFor local builds:\n- Jerboa `jerbuild`/`jerboa` tools, found locally or fetched from\n [SourceHut release artifacts](https://git.sr.ht/~lisp/jerboa)\n- [jerboa-coreutils](https://git.sr.ht/~lisp/jerboa-coreutils) — coreutils builtins\n- [jerboa-ssh](https://github.com/ober/jerboa-ssh) — SSH agent support\n- [jerboa-native-rs](https://git.sr.ht/~lisp/jerboa-native-rs) — Rust native library (TLS via rustls, FUSE, crypto)\n- GCC or Clang\n\n## Building\n\n```bash\n# Native build for THIS host — the everyday build (no Podman).\n# macOS -> jsh-macos; Linux -> static-musl jsh-linux-<arch>; FreeBSD -> jsh-freebsd-<arch>\nmake binary\n\n# Build and install using local Jerboa tools, or fetch release tools if absent.\nmake install\n\n# Cross-build a fully static Linux ELF from any host (Chez xpatch + musl-cross,\n# no Podman/QEMU). scp the result to a Linux box to run.\nmake linux-amd64 # or: make linux-arm64\n\n# Native-only platform builds (run on that platform's host):\nmake freebsd-amd64 # on a FreeBSD amd64 host\nmake android # on a Termux/Android device\n\n# Dynamic jsh binary copied to ./jsh\nmake jsh\n\n# Compile modules only\nmake jsh-compile\n```\n\n> There is no `jsh-musl` target and no Podman/Docker build. `make binary` is the\n> canonical local build; the named `linux-*` targets cross-build statically.\n\n### Static musl Binary (Linux)\n\nCross-build a fully static binary from any host (Chez xpatch + musl-cross, no\ncontainer), or build natively on a Linux box with `make binary`:\n\n```bash\nmake linux-amd64 # cross from any host -> jsh-linux-amd64\n./jsh-linux-amd64 -c 'echo Works anywhere!' # run on a Linux box\n```\n\nBenefits:\n- **Zero dependencies** — works on any Linux (kernel 2.6.39+)\n- **Container-friendly** — runs in `FROM scratch` container images\n- **No Podman/QEMU** — pure Chez cross-compile\n\nSee [docs/musl-build.md](docs/musl-build.md) for details.\n\n### macOS Binary\n\n```bash\nmake macos\n./jsh-macos -c 'echo ok'\n```\n\nUses `.dylib` FFI loading. The `JSH_FFI_LIB` environment variable overrides the default library search path.\n\n### Android / Termux Binary\n\n```bash\nmake android\n./jsh-android -c 'echo ok'\n```\n\nBuilds a PIE ELF for `aarch64-linux-android` using the NDK toolchain. Links `libjerboa_native.a` from `~/jerboa/jerboa-native-rs/target/release/`. See [android.md](android.md) for details.\n\n### Embedded Files\n\nCompile files into the binary for a self-contained deployment with SSH keys, credentials, scripts, and shell config — no external files needed.\n\n```bash\n# Place files in embed/ and build\nmkdir -p embed/.ssh\ncp ~/.ssh/id_rsa embed/.ssh/\nmake binary\n\n# Access embedded files at runtime (jsh-macos / jsh-linux-<arch> / ...)\n./jsh-macos -c 'cat //embed/.ssh/id_rsa'\n./jsh-macos -c 'ssh -i //embed/.ssh/id_rsa user@host'\n```\n\nOptional encryption (ChaCha20-Poly1305, prompted passphrase, no echo):\n\n```bash\nJSH_EMBED_ENCRYPT=1 make binary\n# At runtime:\n,unlock # prompted for passphrase\nssh -i //embed/.ssh/id_rsa user@host\n```\n\nSecurity hardening (activated automatically after `,unlock`):\n- **Passphrase never enters Scheme heap** — prompt + PBKDF2 + zeroing all in C with `explicit_bzero()`\n- **mlock / MADV_DONTDUMP** — key never swapped to disk or included in core dumps\n- **MFD_CLOEXEC** — memfds invisible to child processes unless explicitly passed\n- **Exit cleanup** — decrypted cache and derived key zeroed on shell exit\n\nPassword-store helpers are available in pass-enabled builds after `,unlock`:\n`,pass-store` stores a prompted secret, `,pass` copies a stored secret, and\n`,pass-import-firefox` imports Firefox JSON rows (`url`, `user`, `password`) so\n`,login <site>` can autocomplete login labels and copy the password, username,\nor URL to the clipboard.\n\nSee [docs/embed.md](docs/embed.md) for full documentation.\n\n### Coreutils Builtins\n\n90+ GNU coreutils commands run in-process as shell builtins — no fork/exec overhead:\n\n```bash\n# These all run inside jsh, no external binaries needed\n./jsh -c 'seq 1 1000000 | sort -n | wc -l'\n./jsh -c 'echo hello | tr a-z A-Z | rev'\n./jsh -c 'factor 42'\n```\n\nIncluded commands: basename, cat, chmod, chown, cut, date, dirname, du, env, expr, factor, find, grep, head, id, ln, ls, md5sum, mkdir, mv, od, paste, readlink, realpath, rev, rm, rmdir, seq, sha256sum, sort, stat, tail, tee, touch, tr, true, false, uname, uniq, wc, whoami, xargs, yes, and many more.\n\nPipeline stages run as real POSIX threads (Chez Scheme uses pthreads, not green threads), so `ls | sort | wc` uses 3 CPU cores simultaneously — true parallel execution within a single process.\n\nThe musl static build automatically patches coreutils for static linking (no `load-shared-object`) and registers all required POSIX FFI symbols.\n\n### Sandbox Security\n\nThe `,sb` meta-command provides defense-in-depth isolation using three independent kernel mechanisms:\n\n1. **Landlock LSM** — restricts filesystem access to explicitly allowed paths (symlinks resolved via `realpath(3)` to prevent escapes)\n2. **Seccomp BPF** (Linux, irreversible) — a default-allow **blocklist** that denies\n the dangerous syscall set (ptrace, kernel-module/kexec, bpf, perf_event_open,\n mount, keyctl, …) with `EPERM`, plus socket-creation syscalls under `--no-net`\n and exec/fork for the pure-compute case. Allowing ordinary syscalls keeps\n arbitrary programs working (an earlier allowlist model SIGSYS-killed them).\n3. **SIGALRM timeout** — kills the sandboxed child after the specified deadline\n\nThe sandbox also supports restricted Scheme evaluation via `-e`:\n\n```bash\n,sb -e \"(+ 1 2 3)\" # pure computation in allowlist-only environment\n,sb -r /tmp -c \"cat /tmp/f\" # shell command with Landlock + seccomp\n```\n\nAdditional security features:\n- **FD leak detection** — guardian-based tracking warns about file descriptors and FIFOs not properly closed\n- **FTS query validation** — recording search rejects null bytes, excessive length, and SQL comment markers to prevent FTS5 DoS\n- **Path validation** — sandbox paths must be absolute, non-empty, and null-byte-free\n\n## Testing\n\n```bash\n# Unit tests (66 tests across all core modules)\nmake test\n\n# Binary functional tests (functional tests against the local binary)\nmake test-binary\n\n# Full Oils POSIX compat report vs gsh reference\nmake compat-test\n```\n\nSee [docs/test-status.md](docs/test-status.md) for per-suite results.\n\n## Configuration\n\n```makefile\nJSH_VERSION ?= 0.2.0 # Shell banner/prompt version\nJERBOA_VERSION ?= v0.2.0 # Jerboa artifact tag\nJERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin # Downloaded tool location\nJERBUILD ?= /path/to/jerbuild # Optional explicit tool\nCOREUTILS ?= vendor/jerboa-coreutils/lib # Coreutils library path\nJSH_EMBED ?= embed # Directory for embedded files\n```\n\n## Source Layout\n\n```\njsh.ss # Entry point (Chez script)\njsh-main.c # C main (embeds boot files + program via memfd)\nffi-shim.c # POSIX FFI (fork, exec, signals, termios, memfd, embed crypto, seccomp BPF)\nembed-crypto.c/h # Self-contained ChaCha20-Poly1305 + PBKDF2-HMAC-SHA256\nbuild-jsh.ss # Module compilation driver\nbuild-binary-jsh.ss # 7-step binary build (WPO → boot → C headers → link)\ngen-embed.ss # Build-time embed data generator (scans embed/, encrypts)\nembed/ # Files to compile into the binary (optional)\nsrc/\n compat/gambit.sls # Gambit→Chez compat shims\n jsh/embed.sls # Embedded virtual filesystem module (//embed/ paths)\n jsh/embed-data.sls# Generated: embedded file bytevectors + salt\n jsh/coreutils.sls # Coreutils builtin registration (90+ commands)\n jsh/coreutils-shim.sls # load-shared-object safety shim for dynamic builds\n jsh/ # 31+ translated shell modules (ast, lexer, parser, executor, ...)\ntest/\n test-jsh.ss # Unit test suite (66 tests)\n test-binary.sh # Functional test suite (56 tests, runs against both binaries)\ndocs/\n embed.md # Embedded files documentation\n test-status.md # Per-suite compat results\n optimization.md # Chez compiler optimization notes\n```\n"} {"text":";; FILE: jerboa-shell/startup.ss\n;;; startup.ss — RC file loading and startup sequences for jsh\n;;; Handles login/interactive/non-interactive startup file sourcing.\n\n(export #t)\n(import :std/sugar\n :std/format\n :jsh/util\n :jsh/environment\n :jsh/script\n :jsh/recorder\n :jsh/config)\n\n;;; --- Public interface ---\n\n;; Load startup files based on shell mode.\n;; login?: #t for login shells (first char of $0 is - or --login flag)\n;; interactive?: #t for interactive shells (stdin is tty)\n(def (load-startup-files! env login? interactive?)\n ;; Load ~/.jsh/config first. jsh-config-load! itself warns on and ignores a\n ;; malformed file; the outer with-catch is belt-and-suspenders so a startup\n ;; config problem can never abort shell startup.\n (with-catch (lambda (e) #f)\n (lambda () (jsh-config-load!)))\n (cond\n ;; Interactive login shell\n ((and login? interactive?)\n (source-if-exists! \"/etc/profile\" env)\n ;; First of ~/.jsh_profile, ~/.jsh_login, ~/.profile\n (let ((home (or (env-get env \"HOME\") (home-directory))))\n (or (source-if-exists! (string-append home \"/.jsh_profile\") env)\n (source-if-exists! (string-append home \"/.jsh_login\") env)\n (source-if-exists! (string-append home \"/.profile\") env))))\n ;; Interactive non-login shell\n (interactive?\n (let ((home (or (env-get env \"HOME\") (home-directory))))\n (source-if-exists! (string-append home \"/.jshrc\") env)))\n ;; Non-interactive (script)\n (else\n (let ((env-file (env-get env \"JSH_ENV\")))\n (when env-file\n (source-if-exists! env-file env)))))\n ;; Auto-start recording if JSH_RECORD=1\n (when (and interactive?\n (let ((v (env-get env \"JSH_RECORD\")))\n (and v (string=? v \"1\"))))\n (with-catch (lambda (e) #f)\n (lambda () (recorder-start!)))))\n\n;; Run logout sequence for login shells.\n(def (run-logout! env)\n (let ((home (or (env-get env \"HOME\") (home-directory))))\n (source-if-exists! (string-append home \"/.jsh_logout\") env)))\n\n;;; --- Helpers ---\n\n(def (source-if-exists! path env)\n ;; Source a file if it exists; return #t if sourced, #f if not found.\n (if (file-exists? path)\n (begin\n (source-file! path env)\n #t)\n #f))\n"} -{"text":";; FILE: jerboa-shell/build-jsh-macos.ss\n#!chezscheme\n;;; build-jsh-macos.ss — Build jsh binary on macOS\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:... < build-jsh-macos.ss\n;;;\n;;; This script:\n;;; 1. Patches coreutils/awk/sed/ssl for static builds (no dlopen)\n;;; 2. Compiles jsh modules (using stock scheme)\n;;; 3. Creates boot file + optimized program .so\n;;; 4. Generates C files with embedded boot data\n;;; 5. Compiles C with cc (clang) against static Chez's scheme.h\n;;; 6. Links fully static binary with libkernel.a\n;;;\n;;; The resulting jsh-macos binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") (format \"/Users/~a\" (getenv \"USER\"))))\n(define script-dir (or (getenv \"SCRIPT_DIR\") (current-directory)))\n(define output-name (or (getenv \"JSH_OUTPUT\") \"jsh-macos\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-macos.sh so we know the repo root.\n(define vendor-dir (format \"~a/vendor\" script-dir))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. Callers wrap with (or (getenv \"X\") (dep ...))\n;; to allow env var overrides from the shell script.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jsqlite/src\" home-dir)))\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (dep \"jerboa-coreutils\" \"lib\")))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n(define coreutils-shim\n (let ([upstream (dep-file \"jerboa-coreutils\" \"support/libcoreutils.c\")]\n [local \"patches/libcoreutils.c\"])\n (cond\n [(file-exists? upstream) upstream]\n [(file-exists? local) local]\n [else upstream])))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls).\n;; rustls is preferred over OpenSSL for security.\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; Feature-gated enable flags — gate on BOTH directory existence AND\n;; the feature being in *enabled-features*. This is how JSH_FEATURES\n;; actually controls whether feature dependencies land in the binary.\n(define enable-aws?\n (and has-aws? (memq 'aws *enabled-features*)))\n\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n;; Rust native library — resolve via vendor/ → ~/mine/ → ~/\n(define native-rs-dir\n (let* ([v (format \"~a/jerboa/jerboa-native-rs\" vendor-dir)]\n [m (format \"~a/mine/jerboa/jerboa-native-rs\" home-dir)]\n [h (format \"~a/jerboa/jerboa-native-rs\" home-dir)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n(define native-lib-path\n (format \"~a/target/release/libjerboa_native.a\" native-rs-dir))\n(define native-src-dir\n (format \"~a/src\" native-rs-dir))\n;; Sentinel file written after a successful native build without SQLite.\n;; If absent, the .a was built with default (tls-only) features — must rebuild.\n(define native-features-sentinel\n (format \"~a/target/release/.built-with-tls-crypto-no-sqlite\" native-rs-dir))\n;; Only attempt Rust rebuild if cargo is available (pre-built .a may have been\n;; downloaded by build-jsh-macos.sh — don't clobber it with a failed cargo call)\n(define has-cargo?\n (= 0 (system \"command -v cargo >/dev/null 2>&1\")))\n(when (and has-cargo?\n (file-exists? native-src-dir)\n (or (not (file-exists? native-lib-path))\n ;; Features sentinel absent → stale build (wrong feature set)\n (not (file-exists? native-features-sentinel))\n ;; Check if any .rs file is newer than the .a\n (let ([lib-mtime (file-modification-time native-lib-path)])\n (let check ([files (directory-list native-src-dir)])\n (and (pair? files)\n (let ([f (format \"~a/~a\" native-src-dir (car files))])\n (or (and (> (string-length (car files)) 3)\n (string=? \".rs\" (substring (car files)\n (- (string-length (car files)) 3)\n (string-length (car files))))\n (time>? (file-modification-time f) lib-mtime))\n (check (cdr files)))))))))\n (printf \"~n[0/7] Rebuilding Rust native library (source newer than .a)...~n\")\n (let ([rc (safe-system (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote native-rs-dir)))])\n (unless (= rc 0)\n (fprintf (current-error-port) \"FATAL: cargo build --release --no-default-features --features tls,crypto failed~n\")\n (exit 1)))\n ;; Write sentinel so next build knows the right features were used\n (let ([port (open-output-file native-features-sentinel 'truncate)])\n (display \"tls,crypto,no-sqlite\\n\" port)\n (close-output-port port)))\n(when (and (file-exists? native-lib-path)\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(define has-native-lib? (file-exists? native-lib-path))\n\n;; Rust coreutils static library\n(define rust-host-triple\n (case (machine-type)\n [(tarm64osx) \"aarch64-apple-darwin\"]\n [(ta6osx) \"x86_64-apple-darwin\"]\n [else #f]))\n\n(define rust-coreutils-lib-path\n (or (getenv \"RUST_COREUTILS_LIB\")\n (if rust-host-triple\n (format \"~a/rust-coreutils/target/~a/release/libjsh_coreutils.a\"\n script-dir rust-host-triple)\n (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" script-dir))))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — coreutils builtins will be stubs~n\"))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\"))\n\n;; Chez Scheme static installation\n;; macOS: machine type is tarm64osx (arm64) or ta6osx (x86_64)\n(define chez-machine\n (or (getenv \"CHEZ_MACHINE\")\n (machine-type)))\n\n(define chez-ta6fb\n (or (getenv \"CHEZ_TA6FB\")\n ;; Search Homebrew paths first, then /usr/local\n (let loop ([prefixes '(\"/opt/homebrew/Cellar/chezscheme\" \"/opt/homebrew/lib\" \"/usr/local/lib\")])\n (if (null? prefixes)\n (error 'build \"Cannot find Chez static directory (libkernel.a). Install: brew install chezscheme\")\n (let ([prefix (car prefixes)])\n (if (file-directory? prefix)\n (let check-dirs ([dirs (directory-list prefix)])\n (cond\n [(null? dirs) (loop (cdr prefixes))]\n [else\n (let* ([d (car dirs)]\n ;; For Cellar layout: /opt/homebrew/Cellar/chezscheme/<ver>/lib/csv<ver>/<machine>\n [cellar-path (format \"~a/~a/lib\" prefix d)]\n [direct-path (format \"~a/~a\" prefix d)])\n (cond\n ;; Cellar: check <prefix>/<ver>/lib/csv*/<machine>/libkernel.a\n [(and (file-directory? cellar-path)\n (let ([csv-dirs (filter (lambda (x) (string-prefix? \"csv\" x))\n (directory-list cellar-path))])\n (and (pair? csv-dirs)\n (let ([p (format \"~a/~a/~a\" cellar-path (car csv-dirs) chez-machine)])\n (and (file-exists? (format \"~a/libkernel.a\" p)) p)))))\n => (lambda (p) p)]\n ;; Direct: check <prefix>/csv*/<machine>/libkernel.a\n [(and (string-prefix? \"csv\" d)\n (file-directory? direct-path)\n (let ([p (format \"~a/~a\" direct-path chez-machine)])\n (and (file-exists? (format \"~a/libkernel.a\" p)) p)))\n => (lambda (p) p)]\n [else (check-dirs (cdr dirs))]))]))\n (loop (cdr prefixes))))))))\n\n(define scheme-h-dir chez-ta6fb)\n(define petite-boot-path (format \"~a/petite.boot\" chez-ta6fb))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-ta6fb))\n\n;; OpenSSL include directory — still needed for jerboa_ssh_crypto.c (SSH transport)\n;; libcrypto.a is NO LONGER linked; vault/crypto.sls uses ring via jerboa_native instead\n(define openssl-include-dir\n (let ([brew-inc \"/opt/homebrew/opt/openssl/include\"]\n [brew-inc-x86 \"/usr/local/opt/openssl/include\"])\n (cond\n [(file-directory? brew-inc) brew-inc]\n [(file-directory? brew-inc-x86) brew-inc-x86]\n [else \"/usr/include\"])))\n\n(define openssl-lib-dir\n (let ([brew-lib \"/opt/homebrew/opt/openssl/lib\"]\n [brew-lib-x86 \"/usr/local/opt/openssl/lib\"])\n (cond\n [(file-directory? brew-lib) brew-lib]\n [(file-directory? brew-lib-x86) brew-lib-x86]\n [else \"/usr/lib\"])))\n\n(printf \"Chez static: ~a~n\" chez-ta6fb)\n(printf \"Native lib: ~a~n\" (if has-native-lib? native-lib-path \"not found\"))\n(printf \"~n\")\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo so the broken vendor source is recompiled below.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n;; Compile the Jerboa runtime and stdlib entries before any staged dependency\n;; or jsh module. If these are compiled later, the boot image can embed a\n;; different compilation instance of (jerboa core) than the modules depend on.\n(define boot-jerboa-modules\n '(\"jerboa/runtime\"\n \"std/typed\" \"std/pregexp\" \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\"\n \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/misc/thread\"\n \"jerboa/core\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/alist\"\n \"std/stm\" \"std/foreign\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"\n ;; (std contract condition) — imported by (jerboa core); only has a .ss\n ;; (no .sls), so compile-imported-libraries doesn't auto-write its .so\n ;; and compile-whole-program can't find its .wpo. Force-precompile here.\n \"std/contract/condition\"))\n\n(define (precompile-boot-jerboa-modules! label)\n (printf \"~a~n\" label)\n ;; Parameter settings here must match the step [2/7] compile-program block\n ;; that builds jsh-generated.so. WPO files from a different optimize-level\n ;; or unsafe-* setting are flagged as \"does not define expected compilation\n ;; instance\" by compile-whole-program and fail the build.\n (parameterize ([compile-imported-libraries #t]\n [generate-wpo-files #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (cons (cons jerboa-dir jerboa-dir)\n (library-directories))])\n (for-each\n (lambda (m)\n ;; Source may be either .sls (R6RS) or .ss (Jerboa convention);\n ;; check both. Source absent => skip (module not vendored).\n ;; Compile failures are caught and logged so platform-specific\n ;; modules (e.g. (std os landlock) on macOS) don't abort the loop.\n (let* ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [ss (format \"~a/~a.ss\" jerboa-dir m)]\n [src (cond [(file-exists? sls) sls]\n [(file-exists? ss) ss]\n [else #f])]\n [so (format \"~a/~a.so\" jerboa-dir m)]\n [wpo (format \"~a/~a.wpo\" jerboa-dir m)])\n (when (and src\n (or (not (file-exists? so))\n (not (file-exists? wpo))))\n (printf \" Pre-compiling ~a~n\" src)\n (guard (exn [(condition? exn)\n (printf \" SKIP ~a: ~a~n\"\n m (condition-message-string exn))])\n (compile-library src)))))\n boot-jerboa-modules)))\n\n(define (condition-message-string c)\n ;; Best-effort one-line summary of a Chez condition for skip-log output.\n (cond [(and (condition? c) (message-condition? c))\n (condition-message c)]\n [else (format \"~s\" c)]))\n\n;; Skipped: WPO at step [2/7] now precompiles all transitive imports with\n;; compatible parameter settings. Pre-staging at lower optimize-level here\n;; produced .wpo files that compile-whole-program rejected as \"wrong\n;; compilation instance\".\n;; (precompile-boot-jerboa-modules!\n;; \"[0pre/7] Pre-compiling Jerboa boot dependencies...\")\n\n;; ========== Step 0: Patch coreutils for static builds ==========\n;; Coreutils modules call (load-shared-object #f) at library init time.\n;; In static builds, load-shared-object throws because dlopen is unavailable.\n;; Since FFI symbols are pre-registered via Sforeign_symbol, we patch these out.\n\n(printf \"[0/7] Patching coreutils for static build (no dlopen)...~n\")\n\n(define coreutils-stage (format \"~a/coreutils-stage\" (current-directory)))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(system (format \"mkdir -p '~a'\" coreutils-stage))\n\n(system (format \"cp -a '~a/jerboa-coreutils' '~a/'\"\n coreutils-dir coreutils-stage))\n;; macOS/FreeBSD sed uses -i '' instead of -i (no backup extension)\n(system (format \"find '~a/jerboa-coreutils' -name '*.sls' -exec sed -i '' 's/(load-shared-object #f)/(void)/g' {} +\"\n coreutils-stage))\n;; These modules import string-split explicitly from (std misc string). Newer\n;; (jerboa core) also re-exports string-split, so exclude it from core here.\n(for-each\n (lambda (name)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage name)])\n (when (file-exists? path)\n (system (format \"sed -i '' 's/(jerboa core)/(except (jerboa core) string-split)/' '~a'\"\n path)))))\n '(\"cut.sls\" \"grep.sls\" \"join.sls\"))\n(system (format \"find '~a/jerboa-coreutils' -name '*.so' -delete\"\n coreutils-stage))\n(system (format \"find '~a/jerboa-coreutils' -name '*.wpo' -delete\"\n coreutils-stage))\n\n(printf \" Recompiling patched coreutils...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"common.sls\" \"common/version.sls\" \"common/io.sls\" \"common/security.sls\"))\n (for-each\n (lambda (name)\n (let ([sls (format \"~a/jerboa-coreutils/~a.sls\" coreutils-stage name)])\n (when (file-exists? sls)\n (compile-library sls))))\n '(\"basename\" \"dirname\" \"link\" \"unlink\" \"yes\" \"printenv\"\n \"sleep\" \"whoami\" \"logname\" \"hostname\" \"nproc\" \"tty\" \"sync\" \"hostid\"\n \"cat\" \"head\" \"tail\" \"tac\" \"tee\" \"wc\" \"nl\" \"fold\" \"expand\" \"unexpand\" \"fmt\"\n \"cut\" \"paste\" \"join\" \"comm\" \"sort\" \"uniq\" \"tr\" \"numfmt\"\n \"mkdir\" \"rmdir\" \"mktemp\" \"touch\" \"readlink\" \"realpath\" \"ln\" \"cp\" \"mv\" \"rm\"\n \"install\" \"shred\"\n \"ls\" \"chmod\" \"chown\" \"chgrp\" \"stat\" \"du\" \"df\" \"pathchk\"\n \"date\" \"id\" \"groups\" \"who\" \"users\" \"pinky\" \"uptime\" \"uname\" \"arch\"\n \"seq\" \"expr\" \"basenc\" \"base64\" \"base32\" \"od\"\n \"cksum\" \"md5sum\" \"sha1sum\" \"sha224sum\" \"sha256sum\" \"sha384sum\" \"sha512sum\"\n \"b2sum\" \"sum\"\n \"env\" \"timeout\" \"nice\" \"nohup\" \"chroot\" \"stdbuf\"\n \"truncate\" \"mkfifo\" \"mknod\" \"split\" \"csplit\" \"dd\" \"dircolors\"\n \"tsort\" \"shuf\" \"factor\" \"pr\" \"ptx\" \"stty\"\n \"chcon\" \"runcon\"\n \"dir\" \"vdir\" \"rev\" \"top\")))\n\n;; grep + Rust-backed PCRE2\n(let ([grep-pcre2-patch (format \"~a/patches/grep-pcre2.sls\" (current-directory))])\n (when (file-exists? grep-pcre2-patch)\n (system (format \"mkdir -p '~a/jerboa-coreutils/grep'\" coreutils-stage))\n (system (format \"cp '~a' '~a/jerboa-coreutils/grep/pcre2.sls'\"\n grep-pcre2-patch coreutils-stage))))\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (let ([pcre2-sls (format \"~a/jerboa-coreutils/grep/pcre2.sls\" coreutils-stage)])\n (when (file-exists? pcre2-sls)\n (printf \" Compiling grep/pcre2...~n\")\n (compile-library pcre2-sls)))\n (let ([grep-sls (format \"~a/jerboa-coreutils/grep.sls\" coreutils-stage)])\n (when (file-exists? grep-sls)\n (printf \" Compiling grep...~n\")\n (compile-library grep-sls))))\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: patch pcre2 to use Rust regex\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if enable-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when enable-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (library-directories))])\n (when enable-aws?\n (printf \" Compiling jerboa-aws...~n\")\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0d: Stage jerboa-ssh for static build ==========\n(printf \"[0d/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build\n (system (format \"find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"sed -i '' 's/base64-encode/b64-encode/g' '~a'\" kh))\n (system (format \"sed -i '' 's/base64-decode/b64-decode/g' '~a'\" kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0e: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0e/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n;; Use a separate staging directory for macOS so we don't clobber the\n;; committed musl-targeted vault-stage/ (which has _loaded #f baked in).\n;; Each build cleans + repopulates its own dir from upstream jerboa-fuse.\n(define vault-stage (format \"~a/vault-stage-macos\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" vault-stage vault-stage))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n(when has-jerboa-fuse?\n ;; Copy the jerboa-fuse library tree (chez/fuse/ and chez/vault/)\n (system (format \"mkdir -p '~a/chez/fuse' '~a/chez/vault'\" vault-stage vault-stage))\n ;; FUSE layer\n (system (format \"cp '~a/chez/fuse.sls' '~a/chez/fuse.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/constants.sls' '~a/chez/fuse/constants.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/types.sls' '~a/chez/fuse/types.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/codec.sls' '~a/chez/fuse/codec.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/mount.sls' '~a/chez/fuse/mount.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/access.sls' '~a/chez/fuse/access.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/secmem.sls' '~a/chez/fuse/secmem.sls'\" jerboa-fuse-dir vault-stage))\n ;; Vault layer\n (system (format \"cp '~a/chez/vault.sls' '~a/chez/vault.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/format.sls' '~a/chez/vault/format.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/crypto.sls' '~a/chez/vault/crypto.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/blockstore.sls' '~a/chez/vault/blockstore.sls'\" jerboa-fuse-dir vault-stage))\n ;; Patch out ALL load-shared-object calls (FUSE mount helper + libcrypto + libc)\n ;; Use (if #f #f) instead of (void) since some modules only import (rnrs)\n ;; Simple single-level calls:\n (system (format \"find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object[^)]*)/(if #f #f)/g' {} +\" vault-stage))\n ;; fuse.sls and blockstore.sls have multi-line (load-shared-object (case ...)) blocks\n ;; that the simple sed can't handle. Use Scheme to patch them out.\n (let ([str-has? (lambda (haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))])\n (for-each\n (lambda (file-path)\n (when (file-exists? file-path)\n (let* ([content (let ([p (open-input-file file-path)])\n (let loop ([lines '()])\n (let ([l (get-line p)])\n (if (eof-object? l)\n (begin (close-input-port p) (reverse lines))\n (loop (cons l lines))))))]\n [patched\n (let loop ([lines content] [acc '()] [skip 0])\n (if (null? lines)\n (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (or (str-has? line \"(define _libc-loaded\")\n (str-has? line \"(define libc-loaded\")))\n (let ([name (if (str-has? line \"_libc-loaded\")\n \"_libc-loaded\" \"libc-loaded\")])\n (loop (cdr lines)\n (cons (format \" (define ~a #t)\" name) acc)\n 1))]\n [(and (> skip 0)\n (or (str-has? line \"#t))\")\n (str-has? line \"#f))\")))\n (loop (cdr lines) acc 0)]\n [(> skip 0)\n (loop (cdr lines) acc skip)]\n [else\n (loop (cdr lines) (cons line acc) 0)]))))])\n (let ([p (open-output-file file-path 'replace)])\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched)\n (close-output-port p)))))\n (list (format \"~a/chez/vault/blockstore.sls\" vault-stage)\n (format \"~a/chez/fuse.sls\" vault-stage)))) ;; close let\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up (format → crypto → secmem → mount → constants → types → codec → access → blockstore → fuse → vault)\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n ;; Format layer (no deps)\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n ;; FUSE foundation\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n ;; Secure memory + access control (depend on mount)\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n ;; Vault crypto (depends on format + libcrypto)\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n ;; Vault blockstore (depends on format + crypto + secmem)\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n ;; FUSE main (depends on all fuse sub-modules)\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n ;; Vault main (depends on everything)\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"~n[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(or (not (file-exists? so))\n (time>? (file-modification-time sls) (file-modification-time so)))\n (printf \" Compiling ~a...~n\" sls)\n (compile-library sls)]\n [else\n (printf \" (up to date) ~a~n\" sls)])))\n\n;; NOTE: WPO step uses a fresh subprocess (see step [2/7] below), so per-jsh\n;; module .so files compiled here are NOT inputs to compile-whole-program —\n;; the subprocess recompiles them. We compile here only as a fast sanity check\n;; and so that `jsh.ss` (non-WPO direct-load) keeps working in dev mode.\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n ;; Compat layer\n (compile-jsh-module \"../compat/gambit\")\n (for-each compile-jsh-module '(\"ffi\"))\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n (for-each compile-jsh-module '(\"conditions\" \"ast\" \"registry\"))\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-macos \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss + WPO via subprocess...~n\"\n )\n;; WHY a subprocess: build-jsh-macos.ss imports (jerboa build), which\n;; transitively loads (jerboa core) into THIS scheme process. Once a library\n;; is loaded, compile-program/compile-whole-program won't re-emit its .so/.wpo\n;; — but compile-whole-program needs (jerboa core).wpo on disk to fuse it in.\n;; The fix: shell out to a fresh scheme that imports only (chezscheme), so all\n;; transitive libs get compiled freshly with WPO. Pattern matches\n;; ~/mine/jerboa/support/build-boot.ss + build-jerbuild.sh.\n;;\n;; Also: nuke all .so/.wpo from step [1/7] so the subprocess recompiles every\n;; library with generate-wpo-files #t. Without this, compile-imported-libraries\n;; in the subprocess sees up-to-date .so files and skips them — leaving us\n;; without .wpo files for compile-whole-program to inline.\n(printf \" Clearing .so/.wpo from step [1/7] so subprocess recompiles with WPO...~n\")\n(for-each\n (lambda (dir)\n (when (and (string? dir) (file-directory? dir))\n (system (format \"find '~a' -type f \\\\( -name '*.so' -o -name '*.wpo' \\\\) -delete\"\n dir))))\n (list \"src\" jerboa-dir\n (if has-jerboa-ssh? ssh-stage #f)\n (if has-jerboa-fuse? vault-stage #f)\n awk-stage sed-stage\n (if enable-aws? aws-stage #f)\n coreutils-stage))\n(let* ([libdir-pair->str\n (lambda (e)\n (cond [(pair? e) (format \"~a::~a\" (car e) (cdr e))]\n [else e]))]\n [extra-libdirs\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage)))]\n [all-libdirs (append extra-libdirs (library-directories))]\n [libdirs-str\n (apply string-append\n (let loop ([lst (map libdir-pair->str all-libdirs)] [acc '()])\n (cond [(null? lst) (reverse acc)]\n [(null? acc) (loop (cdr lst) (list (car lst)))]\n [else (loop (cdr lst)\n (cons (car lst) (cons \":\" acc)))])))]\n [scheme-cmd (or (getenv \"SCHEME\")\n (format \"~a/.chez/bin/scheme\" jerboa-dir-base))]\n [build-boot-script (format \"~a/support/build-boot.ss\" jerboa-dir-base)])\n (unless (file-exists? build-boot-script)\n (fprintf (current-error-port)\n \"FATAL: build-boot.ss not found at ~a~n\" build-boot-script)\n (exit 1))\n (let* ([cmd (format \"~a -q --libdirs '~a' --script ~a jsh-generated.ss jsh-generated.wp.so\"\n scheme-cmd libdirs-str build-boot-script)]\n [rc (system cmd)])\n (unless (zero? rc)\n (fprintf (current-error-port)\n \"FATAL: WPO subprocess failed (rc=~a)~ncmd: ~a~n\" rc cmd)\n (exit 1))))\n\n;; Verify jsh-generated.wp.so was created by subprocess\n(unless (file-exists? \"jsh-generated.wp.so\")\n (fprintf (current-error-port) \"FATAL: jsh-generated.wp.so was not created~n\")\n (exit 1))\n\n;; ========== Step 3: (subsumed by step 2 subprocess) ==========\n(define program-so \"jsh-generated.wp.so\")\n\n;; Step 3.5 (precompile-boot-jerboa-modules!) + Step 4 (make-boot-file\n;; \"jsh.boot\") are subsumed by WPO above: every imported library is inlined\n;; into jsh-generated.wp.so by compile-whole-program.\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-macos-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define gcc \"cc\")\n\n(define (resolve-llvm-ar)\n (cond\n [(let ([p (getenv \"LLVM_AR\")])\n (and p (not (string=? p \"\")) p))]\n [(file-exists? \"/opt/homebrew/opt/llvm/bin/llvm-ar\")\n \"/opt/homebrew/opt/llvm/bin/llvm-ar\"]\n [else #f]))\n\n(define llvm-ar (resolve-llvm-ar))\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" home-dir \"=~\"))\n\n;; Helper: write file as C byte array directly to output port (avoids O(n^2) string-append)\n(define (write-c-array filepath varname out)\n (let* ([bv (call-with-port (open-file-input-port filepath) get-bytevector-all)]\n [len (bytevector-length bv)]\n [hex \"0123456789abcdef\"])\n (fprintf out \"static const unsigned char ~a[] = {~n\" varname)\n (do ([i 0 (+ i 1)])\n ((= i len))\n (when (and (> i 0) (= (mod i 16) 0)) (display \",\\n\" out))\n (when (and (> i 0) (not (= (mod i 16) 0))) (display \",\" out))\n (display \"0x\" out)\n (let ([b (bytevector-u8-ref bv i)])\n (display (string-ref hex (fxsrl b 4)) out)\n (display (string-ref hex (fxand b 15)) out)))\n (fprintf out \"~n};~nstatic const unsigned int ~a_len = ~a;~n\" varname len)))\n\n;; Generate static_boot.c\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n (write-c-array petite-boot-path \"petite_boot\" out) (newline out)\n (write-c-array scheme-boot-path \"scheme_boot\" out) (newline out)\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n;; FFI symbol whitelist — auto-generated from ffi-shim.c plus a small set of\n;; non-ffi_ helpers (Cage/Landlock wrappers, Rust-native shims).\n(define ffi-shim-symbols\n (append (read-symbol-list \"ffi-shim-symbols.list\")\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\")))\n\n(define native-symbols\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\"\n \"jerboa_x509_cert_fingerprint\"\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n\n(define native-int-symbols\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n\n;; High-level jsh_* coreutils commands (from Rust jerboa-coreutils).\n;; On macOS these are stubbed out — the Rust coreutils lib is not yet built.\n(define jsh-coreutils-commands\n '(\"jsh_arch\" \"jsh_b2sum\" \"jsh_base32\" \"jsh_base64\" \"jsh_basename\" \"jsh_basenc\"\n \"jsh_cat\" \"jsh_chgrp\" \"jsh_chmod\" \"jsh_chown\" \"jsh_chroot\" \"jsh_cksum\"\n \"jsh_comm\" \"jsh_cp\" \"jsh_csplit\" \"jsh_cu_realpath\" \"jsh_cut\" \"jsh_date\"\n \"jsh_dd\" \"jsh_df\" \"jsh_dir\" \"jsh_dircolors\" \"jsh_dirname\" \"jsh_du\"\n \"jsh_echo\" \"jsh_env\" \"jsh_expand\" \"jsh_expr\" \"jsh_factor\" \"jsh_fmt\"\n \"jsh_fold\" \"jsh_grep\" \"jsh_groups\" \"jsh_head\" \"jsh_hostid\" \"jsh_hostname\"\n \"jsh_id\" \"jsh_install\" \"jsh_join\" \"jsh_kill\" \"jsh_link\" \"jsh_ln\"\n \"jsh_logname\" \"jsh_ls\" \"jsh_md5sum\" \"jsh_mkdir\" \"jsh_mkfifo\" \"jsh_mknod\"\n \"jsh_mktemp\" \"jsh_mv\" \"jsh_nice\" \"jsh_nl\" \"jsh_nohup\" \"jsh_nproc\"\n \"jsh_numfmt\" \"jsh_od\" \"jsh_paste\" \"jsh_pathchk\" \"jsh_pinky\" \"jsh_pr\"\n \"jsh_printenv\" \"jsh_printf\" \"jsh_ptx\" \"jsh_pwd\" \"jsh_readlink\" \"jsh_rm\"\n \"jsh_rmdir\" \"jsh_seq\" \"jsh_sha1sum\" \"jsh_sha224sum\" \"jsh_sha256sum\"\n \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_shred\" \"jsh_shuf\" \"jsh_sleep\"\n \"jsh_sort\" \"jsh_split\" \"jsh_stat\" \"jsh_stty\" \"jsh_sum\" \"jsh_sync\"\n \"jsh_tac\" \"jsh_tail\" \"jsh_tee\" \"jsh_test\" \"jsh_timeout\" \"jsh_touch\"\n \"jsh_tr\" \"jsh_truncate\" \"jsh_tsort\" \"jsh_tty\" \"jsh_uname\" \"jsh_unexpand\"\n \"jsh_uniq\" \"jsh_unlink\" \"jsh_uptime\" \"jsh_users\" \"jsh_vdir\" \"jsh_wc\"\n \"jsh_who\" \"jsh_whoami\" \"jsh_yes\"))\n\n(define coreutils-symbols\n '(\"coreutils_chmod\" \"coreutils_lstat_mode\" \"coreutils_stat_isdir\"\n \"coreutils_chown\" \"coreutils_lchown\"\n \"coreutils_getpwnam_uid\" \"coreutils_getgrnam_gid\"\n \"coreutils_stat_call\" \"coreutils_stat_get\"\n \"coreutils_uid_to_name\" \"coreutils_gid_to_name\"\n \"coreutils_du_stat\" \"coreutils_statvfs\" \"coreutils_statvfs_get\"\n \"coreutils_test_access\" \"coreutils_test_stat\"\n \"coreutils_ls_lstat\" \"coreutils_ls_stat_get\" \"coreutils_ls_readlink\"\n \"coreutils_isatty\" \"coreutils_time_format\"\n \"coreutils_terminal_width\" \"coreutils_terminal_height\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_cp_lstat\" \"coreutils_cp_stat_get\" \"coreutils_cp_readlink\"\n \"coreutils_symlink\" \"coreutils_link\" \"coreutils_utime\"\n \"coreutils_mkdir\" \"coreutils_lstat_type\"\n \"coreutils_unlink\" \"coreutils_rmdir\" \"coreutils_access_w\"\n \"coreutils_rename\" \"coreutils_stat_get_mode\"\n \"coreutils_stat_atime\" \"coreutils_stat_mtime\"\n \"coreutils_file_size\" \"coreutils_fsync\"\n \"coreutils_chgrp_chown\" \"coreutils_chgrp_lchown\"\n \"coreutils_mkstemp\" \"coreutils_mkstemp_get_path\"\n \"coreutils_mkdtemp\" \"coreutils_readlink\" \"coreutils_realpath\"\n \"coreutils_stat_size\" \"coreutils_fsync_path\"))\n\n(define ssh-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n\n;; jerboa_ssh_crypto.c symbols (used by ssh/transport sub-library)\n(define ssh-crypto-symbols\n '(\"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\" \"jerboa_ssh_sha512\"\n \"jerboa_ssh_hmac_sha256\" \"jerboa_ssh_hmac_sha512\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_aes256_ctr_init\" \"jerboa_ssh_aes256_ctr_process\" \"jerboa_ssh_aes256_ctr_free\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_ed25519_sign\" \"jerboa_ssh_ed25519_derive_pubkey\"\n \"jerboa_ssh_tcp_connect\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_write\"\n \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_set_nodelay\"))\n\n;; OpenSSL symbols called directly as (foreign-procedure \"NAME\" ...) by vault/crypto.sls.\n;; The vault's load-shared-object patch leaves _loaded=#t (guard returns #t when no exception),\n;; so these foreign-procedure calls ARE evaluated. Since we link -lssl -lcrypto, we register\n;; the actual function pointers here so Chez can find them.\n(define openssl-ffi-symbols\n '(\"RAND_bytes\"\n \"EVP_sha256\"\n \"PKCS5_PBKDF2_HMAC\"\n \"EVP_CIPHER_CTX_new\"\n \"EVP_CIPHER_CTX_free\"\n \"EVP_aes_256_gcm\"\n \"EVP_EncryptInit_ex\"\n \"EVP_EncryptUpdate\"\n \"EVP_EncryptFinal_ex\"\n \"EVP_CIPHER_CTX_ctrl\"\n \"EVP_DecryptInit_ex\"\n \"EVP_DecryptUpdate\"\n \"EVP_DecryptFinal_ex\"))\n\n;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n(define vault-fuse-symbols\n '(;; Secure memory\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n ;; Process tree\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n ;; FUSE device + mount\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n\n;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n;; jerboa_aead_seal, jerboa_aead_open — all in libjerboa_native (ring). No libcrypto needed.\n;; POSIX symbols needed by vault code (pread/pwrite for file I/O, fsync, uid/gid)\n(define vault-crypto-symbols\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\"))\n\n;; Generate jsh_main_macos.c\n(define program-c (format \"~a/jsh_main_macos.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <mach-o/dyld.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include <dlfcn.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n\n ;; Embed program .so\n (write-c-array program-so \"jsh_program_data\" out)\n (newline out)\n\n ;; Declare static_boot_init\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n\n ;; Declare FFI symbols\n (display \"/* FFI symbols from ffi-shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n\n ;; Rust native symbols\n (when has-native-lib?\n (display \"\\n#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n native-symbols)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n native-int-symbols)\n (display \"#endif\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n\n ;; Coreutils FFI\n (display \"\\n/* FFI symbols from libcoreutils.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n coreutils-symbols)\n\n ;; High-level jsh_* coreutils commands (from Rust libjsh_coreutils.a)\n (display \"\\n/* jsh_* coreutils commands */\\n\" out)\n (if has-rust-coreutils?\n (begin\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n jsh-coreutils-commands))\n (begin\n (display \"/* Stubs — Rust coreutils not built */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"int ~a(int ac, const char **av) { return 127; }\\n\" name))\n jsh-coreutils-commands)\n (display \"void jsh_coreutils_init(int a, char **b) { }\\n\" out)))\n\n ;; jerboa-ssh\n (display \"\\n/* FFI symbols from jerboa_ssh_shim.c + jerboa_ssh_crypto.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n (append ssh-symbols ssh-crypto-symbols))\n\n ;; OpenSSL symbols — called directly as (foreign-procedure \"NAME\" ...) by vault/crypto.sls.\n ;; Linked via -lssl -lcrypto so the symbols are in the binary; we just need to register them.\n (display \"\\n/* OpenSSL symbols used by vault/crypto.sls */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n openssl-ffi-symbols)\n\n ;; jerboa-fuse vault (crypto symbols now from libjerboa_native via ring)\n (display \"/* FFI symbols for vault (from ffi-shim.c + libjerboa_native) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n vault-fuse-symbols)\n (newline out)\n\n ;; POSIX wrappers\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n\n ;; macOS errno compatibility — __errno_location doesn't exist on macOS\n (display \"/* macOS errno compatibility */\\n\" out)\n (display \"static int *macos_errno_location(void) { return &errno; }\\n\\n\" out)\n\n ;; Stubs for symbols not available on macOS\n ;; (regex extended, epoll, inotify, landlock, seccomp)\n (display \"/* Stubs for Linux-only / missing native symbols */\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n ;; Regex stubs only when libjerboa_native.a is absent — it provides real implementations\n (unless has-native-lib?\n (display \"void *jerboa_regex_compile_ex(const char *p, int f) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_find_at(void *r, const char *s, int o, int *ms, int *me) { return 0; }\\n\" out)\n (display \"char *jerboa_regex_captures(void *r, const char *s, int n) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_group_count(void *r) { return 0; }\\n\" out))\n (display \"int jerboa_epoll_create(void) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_ctl(int e, int o, int f, int ev) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_wait(int e, void *ev, int m, int t) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_close(int e) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_init(void) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_add_watch(int f, const char *p, int m) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_rm_watch(int f, int w) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_read(int f, void *b, int s) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_close(int f) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_create_ruleset(void) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_path_rule(int r, const char *p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_net_rule(int r, int p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_enforce(int r) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_available(void) { return 0; }\\n\" out)\n (display \"int jerboa_seccomp_lock(void) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_lock_strict(void) { return -1; }\\n\\n\" out)\n\n ;; htons/htonl are macros on macOS and cannot be used as function pointers directly.\n ;; Emit thin wrappers so Sforeign_symbol can register them.\n (display \"static unsigned short jsh_htons(unsigned short x) { return htons(x); }\\n\" out)\n (display \"static unsigned int jsh_htonl(unsigned int x) { return htonl(x); }\\n\\n\" out)\n\n ;; register_ffi_symbols\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Rust native\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append native-symbols native-int-symbols))\n (display \"#endif\\n\" out))\n ;; POSIX\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"readlink\" \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"\n ;; vault blockstore\n \"flock\" \"pread\" \"pwrite\" \"fsync\"\n ;; top builtin\n \"setpriority\"))\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\" \"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; macOS: __errno_location (Linux glibc) and __error (BSD) → our wrapper\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)macos_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)macos_errno_location);\\n\" out)\n ;; Register stub symbols for Linux-only / missing native functionality\n ;; Note: jerboa_regex_*_ex symbols are omitted here — when has-native-lib? they\n ;; are real symbols registered above via native-symbols; without it they are\n ;; declared as stubs in the definitions section above register_ffi_symbols.\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n ;; When native lib absent, also register the regex_ex stubs\n (unless has-native-lib?\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\")))\n ;; coreutils\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n coreutils-symbols)\n ;; jsh_* coreutils commands (stubs)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n jsh-coreutils-commands)\n (fprintf out \" Sforeign_symbol(\\\"jsh_coreutils_init\\\", (void*)jsh_coreutils_init);\\n\")\n ;; jerboa-ssh\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append ssh-symbols ssh-crypto-symbols))\n ;; OpenSSL — vault/crypto.sls calls these as foreign-procedure\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n openssl-ffi-symbols)\n ;; jerboa-fuse vault\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-fuse-symbols)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-crypto-symbols)\n ;; Sockets — htons/htonl are macros on macOS, use wrappers\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"))\n (display \" Sforeign_symbol(\\\"htons\\\", (void*)jsh_htons);\\n\" out)\n (display \" Sforeign_symbol(\\\"htonl\\\", (void*)jsh_htonl);\\n\" out)\n (display \"}\\n\\n\" out)\n\n ;; Custom main — macOS\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; macOS: _NSGetExecutablePath for exe path\n (display \" /* Resolve exe path via _NSGetExecutablePath (macOS) */\\n\" out)\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" uint32_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (_NSGetExecutablePath(exe_buf, &exe_len) == 0) {\\n\" out)\n (display \" char resolved[4096];\\n\" out)\n (display \" if (realpath(exe_buf, resolved))\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", resolved, 1);\\n\" out)\n (display \" else\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; macOS: no memfd_create — use tmpfile.\n (display \" /* macOS: extract program .so to tmpfile */\\n\" out)\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-program-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program_data, 1, jsh_program_data_len, fp) != jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"fwrite tmpfile\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C ==========\n\n(printf \"[6/7] Compiling C with cc (clang)...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-macos \"Command failed\" cmd)))\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_macos.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_macos.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; landlock-shim.c — Landlock is Linux-only; always use stub on macOS\n;; Note: ffi_landlock_abi_version and ffi_landlock_sandbox are already defined\n;; in ffi-shim.c (with macOS stubs), so we only emit the ffi_landlock_create/add/enforce\n;; and jerboa_landlock_* symbols here.\n(begin\n (printf \" Landlock is Linux-only, generating stub for macOS~n\")\n (system (format \"echo 'int ffi_landlock_create_ruleset(void) { return -1; } int ffi_landlock_add_path_rule(int a, const char *b, int c) { return -1; } int ffi_landlock_add_net_rule(int a, int b, int c) { return -1; } int ffi_landlock_enforce(int a) { return -1; } int jerboa_landlock_abi_version(void) { return -1; } int jerboa_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int jerboa_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, unsigned long long p) { return -1; }' | ~a -c -x c ~a -o '~a/landlock-shim.o' -\"\n gcc harden-cflags build-dir)))\n\n;; coreutils FFI shim\n(if (file-exists? coreutils-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir coreutils-shim))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))))\n\n;; embed-crypto.c (ChaCha20-Poly1305 AEAD for embedded file encryption)\n;; When libjerboa_native.a is present it already exports embed_pbkdf2_sha256,\n;; embed_encrypt, embed_decrypt, embed_random_bytes. Compiling embed-crypto.c\n;; would produce duplicate strong symbols that macOS ld rejects. Use an empty\n;; stub when the Rust native lib is available; compile the full C source otherwise.\n(let ([embed-crypto-src \"embed-crypto.c\"])\n (if has-native-lib?\n (begin\n (printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir)))\n (if (file-exists? embed-crypto-src)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/embed-crypto.o' '~a' -Wall\"\n gcc harden-cflags build-dir embed-crypto-src))\n (begin\n (printf \" Warning: embed-crypto.c not found~n\")\n (system (format \"echo 'int embed_pbkdf2_sha256(void){return -1;} int embed_encrypt(void){return -1;} int embed_decrypt(void){return -1;} int embed_random_bytes(void){return -1;}' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))))))\n\n;; jerboa-ssh shim\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Use standalone ed25519 backend (Rust libjerboa_native provides the symbols)\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c (SSH transport crypto + TCP — requires OpenSSL)\n (let ([crypto-src (dep-file \"jerboa-ssh\" \"jerboa_ssh_crypto.c\")])\n (if (file-exists? crypto-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jerboa-ssh-crypto.o' '~a' -Wall\"\n gcc harden-cflags openssl-include-dir build-dir crypto-src))\n (begin\n (printf \" Warning: jerboa_ssh_crypto.c not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))))))\n (begin\n (printf \" Warning: jerboa-ssh shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking ~a binary...~n\" output-name)\n\n;; When both Rust archives are present, merge them into one to deduplicate Rust\n;; runtime symbols (rust_eh_personality, std::panicking, etc.) that appear as\n;; strong (T/S) symbols in both libjerboa_native.a and libjsh_coreutils.a.\n;;\n;; Strategy:\n;; 1. Extract native objects directly into merge-dir (strong originals kept).\n;; 2. Extract coreutils objects into a subdirectory, then use llvm-objcopy to\n;; weaken any symbol that is strongly defined in BOTH archives — so the\n;; linker picks the native copy and ignores the weak coreutils duplicate.\n;; 3. Copy weakened coreutils objects with a \"cu-\" prefix (avoiding name\n;; collisions with native objects) and re-archive everything.\n;; Python script to make duplicate global symbols local (clear N_EXT) in Mach-O .o files.\n;; llvm-objcopy --weaken-symbol doesn't work on macOS Mach-O; direct byte patching does.\n(define rust-dedup-py\n (let ([py-path (format \"~a/rust_dedup.py\" build-dir)])\n (call-with-output-file py-path\n (lambda (out)\n (display\n (string-append\n \"import sys, struct\\n\"\n \"filename = sys.argv[1]\\n\"\n \"target_syms = set(sys.argv[2:])\\n\"\n \"with open(filename, 'r+b') as f: data = bytearray(f.read())\\n\"\n \"if len(data) < 32: sys.exit(0)\\n\"\n \"magic = struct.unpack_from('<I', data, 0)[0]\\n\"\n \"if magic != 0xFEEDFACF: sys.exit(0) # not 64-bit MachO\\n\"\n \"ncmds = struct.unpack_from('<I', data, 16)[0]\\n\"\n \"cmdoff, symoff, stroff, nsyms = 32, None, None, 0\\n\"\n \"for _ in range(ncmds):\\n\"\n \" cmd, sz = struct.unpack_from('<II', data, cmdoff)\\n\"\n \" if cmd == 2:\\n\"\n \" symoff, nsyms, stroff, _ = struct.unpack_from('<IIII', data, cmdoff+8)\\n\"\n \" break\\n\"\n \" cmdoff += sz\\n\"\n \"if symoff is None: sys.exit(0)\\n\"\n \"N_EXT = 0x01\\n\"\n \"for i in range(nsyms):\\n\"\n \" off = symoff + i * 16\\n\"\n \" n_strx = struct.unpack_from('<I', data, off)[0]\\n\"\n \" n_type = data[off + 4]\\n\"\n \" if n_type & N_EXT:\\n\"\n \" nm = data[stroff+n_strx : data.index(b'\\\\x00', stroff+n_strx)].decode('ascii','replace')\\n\"\n \" if nm in target_syms:\\n\"\n \" data[off + 4] = n_type & ~N_EXT\\n\"\n \"with open(filename, 'wb') as f: f.write(data)\\n\")\n out)))\n py-path))\n\n(define combined-rust-lib\n (and has-native-lib? has-rust-coreutils?\n (let* ([merge-dir (format \"~a/rust-merge\" build-dir)]\n [cu-dir (format \"~a/cu\" merge-dir)]\n [combined (format \"~a/librust_combined.a\" build-dir)])\n (unless llvm-ar\n (error 'build-jsh-macos\n \"LLVM ar is required to read Rust archives on macOS; install llvm or set LLVM_AR\"\n rust-coreutils-lib-path))\n (run-cmd (format \"rm -rf '~a' && mkdir -p '~a' '~a'\" merge-dir merge-dir cu-dir))\n ;; Extract native objects into merge-dir\n (run-cmd (format \"cd '~a' && '~a' x '~a'\" merge-dir llvm-ar native-lib-path))\n ;; Extract coreutils objects into cu-dir\n (run-cmd (format \"cd '~a' && '~a' x '~a'\" cu-dir llvm-ar rust-coreutils-lib-path))\n ;; Find symbols defined (global T/S) in BOTH archives; clear N_EXT in the\n ;; coreutils objects to make them local — linker picks native's definitions.\n ;; Write a shell script to avoid nested-quote hell with bash -c '...awk...'\n (let ([sh-path (format \"~a/dedup.sh\" build-dir)])\n (call-with-output-file sh-path\n (lambda (out)\n (display \"#!/bin/bash\\nset -e\\n\" out)\n (display (format \"NAT=$(nm '~a' 2>/dev/null | awk '/ [TS] /{print $NF}' | sort -u)\\n\"\n native-lib-path) out)\n (display (format \"CU=$(nm '~a' 2>/dev/null | awk '/ [TS] /{print $NF}' | sort -u)\\n\"\n rust-coreutils-lib-path) out)\n (display \"DUPES=$(comm -12 <(echo \\\"$NAT\\\") <(echo \\\"$CU\\\"))\\n\" out)\n (display \"[ -z \\\"$DUPES\\\" ] && exit 0\\n\" out)\n (display (format \"for f in '~a'/*.o; do python3 '~a' \\\"$f\\\" $DUPES; done\\n\"\n cu-dir rust-dedup-py) out)))\n (run-cmd (format \"bash '~a'\" sh-path)))\n ;; Copy patched coreutils objects with \"cu-\" prefix to avoid name conflicts\n (run-cmd (format \"for f in '~a'/*.o; do cp \\\"$f\\\" '~a/cu-'\\\"$(basename $f)\\\"; done\"\n cu-dir merge-dir))\n ;; Build combined archive from all objects\n (run-cmd (format \"'~a' rcs '~a' '~a'/*.o\" llvm-ar combined merge-dir))\n combined)))\n\n;; macOS does not support fully static binaries — link dynamically against system libs.\n(let* ([objs (format \"~a/jsh_main_macos.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/coreutils-ffi.o ~a/landlock-shim.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o\"\n build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir)]\n ;; Use combined archive when both Rust libs present, else fall back individually\n [native-flag (cond [combined-rust-lib (format \" ~a\" combined-rust-lib)]\n [has-native-lib? (format \" ~a\" native-lib-path)]\n [else \"\"])]\n [coreutils-flag (if combined-rust-lib \"\" ; already in combined\n (if has-rust-coreutils? (format \" ~a\" rust-coreutils-lib-path) \"\"))]\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n [cxx-libs (if has-native-lib? \" -lc++\" \"\")]\n [link-libs (format \"-L~a -L~a -L/opt/homebrew/lib -L/usr/local/lib -lssl -lcrypto -lkernel -llz4 -lz -lm -liconv -lncurses -lutil\"\n chez-ta6fb openssl-lib-dir)]\n [link-cmd (format \"~a -o ~a ~a~a~a~a ~a\"\n gcc output-name objs native-flag coreutils-flag\n cxx-libs link-libs)])\n (printf \" ~a~n\" link-cmd)\n (run-cmd link-cmd))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? output-name)\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port output-name))])\n (run-cmd (format \"strip ~a\" output-name))\n (let ([post-size (file-length (open-file-input-port output-name))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n ;; Compute SHA-256 integrity hash\n (printf \"[harden] Computing integrity hash...~n\")\n ;; macOS uses shasum -a 256; FreeBSD uses sha256 -q; Linux uses sha256sum\n (system (format \"shasum -a 256 ~a | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt 2>/dev/null || sha256sum ~a | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\"\n output-name output-name))\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port (string-append output-name \".sha256\")\n (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote ~a.sha256 (32 bytes)~n\" output-name)))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(when enable-aws? (system (format \"rm -rf '~a'\" aws-stage)))\n(system (format \"rm -rf '~a'\" awk-stage))\n(system (format \"rm -rf '~a'\" sed-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Binary created: ~a~n~n\" output-name)\n(system (format \"ls -lh ~a\" output-name))\n(printf \"~n\")\n(system (format \"file ~a\" output-name))\n(printf \"~n\")\n(system (format \"otool -L ~a 2>/dev/null || true\" output-name))\n(printf \"~nTest: ./~a -c 'echo Hello from jsh on macOS'~n\" output-name)\n"} +{"text":";; FILE: jerboa-shell/build-jsh-macos.ss\n#!chezscheme\n;;; build-jsh-macos.ss — Build jsh binary on macOS\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:... < build-jsh-macos.ss\n;;;\n;;; This script:\n;;; 1. Patches coreutils/awk/sed/ssl for static builds (no dlopen)\n;;; 2. Compiles jsh modules (using stock scheme)\n;;; 3. Creates boot file + optimized program .so\n;;; 4. Generates C files with embedded boot data\n;;; 5. Compiles C with cc (clang) against static Chez's scheme.h\n;;; 6. Links fully static binary with libkernel.a\n;;;\n;;; The resulting jsh-macos binary has zero runtime dependencies.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table)\n (jerboa build)\n (only (std os shell) shell-quote)\n (only (std security taint) safe-system))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") (format \"/Users/~a\" (getenv \"USER\"))))\n(define script-dir (or (getenv \"SCRIPT_DIR\") (current-directory)))\n(define output-name (or (getenv \"JSH_OUTPUT\") \"jsh-macos\"))\n\n;; vendor/ directory — canonical source for all dependencies.\n;; SCRIPT_DIR is exported by build-jsh-macos.sh so we know the repo root.\n(define vendor-dir (format \"~a/vendor\" script-dir))\n\n;; Resolve a dependency directory: vendor/ first, then ~/mine/<name>/,\n;; then ~/<name>/ as last resort. Callers wrap with (or (getenv \"X\") (dep ...))\n;; to allow env var overrides from the shell script.\n(define (dep name subpath)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name subpath)]\n [m (format \"~a/mine/~a/~a\" home-dir name subpath)]\n [h (format \"~a/~a/~a\" home-dir name subpath)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n\n;; Resolve a single file inside a dependency repo.\n(define (dep-file name filename)\n (let* ([v (format \"~a/~a/~a\" vendor-dir name filename)]\n [m (format \"~a/mine/~a/~a\" home-dir name filename)]\n [h (format \"~a/~a/~a\" home-dir name filename)])\n (cond\n [(file-exists? v) v]\n [(file-exists? m) m]\n [else h])))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (dep \"jerboa\" \"lib\")))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (dep \"jerboa\" \".\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (dep \"jerboa-ssh\" \"src\")))\n\n(define jerboa-ssh-shim\n (or (getenv \"JERBOA_SSH_SHIM\")\n (dep-file \"jerboa-ssh\" \"jerboa_ssh_shim.c\")))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jerboa-sqlite/src\" home-dir)))\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (dep \"jerboa-crypto\" \"src\")))\n\n(define jerboa-crypto-shim\n (or (getenv \"JERBOA_CRYPTO_SHIM\")\n (dep-file \"jerboa-crypto\" \"jerboa_crypto_shim.c\")))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (dep \"jerboa-coreutils\" \"lib\")))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (dep \"jerboa-awk\" \"lib\")))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (dep \"jerboa-sed\" \"lib\")))\n\n(define coreutils-shim\n (let ([upstream (dep-file \"jerboa-coreutils\" \"support/libcoreutils.c\")]\n [local \"patches/libcoreutils.c\"])\n (cond\n [(file-exists? upstream) upstream]\n [(file-exists? local) local]\n [else upstream])))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls).\n;; rustls is preferred over OpenSSL for security.\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (dep \"jerboa-aws\" \"lib\")))\n\n(define has-aws?\n ;; jerboa-aws lives as a subdirectory inside aws-dir (e.g. vendor/jerboa-aws/lib/jerboa-aws/)\n (file-directory? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; Feature-gated enable flags — gate on BOTH directory existence AND\n;; the feature being in *enabled-features*. This is how JSH_FEATURES\n;; actually controls whether feature dependencies land in the binary.\n(define enable-aws?\n (and has-aws? (memq 'aws *enabled-features*)))\n\n(define jerboa-fuse-dir\n (or (getenv \"JERBOA_FUSE_DIR\")\n (dep \"jerboa-fuse\" \"lib\")))\n\n;; Rust native library — resolve via vendor/ → ~/mine/ → ~/\n(define native-rs-dir\n (let* ([v (format \"~a/jerboa/jerboa-native-rs\" vendor-dir)]\n [m (format \"~a/mine/jerboa/jerboa-native-rs\" home-dir)]\n [h (format \"~a/jerboa/jerboa-native-rs\" home-dir)])\n (cond\n [(file-directory? v) v]\n [(file-directory? m) m]\n [else h])))\n(define native-lib-path\n (format \"~a/target/release/libjerboa_native.a\" native-rs-dir))\n(define native-src-dir\n (format \"~a/src\" native-rs-dir))\n;; Sentinel file written after a successful native build without SQLite.\n;; If absent, the .a was built with default (tls-only) features — must rebuild.\n(define native-features-sentinel\n (format \"~a/target/release/.built-with-tls-crypto-no-sqlite\" native-rs-dir))\n;; Only attempt Rust rebuild if cargo is available (pre-built .a may have been\n;; downloaded by build-jsh-macos.sh — don't clobber it with a failed cargo call)\n(define has-cargo?\n (= 0 (system \"command -v cargo >/dev/null 2>&1\")))\n(when (and has-cargo?\n (file-exists? native-src-dir)\n (or (not (file-exists? native-lib-path))\n ;; Features sentinel absent → stale build (wrong feature set)\n (not (file-exists? native-features-sentinel))\n ;; Check if any .rs file is newer than the .a\n (let ([lib-mtime (file-modification-time native-lib-path)])\n (let check ([files (directory-list native-src-dir)])\n (and (pair? files)\n (let ([f (format \"~a/~a\" native-src-dir (car files))])\n (or (and (> (string-length (car files)) 3)\n (string=? \".rs\" (substring (car files)\n (- (string-length (car files)) 3)\n (string-length (car files))))\n (time>? (file-modification-time f) lib-mtime))\n (check (cdr files)))))))))\n (printf \"~n[0/7] Rebuilding Rust native library (source newer than .a)...~n\")\n (let ([rc (safe-system (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote native-rs-dir)))])\n (unless (= rc 0)\n (fprintf (current-error-port) \"FATAL: cargo build --release --no-default-features --features tls,crypto failed~n\")\n (exit 1)))\n ;; Write sentinel so next build knows the right features were used\n (let ([port (open-output-file native-features-sentinel 'truncate)])\n (display \"tls,crypto,no-sqlite\\n\" port)\n (close-output-port port)))\n(when (and (file-exists? native-lib-path)\n (= 0 (safe-system (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote native-lib-path)))))\n (fprintf (current-error-port)\n \"FATAL: native SQLite symbols found in ~a; jsh must use jsqlite~n\"\n native-lib-path)\n (exit 1))\n(define has-native-lib? (file-exists? native-lib-path))\n\n;; Rust coreutils static library\n(define rust-host-triple\n (case (machine-type)\n [(tarm64osx) \"aarch64-apple-darwin\"]\n [(ta6osx) \"x86_64-apple-darwin\"]\n [else #f]))\n\n(define rust-coreutils-lib-path\n (or (getenv \"RUST_COREUTILS_LIB\")\n (if rust-host-triple\n (format \"~a/rust-coreutils/target/~a/release/libjsh_coreutils.a\"\n script-dir rust-host-triple)\n (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" script-dir))))\n(define has-rust-coreutils? (file-exists? rust-coreutils-lib-path))\n(unless has-rust-coreutils?\n (printf \" Warning: libjsh_coreutils.a not found — coreutils builtins will be stubs~n\"))\n(unless has-native-lib?\n (printf \" Warning: libjerboa_native.a not found — Rust native symbols disabled~n\"))\n\n;; Chez Scheme static installation\n;; macOS: machine type is tarm64osx (arm64) or ta6osx (x86_64)\n(define chez-machine\n (or (getenv \"CHEZ_MACHINE\")\n (machine-type)))\n\n(define chez-ta6fb\n (or (getenv \"CHEZ_TA6FB\")\n ;; Search Homebrew paths first, then /usr/local\n (let loop ([prefixes '(\"/opt/homebrew/Cellar/chezscheme\" \"/opt/homebrew/lib\" \"/usr/local/lib\")])\n (if (null? prefixes)\n (error 'build \"Cannot find Chez static directory (libkernel.a). Install: brew install chezscheme\")\n (let ([prefix (car prefixes)])\n (if (file-directory? prefix)\n (let check-dirs ([dirs (directory-list prefix)])\n (cond\n [(null? dirs) (loop (cdr prefixes))]\n [else\n (let* ([d (car dirs)]\n ;; For Cellar layout: /opt/homebrew/Cellar/chezscheme/<ver>/lib/csv<ver>/<machine>\n [cellar-path (format \"~a/~a/lib\" prefix d)]\n [direct-path (format \"~a/~a\" prefix d)])\n (cond\n ;; Cellar: check <prefix>/<ver>/lib/csv*/<machine>/libkernel.a\n [(and (file-directory? cellar-path)\n (let ([csv-dirs (filter (lambda (x) (string-prefix? \"csv\" x))\n (directory-list cellar-path))])\n (and (pair? csv-dirs)\n (let ([p (format \"~a/~a/~a\" cellar-path (car csv-dirs) chez-machine)])\n (and (file-exists? (format \"~a/libkernel.a\" p)) p)))))\n => (lambda (p) p)]\n ;; Direct: check <prefix>/csv*/<machine>/libkernel.a\n [(and (string-prefix? \"csv\" d)\n (file-directory? direct-path)\n (let ([p (format \"~a/~a\" direct-path chez-machine)])\n (and (file-exists? (format \"~a/libkernel.a\" p)) p)))\n => (lambda (p) p)]\n [else (check-dirs (cdr dirs))]))]))\n (loop (cdr prefixes))))))))\n\n(define scheme-h-dir chez-ta6fb)\n(define petite-boot-path (format \"~a/petite.boot\" chez-ta6fb))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-ta6fb))\n\n;; OpenSSL include directory — still needed for jerboa_ssh_crypto.c (SSH transport)\n;; libcrypto.a is NO LONGER linked; vault/crypto.sls uses ring via jerboa_native instead\n(define openssl-include-dir\n (let ([brew-inc \"/opt/homebrew/opt/openssl/include\"]\n [brew-inc-x86 \"/usr/local/opt/openssl/include\"])\n (cond\n [(file-directory? brew-inc) brew-inc]\n [(file-directory? brew-inc-x86) brew-inc-x86]\n [else \"/usr/include\"])))\n\n(define openssl-lib-dir\n (let ([brew-lib \"/opt/homebrew/opt/openssl/lib\"]\n [brew-lib-x86 \"/usr/local/opt/openssl/lib\"])\n (cond\n [(file-directory? brew-lib) brew-lib]\n [(file-directory? brew-lib-x86) brew-lib-x86]\n [else \"/usr/lib\"])))\n\n(printf \"Chez static: ~a~n\" chez-ta6fb)\n(printf \"Native lib: ~a~n\" (if has-native-lib? native-lib-path \"not found\"))\n(printf \"~n\")\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo so the broken vendor source is recompiled below.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n;; Compile the Jerboa runtime and stdlib entries before any staged dependency\n;; or jsh module. If these are compiled later, the boot image can embed a\n;; different compilation instance of (jerboa core) than the modules depend on.\n(define boot-jerboa-modules\n '(\"jerboa/runtime\"\n \"std/typed\" \"std/pregexp\" \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\"\n \"std/os/path\" \"std/os/path-caps\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/net/address\" \"std/misc/thread\"\n \"jerboa/core\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/regex\" \"std/match2\" \"std/sugar\"\n \"std/misc/alist\"\n \"std/stm\" \"std/foreign\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\" \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\"\n \"std/net/tcp\" \"std/net/allow-proxy\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"\n ;; (std contract condition) — imported by (jerboa core); only has a .ss\n ;; (no .sls), so compile-imported-libraries doesn't auto-write its .so\n ;; and compile-whole-program can't find its .wpo. Force-precompile here.\n \"std/contract/condition\"))\n\n(define (precompile-boot-jerboa-modules! label)\n (printf \"~a~n\" label)\n ;; Parameter settings here must match the step [2/7] compile-program block\n ;; that builds jsh-generated.so. WPO files from a different optimize-level\n ;; or unsafe-* setting are flagged as \"does not define expected compilation\n ;; instance\" by compile-whole-program and fail the build.\n (parameterize ([compile-imported-libraries #t]\n [generate-wpo-files #t]\n [optimize-level 3]\n [cp0-effort-limit 500]\n [cp0-score-limit 50]\n [cp0-outer-unroll-limit 1]\n [commonization-level 4]\n [enable-unsafe-application #t]\n [enable-unsafe-variable-reference #t]\n [enable-arithmetic-left-associative #t]\n [debug-level 0]\n [generate-inspector-information #f]\n [library-directories\n (cons (cons jerboa-dir jerboa-dir)\n (library-directories))])\n (for-each\n (lambda (m)\n ;; Source may be either .sls (R6RS) or .ss (Jerboa convention);\n ;; check both. Source absent => skip (module not vendored).\n ;; Compile failures are caught and logged so platform-specific\n ;; modules (e.g. (std os landlock) on macOS) don't abort the loop.\n (let* ([sls (format \"~a/~a.sls\" jerboa-dir m)]\n [ss (format \"~a/~a.ss\" jerboa-dir m)]\n [src (cond [(file-exists? sls) sls]\n [(file-exists? ss) ss]\n [else #f])]\n [so (format \"~a/~a.so\" jerboa-dir m)]\n [wpo (format \"~a/~a.wpo\" jerboa-dir m)])\n (when (and src\n (or (not (file-exists? so))\n (not (file-exists? wpo))))\n (printf \" Pre-compiling ~a~n\" src)\n (guard (exn [(condition? exn)\n (printf \" SKIP ~a: ~a~n\"\n m (condition-message-string exn))])\n (compile-library src)))))\n boot-jerboa-modules)))\n\n(define (condition-message-string c)\n ;; Best-effort one-line summary of a Chez condition for skip-log output.\n (cond [(and (condition? c) (message-condition? c))\n (condition-message c)]\n [else (format \"~s\" c)]))\n\n;; Skipped: WPO at step [2/7] now precompiles all transitive imports with\n;; compatible parameter settings. Pre-staging at lower optimize-level here\n;; produced .wpo files that compile-whole-program rejected as \"wrong\n;; compilation instance\".\n;; (precompile-boot-jerboa-modules!\n;; \"[0pre/7] Pre-compiling Jerboa boot dependencies...\")\n\n;; ========== Step 0: Patch coreutils for static builds ==========\n;; Coreutils modules call (load-shared-object #f) at library init time.\n;; In static builds, load-shared-object throws because dlopen is unavailable.\n;; Since FFI symbols are pre-registered via Sforeign_symbol, we patch these out.\n\n(printf \"[0/7] Patching coreutils for static build (no dlopen)...~n\")\n\n(define coreutils-stage (format \"~a/coreutils-stage\" (current-directory)))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(system (format \"mkdir -p '~a'\" coreutils-stage))\n\n(system (format \"cp -a '~a/jerboa-coreutils' '~a/'\"\n coreutils-dir coreutils-stage))\n;; macOS/FreeBSD sed uses -i '' instead of -i (no backup extension)\n(system (format \"find '~a/jerboa-coreutils' -name '*.sls' -exec sed -i '' 's/(load-shared-object #f)/(void)/g' {} +\"\n coreutils-stage))\n;; These modules import string-split explicitly from (std misc string). Newer\n;; (jerboa core) also re-exports string-split, so exclude it from core here.\n(for-each\n (lambda (name)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage name)])\n (when (file-exists? path)\n (system (format \"sed -i '' 's/(jerboa core)/(except (jerboa core) string-split)/' '~a'\"\n path)))))\n '(\"cut.sls\" \"grep.sls\" \"join.sls\"))\n(system (format \"find '~a/jerboa-coreutils' -name '*.so' -delete\"\n coreutils-stage))\n(system (format \"find '~a/jerboa-coreutils' -name '*.wpo' -delete\"\n coreutils-stage))\n\n(printf \" Recompiling patched coreutils...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-coreutils/~a\" coreutils-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"common.sls\" \"common/version.sls\" \"common/io.sls\" \"common/security.sls\"))\n (for-each\n (lambda (name)\n (let ([sls (format \"~a/jerboa-coreutils/~a.sls\" coreutils-stage name)])\n (when (file-exists? sls)\n (compile-library sls))))\n '(\"basename\" \"dirname\" \"link\" \"unlink\" \"yes\" \"printenv\"\n \"sleep\" \"whoami\" \"logname\" \"hostname\" \"nproc\" \"tty\" \"sync\" \"hostid\"\n \"cat\" \"head\" \"tail\" \"tac\" \"tee\" \"wc\" \"nl\" \"fold\" \"expand\" \"unexpand\" \"fmt\"\n \"cut\" \"paste\" \"join\" \"comm\" \"sort\" \"uniq\" \"tr\" \"numfmt\"\n \"mkdir\" \"rmdir\" \"mktemp\" \"touch\" \"readlink\" \"realpath\" \"ln\" \"cp\" \"mv\" \"rm\"\n \"install\" \"shred\"\n \"ls\" \"chmod\" \"chown\" \"chgrp\" \"stat\" \"du\" \"df\" \"pathchk\"\n \"date\" \"id\" \"groups\" \"who\" \"users\" \"pinky\" \"uptime\" \"uname\" \"arch\"\n \"seq\" \"expr\" \"basenc\" \"base64\" \"base32\" \"od\"\n \"cksum\" \"md5sum\" \"sha1sum\" \"sha224sum\" \"sha256sum\" \"sha384sum\" \"sha512sum\"\n \"b2sum\" \"sum\"\n \"env\" \"timeout\" \"nice\" \"nohup\" \"chroot\" \"stdbuf\"\n \"truncate\" \"mkfifo\" \"mknod\" \"split\" \"csplit\" \"dd\" \"dircolors\"\n \"tsort\" \"shuf\" \"factor\" \"pr\" \"ptx\" \"stty\"\n \"chcon\" \"runcon\"\n \"dir\" \"vdir\" \"rev\" \"top\")))\n\n;; grep + Rust-backed PCRE2\n(let ([grep-pcre2-patch (format \"~a/patches/grep-pcre2.sls\" (current-directory))])\n (when (file-exists? grep-pcre2-patch)\n (system (format \"mkdir -p '~a/jerboa-coreutils/grep'\" coreutils-stage))\n (system (format \"cp '~a' '~a/jerboa-coreutils/grep/pcre2.sls'\"\n grep-pcre2-patch coreutils-stage))))\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons coreutils-stage coreutils-stage)\n (library-directories))])\n (let ([pcre2-sls (format \"~a/jerboa-coreutils/grep/pcre2.sls\" coreutils-stage)])\n (when (file-exists? pcre2-sls)\n (printf \" Compiling grep/pcre2...~n\")\n (compile-library pcre2-sls)))\n (let ([grep-sls (format \"~a/jerboa-coreutils/grep.sls\" coreutils-stage)])\n (when (file-exists? grep-sls)\n (printf \" Compiling grep...~n\")\n (compile-library grep-sls))))\n\n;; ========== Step 0a: Stage jerboa-awk and jerboa-sed ==========\n(printf \"[0a/7] Staging jerboa-awk and jerboa-sed for static build...~n\")\n\n(define awk-stage (format \"~a/awk-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" awk-stage awk-stage))\n(system (format \"cp -a '~a/jerboa-awk' '~a/'\" awk-dir awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.so' -delete\" awk-stage))\n(system (format \"find '~a/jerboa-awk' -name '*.wpo' -delete\" awk-stage))\n\n(printf \" Compiling jerboa-awk...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons awk-stage awk-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-awk/~a.sls\" awk-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"ast\" \"value\" \"lexer\" \"parser\" \"runtime\"\n \"builtins/string\" \"builtins/math\" \"builtins/io\" \"main\")))\n\n;; jerboa-sed: patch pcre2 to use Rust regex\n(define sed-stage (format \"~a/sed-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" sed-stage sed-stage))\n(system (format \"cp -a '~a/sed' '~a/'\" sed-dir sed-stage))\n(system (format \"find '~a/sed' -name '*.so' -delete\" sed-stage))\n(system (format \"find '~a/sed' -name '*.wpo' -delete\" sed-stage))\n(let ([sed-pcre2-patch (format \"~a/patches/sed-pcre2.sls\" (current-directory))])\n (when (file-exists? sed-pcre2-patch)\n (system (format \"cp '~a' '~a/sed/pcre2.sls'\" sed-pcre2-patch sed-stage))))\n\n(printf \" Compiling jerboa-sed...~n\")\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons sed-stage sed-stage)\n (library-directories))])\n (for-each\n (lambda (f)\n (let ([path (format \"~a/sed/~a.sls\" sed-stage f)])\n (when (file-exists? path)\n (printf \" ~a~n\" f)\n (compile-library path))))\n '(\"pcre2\" \"ast\" \"parser\" \"engine\" \"main\")))\n\n;; ========== Step 0b: Stage jerboa-aws ==========\n;; jerboa-aws now uses (std net request) (rustls TLS) instead of\n;; jerboa-https → jerboa-ssl (OpenSSL via load-shared-object). The\n;; replacement (jerboa-aws request) library is in patches/jerboa-aws-request.sls.\n(printf \"[0b/7] Staging~a for static build...~n\"\n (if enable-aws? \" jerboa-aws\" \" (no jerboa-aws)\"))\n\n(define aws-stage (format \"~a/aws-stage\" (current-directory)))\n(when enable-aws?\n (system (format \"rm -rf '~a' && mkdir -p '~a'\" aws-stage aws-stage))\n (system (format \"cp -a '~a/jerboa-aws' '~a/'\" aws-dir aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.so' -delete\" aws-stage))\n (system (format \"find '~a/jerboa-aws' -name '*.wpo' -delete\" aws-stage))\n ;; Apply patches/jerboa-aws-crypto.sls — removes bytevector-append def (now a Chez builtin)\n (let ([patch (format \"~a/patches/jerboa-aws-crypto.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/crypto.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/crypto.so' '~a/jerboa-aws/crypto.wpo'\"\n aws-stage aws-stage))))\n ;; Apply patches/jerboa-aws-request.sls — replaces (jerboa-aws request)\n ;; with a thin re-export of (std net request) (rustls-backed). Drops the\n ;; jerboa-https/jerboa-ssl OpenSSL dependency.\n (let ([patch (format \"~a/patches/jerboa-aws-request.sls\" (current-directory))])\n (when (file-exists? patch)\n (system (format \"cp '~a' '~a/jerboa-aws/request.sls'\" patch aws-stage))\n (system (format \"rm -f '~a/jerboa-aws/request.so' '~a/jerboa-aws/request.wpo'\"\n aws-stage aws-stage)))))\n\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (library-directories))])\n (when enable-aws?\n (printf \" Compiling jerboa-aws...~n\")\n (for-each\n (lambda (f)\n (let ([path (format \"~a/jerboa-aws/~a.sls\" aws-stage f)])\n (when (file-exists? path) (compile-library path))))\n '(\"json\" \"xml\" \"uri\" \"time\" \"crypto\" \"creds\" \"sigv4\"\n \"request\" \"api\" \"json-api\"\n \"ec2/xml\" \"ec2/params\" \"ec2/api\"\n \"ec2/instances\" \"ec2/security-groups\" \"ec2/vpcs\" \"ec2/subnets\"\n \"ec2/volumes\" \"ec2/snapshots\" \"ec2/addresses\" \"ec2/key-pairs\"\n \"ec2/network-interfaces\" \"ec2/images\" \"ec2/regions\"\n \"ec2/internet-gateways\" \"ec2/nat-gateways\" \"ec2/route-tables\"\n \"ec2/launch-templates\" \"ec2/tags\"\n \"s3/xml\" \"s3/api\" \"s3/buckets\" \"s3/objects\"\n \"sts/api\" \"sts/operations\"\n \"iam/api\" \"iam/users\" \"iam/groups\" \"iam/roles\" \"iam/policies\" \"iam/access-keys\"\n \"lambda/api\" \"lambda/functions\"\n \"dynamodb/api\" \"dynamodb/operations\"\n \"logs/api\" \"logs/operations\"\n \"sns/api\" \"sns/operations\"\n \"sqs/api\" \"sqs/operations\"\n \"ssm/api\" \"ssm/operations\" \"pssm\"\n \"rds/api\" \"rds/db-instances\"\n \"elbv2/api\" \"elbv2/operations\"\n \"cfn/api\" \"cfn/stacks\"\n \"cloudwatch/api\" \"cloudwatch/operations\"\n \"compute-optimizer/api\" \"compute-optimizer/operations\"\n \"cost-optimization-hub/api\" \"cost-optimization-hub/operations\"\n \"cli/format\" \"cli/main\"))))\n\n;; ========== Step 0d: Stage jerboa-ssh for static build ==========\n(printf \"[0d/7] Staging jerboa-ssh for static build...~n\")\n\n(define ssh-stage (format \"~a/ssh-stage\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" ssh-stage ssh-stage))\n\n(define has-jerboa-ssh?\n (file-exists? (format \"~a/jerboa-ssh.sls\" jerboa-ssh-dir)))\n\n(when has-jerboa-ssh?\n ;; Copy all source files (including ssh/* sub-libraries)\n (system (format \"cp '~a/jerboa-ssh.sls' '~a/jerboa-ssh.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"mkdir -p '~a/jerboa-ssh' '~a/ssh'\" ssh-stage ssh-stage))\n (system (format \"cp '~a/jerboa-ssh/crypto.sls' '~a/jerboa-ssh/crypto.sls'\" jerboa-ssh-dir ssh-stage))\n (system (format \"cp '~a/ssh/'*.sls '~a/ssh/' 2>/dev/null\" jerboa-ssh-dir ssh-stage))\n ;; Patch out load-shared-object for static build\n (system (format \"find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object[^)]*)/(void)/g' {} +\" ssh-stage))\n ;; Delete any stale .so files\n (system (format \"find '~a' -name '*.so' -delete\" ssh-stage))\n ;; Remove bytevector-append local defs — now a Chez builtin\n (let ([strip-bva!\n (lambda (path)\n (when (file-exists? path)\n (let* ([lines (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([l (get-line p)])\n (if (eof-object? l) (reverse acc)\n (loop (cons l acc)))))))]\n [patched\n (let loop ([lines lines] [acc '()] [skip 0])\n (if (null? lines) (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (>= (string-length line) 28)\n (string=? (substring line 0 28)\n \" (define (bytevector-append\"))\n (loop (cdr lines) acc 8)]\n [(> skip 0) (loop (cdr lines) acc (- skip 1))]\n [else (loop (cdr lines) (cons line acc) 0)]))))])\n (call-with-output-file path\n (lambda (p)\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched))\n 'replace))))])\n (for-each strip-bva!\n (list (format \"~a/ssh/kex.sls\" ssh-stage)\n (format \"~a/ssh/session.sls\" ssh-stage)\n (format \"~a/ssh/auth.sls\" ssh-stage)\n (format \"~a/ssh/sftp.sls\" ssh-stage))))\n ;; Rename base64-encode/decode in known-hosts — now Chez builtins\n (let ([kh (format \"~a/ssh/known-hosts.sls\" ssh-stage)])\n (when (file-exists? kh)\n (system (format \"sed -i '' 's/base64-encode/b64-encode/g' '~a'\" kh))\n (system (format \"sed -i '' 's/base64-decode/b64-decode/g' '~a'\" kh))))\n ;; Compile\n (printf \" Compiling jerboa-ssh...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons ssh-stage ssh-stage)\n (library-directories))])\n (compile-library (format \"~a/jerboa-ssh.sls\" ssh-stage))))\n\n(unless has-jerboa-ssh?\n (printf \" jerboa-ssh not found, skipping~n\"))\n\n;; ========== Step 0e: Stage jerboa-fuse (vault) for static build ==========\n(printf \"[0e/7] Staging jerboa-fuse (vault) for static build...~n\")\n\n;; Use a separate staging directory for macOS so we don't clobber the\n;; committed musl-targeted vault-stage/ (which has _loaded #f baked in).\n;; Each build cleans + repopulates its own dir from upstream jerboa-fuse.\n(define vault-stage (format \"~a/vault-stage-macos\" (current-directory)))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" vault-stage vault-stage))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/fuse.sls\" jerboa-fuse-dir)))\n\n(when has-jerboa-fuse?\n ;; Copy the jerboa-fuse library tree (chez/fuse/ and chez/vault/)\n (system (format \"mkdir -p '~a/chez/fuse' '~a/chez/vault'\" vault-stage vault-stage))\n ;; FUSE layer\n (system (format \"cp '~a/chez/fuse.sls' '~a/chez/fuse.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/constants.sls' '~a/chez/fuse/constants.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/types.sls' '~a/chez/fuse/types.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/codec.sls' '~a/chez/fuse/codec.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/mount.sls' '~a/chez/fuse/mount.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/access.sls' '~a/chez/fuse/access.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/fuse/secmem.sls' '~a/chez/fuse/secmem.sls'\" jerboa-fuse-dir vault-stage))\n ;; Vault layer\n (system (format \"cp '~a/chez/vault.sls' '~a/chez/vault.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/format.sls' '~a/chez/vault/format.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/crypto.sls' '~a/chez/vault/crypto.sls'\" jerboa-fuse-dir vault-stage))\n (system (format \"cp '~a/chez/vault/blockstore.sls' '~a/chez/vault/blockstore.sls'\" jerboa-fuse-dir vault-stage))\n ;; Patch out ALL load-shared-object calls (FUSE mount helper + libcrypto + libc)\n ;; Use (if #f #f) instead of (void) since some modules only import (rnrs)\n ;; Simple single-level calls:\n (system (format \"find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object[^)]*)/(if #f #f)/g' {} +\" vault-stage))\n ;; fuse.sls and blockstore.sls have multi-line (load-shared-object (case ...)) blocks\n ;; that the simple sed can't handle. Use Scheme to patch them out.\n (let ([str-has? (lambda (haystack needle)\n (let ([hlen (string-length haystack)]\n [nlen (string-length needle)])\n (let loop ([i 0])\n (cond\n [(> (+ i nlen) hlen) #f]\n [(string=? (substring haystack i (+ i nlen)) needle) #t]\n [else (loop (+ i 1))]))))])\n (for-each\n (lambda (file-path)\n (when (file-exists? file-path)\n (let* ([content (let ([p (open-input-file file-path)])\n (let loop ([lines '()])\n (let ([l (get-line p)])\n (if (eof-object? l)\n (begin (close-input-port p) (reverse lines))\n (loop (cons l lines))))))]\n [patched\n (let loop ([lines content] [acc '()] [skip 0])\n (if (null? lines)\n (reverse acc)\n (let ([line (car lines)])\n (cond\n [(and (= skip 0)\n (or (str-has? line \"(define _libc-loaded\")\n (str-has? line \"(define libc-loaded\")))\n (let ([name (if (str-has? line \"_libc-loaded\")\n \"_libc-loaded\" \"libc-loaded\")])\n (loop (cdr lines)\n (cons (format \" (define ~a #t)\" name) acc)\n 1))]\n [(and (> skip 0)\n (or (str-has? line \"#t))\")\n (str-has? line \"#f))\")))\n (loop (cdr lines) acc 0)]\n [(> skip 0)\n (loop (cdr lines) acc skip)]\n [else\n (loop (cdr lines) (cons line acc) 0)]))))])\n (let ([p (open-output-file file-path 'replace)])\n (for-each (lambda (l) (put-string p l) (put-string p \"\\n\")) patched)\n (close-output-port p)))))\n (list (format \"~a/chez/vault/blockstore.sls\" vault-stage)\n (format \"~a/chez/fuse.sls\" vault-stage)))) ;; close let\n ;; Delete stale compiled files\n (system (format \"find '~a' -name '*.so' -delete\" vault-stage))\n (system (format \"find '~a' -name '*.wpo' -delete\" vault-stage))\n ;; Compile — bottom up (format → crypto → secmem → mount → constants → types → codec → access → blockstore → fuse → vault)\n (printf \" Compiling jerboa-fuse (vault)...~n\")\n (parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (cons (cons vault-stage vault-stage)\n (library-directories))])\n ;; Format layer (no deps)\n (compile-library (format \"~a/chez/vault/format.sls\" vault-stage))\n ;; FUSE foundation\n (compile-library (format \"~a/chez/fuse/constants.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/types.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/mount.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/codec.sls\" vault-stage))\n ;; Secure memory + access control (depend on mount)\n (compile-library (format \"~a/chez/fuse/secmem.sls\" vault-stage))\n (compile-library (format \"~a/chez/fuse/access.sls\" vault-stage))\n ;; Vault crypto (depends on format + libcrypto)\n (compile-library (format \"~a/chez/vault/crypto.sls\" vault-stage))\n ;; Vault blockstore (depends on format + crypto + secmem)\n (compile-library (format \"~a/chez/vault/blockstore.sls\" vault-stage))\n ;; FUSE main (depends on all fuse sub-modules)\n (compile-library (format \"~a/chez/fuse.sls\" vault-stage))\n ;; Vault main (depends on everything)\n (compile-library (format \"~a/chez/vault.sls\" vault-stage))))\n\n(unless has-jerboa-fuse?\n (printf \" jerboa-fuse not found, skipping~n\"))\n\n;; ========== Step 1: Compile jsh modules ==========\n\n(printf \"~n[1/7] Compiling jsh modules...~n\")\n\n(define (compile-jsh-module name)\n (let* ([sls (string-append \"src/jsh/\" name \".sls\")]\n [so (string-append \"src/jsh/\" name \".so\")])\n (cond\n [(not (file-exists? sls))\n (printf \" SKIP (not found): ~a~n\" sls)]\n [(or (not (file-exists? so))\n (time>? (file-modification-time sls) (file-modification-time so)))\n (printf \" Compiling ~a...~n\" sls)\n (compile-library sls)]\n [else\n (printf \" (up to date) ~a~n\" sls)])))\n\n;; NOTE: WPO step uses a fresh subprocess (see step [2/7] below), so per-jsh\n;; module .so files compiled here are NOT inputs to compile-whole-program —\n;; the subprocess recompiles them. We compile here only as a fast sanity check\n;; and so that `jsh.ss` (non-WPO direct-load) keeps working in dev mode.\n(parameterize ([optimize-level 2]\n [generate-inspector-information #f]\n [compile-imported-libraries #t]\n [library-directories\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage))\n (library-directories))])\n ;; Compat layer\n (compile-jsh-module \"../compat/gambit\")\n (for-each compile-jsh-module '(\"ffi\"))\n (for-each compile-jsh-module '(\"embed-data\" \"embed\"))\n (for-each compile-jsh-module '(\"conditions\" \"ast\" \"registry\"))\n (for-each compile-jsh-module '(\"macros\" \"util\" \"config\"))\n (for-each compile-jsh-module\n '(\"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"pregexp-compat\" \"static-compat\" \"stage\" \"recording-index\" \"recorder\" \"player\"\n \"environment\"))\n (for-each compile-jsh-module '(\"parser\" \"functions\" \"signals\" \"expander\"))\n (for-each compile-jsh-module '(\"redirect\" \"control\" \"jobs\" \"builtins\"))\n (for-each compile-jsh-module '(\"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"))\n (for-each compile-jsh-module '(\"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\" \"mux-transport\" \"mux-relay\" \"mux-server\" \"mux-client\" \"mux-router\"))\n (compile-jsh-module \"aws\")\n (compile-jsh-module \"worm\")\n (compile-jsh-module \"pass\")\n (for-each compile-jsh-module '(\"lineedit\" \"fzf\" \"script\" \"startup\" \"sandbox\" \"harden\" \"rl\" \"limits\" \"main\"))\n (compile-jsh-module \"coreutils\"))\n\n;; ========== Step 2: Compile program ==========\n\n;; Generate jsh-generated.ss from jsh.ss with the feature manifest baked in\n;; so ,features prints what was actually built. Always regenerate so the\n;; manifest tracks JSH_FEATURES even when an old jsh-generated.ss is on disk.\n(printf \" Generating jsh-generated.ss with features manifest~n\")\n(unless (file-exists? \"jsh.ss\")\n (error 'build-jsh-macos \"Program source not found\" \"jsh.ss\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"~n[2/7] Compiling jsh-generated.ss + WPO via subprocess...~n\"\n )\n;; WHY a subprocess: build-jsh-macos.ss imports (jerboa build), which\n;; transitively loads (jerboa core) into THIS scheme process. Once a library\n;; is loaded, compile-program/compile-whole-program won't re-emit its .so/.wpo\n;; — but compile-whole-program needs (jerboa core).wpo on disk to fuse it in.\n;; The fix: shell out to a fresh scheme that imports only (chezscheme), so all\n;; transitive libs get compiled freshly with WPO. Pattern matches\n;; ~/mine/jerboa/support/build-boot.ss + build-jerbuild.sh.\n;;\n;; Also: nuke all .so/.wpo from step [1/7] so the subprocess recompiles every\n;; library with generate-wpo-files #t. Without this, compile-imported-libraries\n;; in the subprocess sees up-to-date .so files and skips them — leaving us\n;; without .wpo files for compile-whole-program to inline.\n(printf \" Clearing .so/.wpo from step [1/7] so subprocess recompiles with WPO...~n\")\n(for-each\n (lambda (dir)\n (when (and (string? dir) (file-directory? dir))\n (system (format \"find '~a' -type f \\\\( -name '*.so' -o -name '*.wpo' \\\\) -delete\"\n dir))))\n (list \"src\" jerboa-dir\n (if has-jerboa-ssh? ssh-stage #f)\n (if has-jerboa-fuse? vault-stage #f)\n awk-stage sed-stage\n (if enable-aws? aws-stage #f)\n coreutils-stage))\n(let* ([libdir-pair->str\n (lambda (e)\n (cond [(pair? e) (format \"~a::~a\" (car e) (cdr e))]\n [else e]))]\n [extra-libdirs\n (append\n (if enable-aws? (list (cons aws-stage aws-stage)) '())\n (if has-jerboa-ssh? (list (cons ssh-stage ssh-stage)) '())\n (if has-jerboa-fuse? (list (cons vault-stage vault-stage)) '())\n (list (cons awk-stage awk-stage)\n (cons sed-stage sed-stage)))]\n [all-libdirs (append extra-libdirs (library-directories))]\n [libdirs-str\n (apply string-append\n (let loop ([lst (map libdir-pair->str all-libdirs)] [acc '()])\n (cond [(null? lst) (reverse acc)]\n [(null? acc) (loop (cdr lst) (list (car lst)))]\n [else (loop (cdr lst)\n (cons (car lst) (cons \":\" acc)))])))]\n [scheme-cmd (or (getenv \"SCHEME\")\n (format \"~a/.chez/bin/scheme\" jerboa-dir-base))]\n [build-boot-script (format \"~a/support/build-boot.ss\" jerboa-dir-base)])\n (unless (file-exists? build-boot-script)\n (fprintf (current-error-port)\n \"FATAL: build-boot.ss not found at ~a~n\" build-boot-script)\n (exit 1))\n (let* ([cmd (format \"~a -q --libdirs '~a' --script ~a jsh-generated.ss jsh-generated.wp.so\"\n scheme-cmd libdirs-str build-boot-script)]\n [rc (system cmd)])\n (unless (zero? rc)\n (fprintf (current-error-port)\n \"FATAL: WPO subprocess failed (rc=~a)~ncmd: ~a~n\" rc cmd)\n (exit 1))))\n\n;; Verify jsh-generated.wp.so was created by subprocess\n(unless (file-exists? \"jsh-generated.wp.so\")\n (fprintf (current-error-port) \"FATAL: jsh-generated.wp.so was not created~n\")\n (exit 1))\n\n;; ========== Step 3: (subsumed by step 2 subprocess) ==========\n(define program-so \"jsh-generated.wp.so\")\n\n;; Step 3.5 (precompile-boot-jerboa-modules!) + Step 4 (make-boot-file\n;; \"jsh.boot\") are subsumed by WPO above: every imported library is inlined\n;; into jsh-generated.wp.so by compile-whole-program.\n\n;; ========== Step 5: Generate C with embedded data ==========\n\n(printf \"[5/7] Generating C with embedded boot files + program...~n\")\n\n(define build-dir \"/tmp/jerboa-macos-jsh-build\")\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define gcc \"cc\")\n\n(define (resolve-llvm-ar)\n (cond\n [(let ([p (getenv \"LLVM_AR\")])\n (and p (not (string=? p \"\")) p))]\n [(file-exists? \"/opt/homebrew/opt/llvm/bin/llvm-ar\")\n \"/opt/homebrew/opt/llvm/bin/llvm-ar\"]\n [else #f]))\n\n(define llvm-ar (resolve-llvm-ar))\n(define harden-cflags\n (string-append \"-ffile-prefix-map=\" (current-directory) \"=.\"\n \" -ffile-prefix-map=\" home-dir \"=~\"))\n\n;; Helper: write file as C byte array directly to output port (avoids O(n^2) string-append)\n(define (write-c-array filepath varname out)\n (let* ([bv (call-with-port (open-file-input-port filepath) get-bytevector-all)]\n [len (bytevector-length bv)]\n [hex \"0123456789abcdef\"])\n (fprintf out \"static const unsigned char ~a[] = {~n\" varname)\n (do ([i 0 (+ i 1)])\n ((= i len))\n (when (and (> i 0) (= (mod i 16) 0)) (display \",\\n\" out))\n (when (and (> i 0) (not (= (mod i 16) 0))) (display \",\" out))\n (display \"0x\" out)\n (let ([b (bytevector-u8-ref bv i)])\n (display (string-ref hex (fxsrl b 4)) out)\n (display (string-ref hex (fxand b 15)) out)))\n (fprintf out \"~n};~nstatic const unsigned int ~a_len = ~a;~n\" varname len)))\n\n;; Generate static_boot.c\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n (write-c-array petite-boot-path \"petite_boot\" out) (newline out)\n (write-c-array scheme-boot-path \"scheme_boot\" out) (newline out)\n (display \"void static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", petite_boot, petite_boot_len);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", scheme_boot, scheme_boot_len);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Read one-symbol-per-line whitelist generated from ffi-shim.c.\n;; The Makefile regenerates this file from ffi-shim.c on every build so it\n;; can never drift — see tools/extract-ffi-symbols.sh.\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (port)\n (let loop ([acc '()])\n (let ([line (get-line port)])\n (if (eof-object? line)\n (reverse acc)\n (let ([trimmed (let loop ([i 0])\n (cond [(= i (string-length line)) line]\n [(char-whitespace? (string-ref line i))\n (loop (+ i 1))]\n [else (substring line i (string-length line))]))])\n (if (or (= (string-length trimmed) 0)\n (char=? (string-ref trimmed 0) #\\;)\n (char=? (string-ref trimmed 0) #\\#))\n (loop acc)\n (loop (cons trimmed acc))))))))))\n\n;; FFI symbol whitelist — auto-generated from ffi-shim.c plus a small set of\n;; non-ffi_ helpers (Cage/Landlock wrappers, Rust-native shims).\n(define ffi-shim-symbols\n (append (read-symbol-list \"ffi-shim-symbols.list\")\n '(\"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_x25519_generate_keypair\" \"jerboa_x25519_diffie_hellman\"\n \"jerboa_hkdf_sha256\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\")))\n\n(define native-symbols\n '(\"jerboa_last_error\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\" \"jerboa_md5\"\n \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_random_bytes\" \"jerboa_timing_safe_equal\"\n \"jerboa_aead_seal\" \"jerboa_aead_open\"\n \"jerboa_chacha20_seal\" \"jerboa_chacha20_open\"\n \"jerboa_scrypt\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_wipe\" \"jerboa_secure_random_fill\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_regex_compile\" \"jerboa_regex_free\" \"jerboa_regex_is_match\"\n \"jerboa_regex_find\" \"jerboa_regex_replace_all\"\n \"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\" \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\" \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_timing_check\" \"jerboa_antidebug_check_all\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_verify_hash\"\n \"jerboa_integrity_sign_verify\" \"jerboa_integrity_hash_file\"\n \"jerboa_integrity_hash_region\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\"\n \"jerboa_x509_cert_fingerprint\"\n \"jerboa_socks5_server_start\" \"jerboa_socks5_server_stop\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_stats\"))\n\n(define native-int-symbols\n '(\"jerboa_antidebug_ptrace\"\n \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_check_ld_preload\"))\n\n;; High-level jsh_* coreutils commands (from Rust jerboa-coreutils).\n;; On macOS these are stubbed out — the Rust coreutils lib is not yet built.\n(define jsh-coreutils-commands\n '(\"jsh_arch\" \"jsh_b2sum\" \"jsh_base32\" \"jsh_base64\" \"jsh_basename\" \"jsh_basenc\"\n \"jsh_cat\" \"jsh_chgrp\" \"jsh_chmod\" \"jsh_chown\" \"jsh_chroot\" \"jsh_cksum\"\n \"jsh_comm\" \"jsh_cp\" \"jsh_csplit\" \"jsh_cu_realpath\" \"jsh_cut\" \"jsh_date\"\n \"jsh_dd\" \"jsh_df\" \"jsh_dir\" \"jsh_dircolors\" \"jsh_dirname\" \"jsh_du\"\n \"jsh_echo\" \"jsh_env\" \"jsh_expand\" \"jsh_expr\" \"jsh_factor\" \"jsh_fmt\"\n \"jsh_fold\" \"jsh_grep\" \"jsh_groups\" \"jsh_head\" \"jsh_hostid\" \"jsh_hostname\"\n \"jsh_id\" \"jsh_install\" \"jsh_join\" \"jsh_kill\" \"jsh_link\" \"jsh_ln\"\n \"jsh_logname\" \"jsh_ls\" \"jsh_md5sum\" \"jsh_mkdir\" \"jsh_mkfifo\" \"jsh_mknod\"\n \"jsh_mktemp\" \"jsh_mv\" \"jsh_nice\" \"jsh_nl\" \"jsh_nohup\" \"jsh_nproc\"\n \"jsh_numfmt\" \"jsh_od\" \"jsh_paste\" \"jsh_pathchk\" \"jsh_pinky\" \"jsh_pr\"\n \"jsh_printenv\" \"jsh_printf\" \"jsh_ptx\" \"jsh_pwd\" \"jsh_readlink\" \"jsh_rm\"\n \"jsh_rmdir\" \"jsh_seq\" \"jsh_sha1sum\" \"jsh_sha224sum\" \"jsh_sha256sum\"\n \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_shred\" \"jsh_shuf\" \"jsh_sleep\"\n \"jsh_sort\" \"jsh_split\" \"jsh_stat\" \"jsh_stty\" \"jsh_sum\" \"jsh_sync\"\n \"jsh_tac\" \"jsh_tail\" \"jsh_tee\" \"jsh_test\" \"jsh_timeout\" \"jsh_touch\"\n \"jsh_tr\" \"jsh_truncate\" \"jsh_tsort\" \"jsh_tty\" \"jsh_uname\" \"jsh_unexpand\"\n \"jsh_uniq\" \"jsh_unlink\" \"jsh_uptime\" \"jsh_users\" \"jsh_vdir\" \"jsh_wc\"\n \"jsh_who\" \"jsh_whoami\" \"jsh_yes\"))\n\n(define coreutils-symbols\n '(\"coreutils_chmod\" \"coreutils_lstat_mode\" \"coreutils_stat_isdir\"\n \"coreutils_chown\" \"coreutils_lchown\"\n \"coreutils_getpwnam_uid\" \"coreutils_getgrnam_gid\"\n \"coreutils_stat_call\" \"coreutils_stat_get\"\n \"coreutils_uid_to_name\" \"coreutils_gid_to_name\"\n \"coreutils_du_stat\" \"coreutils_statvfs\" \"coreutils_statvfs_get\"\n \"coreutils_test_access\" \"coreutils_test_stat\"\n \"coreutils_ls_lstat\" \"coreutils_ls_stat_get\" \"coreutils_ls_readlink\"\n \"coreutils_isatty\" \"coreutils_time_format\"\n \"coreutils_terminal_width\" \"coreutils_terminal_height\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_cp_lstat\" \"coreutils_cp_stat_get\" \"coreutils_cp_readlink\"\n \"coreutils_symlink\" \"coreutils_link\" \"coreutils_utime\"\n \"coreutils_mkdir\" \"coreutils_lstat_type\"\n \"coreutils_unlink\" \"coreutils_rmdir\" \"coreutils_access_w\"\n \"coreutils_rename\" \"coreutils_stat_get_mode\"\n \"coreutils_stat_atime\" \"coreutils_stat_mtime\"\n \"coreutils_file_size\" \"coreutils_fsync\"\n \"coreutils_chgrp_chown\" \"coreutils_chgrp_lchown\"\n \"coreutils_mkstemp\" \"coreutils_mkstemp_get_path\"\n \"coreutils_mkdtemp\" \"coreutils_readlink\" \"coreutils_realpath\"\n \"coreutils_stat_size\" \"coreutils_fsync_path\"))\n\n(define ssh-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"))\n\n;; jerboa_ssh_crypto.c symbols (used by ssh/transport sub-library)\n(define ssh-crypto-symbols\n '(\"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\" \"jerboa_ssh_sha512\"\n \"jerboa_ssh_hmac_sha256\" \"jerboa_ssh_hmac_sha512\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_aes256_ctr_init\" \"jerboa_ssh_aes256_ctr_process\" \"jerboa_ssh_aes256_ctr_free\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_ed25519_sign\" \"jerboa_ssh_ed25519_derive_pubkey\"\n \"jerboa_ssh_tcp_connect\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_write\"\n \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_set_nodelay\"))\n\n;; OpenSSL symbols called directly as (foreign-procedure \"NAME\" ...) by vault/crypto.sls.\n;; The vault's load-shared-object patch leaves _loaded=#t (guard returns #t when no exception),\n;; so these foreign-procedure calls ARE evaluated. Since we link -lssl -lcrypto, we register\n;; the actual function pointers here so Chez can find them.\n(define openssl-ffi-symbols\n '(\"RAND_bytes\"\n \"EVP_sha256\"\n \"PKCS5_PBKDF2_HMAC\"\n \"EVP_CIPHER_CTX_new\"\n \"EVP_CIPHER_CTX_free\"\n \"EVP_aes_256_gcm\"\n \"EVP_EncryptInit_ex\"\n \"EVP_EncryptUpdate\"\n \"EVP_EncryptFinal_ex\"\n \"EVP_CIPHER_CTX_ctrl\"\n \"EVP_DecryptInit_ex\"\n \"EVP_DecryptUpdate\"\n \"EVP_DecryptFinal_ex\"))\n\n;; jerboa-fuse vault symbols (from ffi-shim.c vault section)\n(define vault-fuse-symbols\n '(;; Secure memory\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n ;; Process tree\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n ;; FUSE device + mount\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"))\n\n;; vault/crypto.sls now uses jerboa_random_bytes, jerboa_pbkdf2_derive,\n;; jerboa_aead_seal, jerboa_aead_open — all in libjerboa_native (ring). No libcrypto needed.\n;; POSIX symbols needed by vault code (pread/pwrite for file I/O, fsync, uid/gid)\n(define vault-crypto-symbols\n '(\"pread\" \"pwrite\" \"fsync\" \"getuid\" \"getgid\"))\n\n;; Generate jsh_main_macos.c\n(define program-c (format \"~a/jsh_main_macos.c\" build-dir))\n(call-with-output-file program-c\n (lambda (out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <mach-o/dyld.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <termios.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include <dlfcn.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\\n\" out)\n\n (when has-native-lib?\n (display \"#define HAS_JERBOA_NATIVE 1\\n\\n\" out))\n\n ;; Embed program .so\n (write-c-array program-so \"jsh_program_data\" out)\n (newline out)\n\n ;; Declare static_boot_init\n (display \"extern void static_boot_init(void);\\n\\n\" out)\n\n ;; Declare FFI symbols\n (display \"/* FFI symbols from ffi-shim.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n ffi-shim-symbols)\n\n ;; Rust native symbols\n (when has-native-lib?\n (display \"\\n#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n native-symbols)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(void);\\n\" name))\n native-int-symbols)\n (display \"#endif\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_server_new_mtls_pem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_mem() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_tls_connect_mtls_pem_ca() { }\\n\" out)\n (display \"__attribute__((weak)) void jerboa_x509_generate_self_signed_mem() { }\\n\" out))\n\n ;; Coreutils FFI\n (display \"\\n/* FFI symbols from libcoreutils.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n coreutils-symbols)\n\n ;; High-level jsh_* coreutils commands (from Rust libjsh_coreutils.a)\n (display \"\\n/* jsh_* coreutils commands */\\n\" out)\n (if has-rust-coreutils?\n (begin\n (display \"extern void jsh_coreutils_init(int, char**);\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern int ~a(int, const char**);\\n\" name))\n jsh-coreutils-commands))\n (begin\n (display \"/* Stubs — Rust coreutils not built */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"int ~a(int ac, const char **av) { return 127; }\\n\" name))\n jsh-coreutils-commands)\n (display \"void jsh_coreutils_init(int a, char **b) { }\\n\" out)))\n\n ;; jerboa-ssh\n (display \"\\n/* FFI symbols from jerboa_ssh_shim.c + jerboa_ssh_crypto.c */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n (append ssh-symbols ssh-crypto-symbols))\n\n ;; OpenSSL symbols — called directly as (foreign-procedure \"NAME\" ...) by vault/crypto.sls.\n ;; Linked via -lssl -lcrypto so the symbols are in the binary; we just need to register them.\n (display \"\\n/* OpenSSL symbols used by vault/crypto.sls */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n openssl-ffi-symbols)\n\n ;; jerboa-fuse vault (crypto symbols now from libjerboa_native via ring)\n (display \"/* FFI symbols for vault (from ffi-shim.c + libjerboa_native) */\\n\" out)\n (for-each\n (lambda (name) (fprintf out \"extern void ~a();\\n\" name))\n vault-fuse-symbols)\n (newline out)\n\n ;; POSIX wrappers\n (display \"/* Wrappers for variadic/macro POSIX functions */\\n\" out)\n (display \"static int wrap_open(const char *path, int flags, int mode) { return open(path, flags, mode); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *path, int mode) { return mkfifo(path, mode); }\\n\" out)\n (display \"static int wrap_umask(int mask) { return (int)umask((mode_t)mask); }\\n\" out)\n (display \"static int wrap_mkdir(const char *path, int mode) { return mkdir(path, (mode_t)mode); }\\n\\n\" out)\n\n ;; macOS errno compatibility — __errno_location doesn't exist on macOS\n (display \"/* macOS errno compatibility */\\n\" out)\n (display \"static int *macos_errno_location(void) { return &errno; }\\n\\n\" out)\n\n ;; Stubs for symbols not available on macOS\n ;; (regex extended, epoll, inotify, landlock, seccomp)\n (display \"/* Stubs for Linux-only / missing native symbols */\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n ;; Regex stubs only when libjerboa_native.a is absent — it provides real implementations\n (unless has-native-lib?\n (display \"void *jerboa_regex_compile_ex(const char *p, int f) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_find_at(void *r, const char *s, int o, int *ms, int *me) { return 0; }\\n\" out)\n (display \"char *jerboa_regex_captures(void *r, const char *s, int n) { return NULL; }\\n\" out)\n (display \"int jerboa_regex_group_count(void *r) { return 0; }\\n\" out))\n (display \"int jerboa_epoll_create(void) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_ctl(int e, int o, int f, int ev) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_wait(int e, void *ev, int m, int t) { return -1; }\\n\" out)\n (display \"int jerboa_epoll_close(int e) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_init(void) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_add_watch(int f, const char *p, int m) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_rm_watch(int f, int w) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_read(int f, void *b, int s) { return -1; }\\n\" out)\n (display \"int jerboa_inotify_close(int f) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_create_ruleset(void) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_path_rule(int r, const char *p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_add_net_rule(int r, int p, int a) { return -1; }\\n\" out)\n (display \"int jerboa_landlock_enforce(int r) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_available(void) { return 0; }\\n\" out)\n (display \"int jerboa_seccomp_lock(void) { return -1; }\\n\" out)\n (display \"int jerboa_seccomp_lock_strict(void) { return -1; }\\n\\n\" out)\n\n ;; htons/htonl are macros on macOS and cannot be used as function pointers directly.\n ;; Emit thin wrappers so Sforeign_symbol can register them.\n (display \"static unsigned short jsh_htons(unsigned short x) { return htons(x); }\\n\" out)\n (display \"static unsigned int jsh_htonl(unsigned int x) { return htonl(x); }\\n\\n\" out)\n\n ;; register_ffi_symbols\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n ffi-shim-symbols)\n ;; Rust native\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append native-symbols native-int-symbols))\n (display \"#endif\\n\" out))\n ;; POSIX\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"readlink\" \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"\n ;; vault blockstore\n \"flock\" \"pread\" \"pwrite\" \"fsync\"\n ;; top builtin\n \"setpriority\"))\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" name name))\n '(\"mkdir\" \"open\" \"fcntl\" \"mkfifo\" \"umask\"))\n ;; macOS: __errno_location (Linux glibc) and __error (BSD) → our wrapper\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)macos_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)macos_errno_location);\\n\" out)\n ;; Register stub symbols for Linux-only / missing native functionality\n ;; Note: jerboa_regex_*_ex symbols are omitted here — when has-native-lib? they\n ;; are real symbols registered above via native-symbols; without it they are\n ;; declared as stubs in the definitions section above register_ffi_symbols.\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_create_ruleset\" \"jerboa_landlock_add_path_rule\"\n \"jerboa_landlock_add_net_rule\" \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n ;; When native lib absent, also register the regex_ex stubs\n (unless has-native-lib?\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"jerboa_regex_compile_ex\" \"jerboa_regex_find_at\"\n \"jerboa_regex_captures\" \"jerboa_regex_group_count\")))\n ;; coreutils\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n coreutils-symbols)\n ;; jsh_* coreutils commands (stubs)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n jsh-coreutils-commands)\n (fprintf out \" Sforeign_symbol(\\\"jsh_coreutils_init\\\", (void*)jsh_coreutils_init);\\n\")\n ;; jerboa-ssh\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n (append ssh-symbols ssh-crypto-symbols))\n ;; OpenSSL — vault/crypto.sls calls these as foreign-procedure\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n openssl-ffi-symbols)\n ;; jerboa-fuse vault\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-fuse-symbols)\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n vault-crypto-symbols)\n ;; Sockets — htons/htonl are macros on macOS, use wrappers\n (for-each\n (lambda (name) (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" name name))\n '(\"socket\" \"bind\" \"setsockopt\" \"getsockname\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"))\n (display \" Sforeign_symbol(\\\"htons\\\", (void*)jsh_htons);\\n\" out)\n (display \" Sforeign_symbol(\\\"htonl\\\", (void*)jsh_htonl);\\n\" out)\n (display \"}\\n\\n\" out)\n\n ;; Custom main — macOS\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; macOS: _NSGetExecutablePath for exe path\n (display \" /* Resolve exe path via _NSGetExecutablePath (macOS) */\\n\" out)\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" uint32_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (_NSGetExecutablePath(exe_buf, &exe_len) == 0) {\\n\" out)\n (display \" char resolved[4096];\\n\" out)\n (display \" if (realpath(exe_buf, resolved))\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", resolved, 1);\\n\" out)\n (display \" else\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; C-level hardening\n (when has-native-lib?\n (display \"#ifdef HAS_JERBOA_NATIVE\\n\" out)\n (display \" if (!getenv(\\\"JSH_DEV\\\")) {\\n\" out)\n (display \" if (jerboa_antidebug_check_tracer() == 1) _exit(1);\\n\" out)\n (display \" if (jerboa_antidebug_check_ld_preload() == 1) _exit(1);\\n\" out)\n (display \" }\\n\" out)\n (display \"#endif\\n\\n\" out))\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; macOS: no memfd_create — use tmpfile.\n (display \" /* macOS: extract program .so to tmpfile */\\n\" out)\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-program-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program_data, 1, jsh_program_data_len, fp) != jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"fwrite tmpfile\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; ========== Step 6: Compile C ==========\n\n(printf \"[6/7] Compiling C with cc (clang)...~n\")\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-macos \"Command failed\" cmd)))\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_macos.c\n(run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jsh_main_macos.o' '~a'\"\n gcc harden-cflags scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags build-dir))\n\n;; landlock-shim.c — Landlock is Linux-only; always use stub on macOS\n;; Note: ffi_landlock_abi_version and ffi_landlock_sandbox are already defined\n;; in ffi-shim.c (with macOS stubs), so we only emit the ffi_landlock_create/add/enforce\n;; and jerboa_landlock_* symbols here.\n(begin\n (printf \" Landlock is Linux-only, generating stub for macOS~n\")\n (system (format \"echo 'int ffi_landlock_create_ruleset(void) { return -1; } int ffi_landlock_add_path_rule(int a, const char *b, int c) { return -1; } int ffi_landlock_add_net_rule(int a, int b, int c) { return -1; } int ffi_landlock_enforce(int a) { return -1; } int jerboa_landlock_abi_version(void) { return -1; } int jerboa_landlock_sandbox(const char *r, const char *w, const char *e) { return -1; } int jerboa_landlock_sandbox_ex(const char *r, const char *w, const char *e, int fs, int nm, unsigned long long p) { return -1; }' | ~a -c -x c ~a -o '~a/landlock-shim.o' -\"\n gcc harden-cflags build-dir)))\n\n;; coreutils FFI shim\n(if (file-exists? coreutils-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir coreutils-shim))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir))))\n\n;; embed-crypto.c (ChaCha20-Poly1305 AEAD for embedded file encryption)\n;; When libjerboa_native.a is present it already exports embed_pbkdf2_sha256,\n;; embed_encrypt, embed_decrypt, embed_random_bytes. Compiling embed-crypto.c\n;; would produce duplicate strong symbols that macOS ld rejects. Use an empty\n;; stub when the Rust native lib is available; compile the full C source otherwise.\n(let ([embed-crypto-src \"embed-crypto.c\"])\n (if has-native-lib?\n (begin\n (printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir)))\n (if (file-exists? embed-crypto-src)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/embed-crypto.o' '~a' -Wall\"\n gcc harden-cflags build-dir embed-crypto-src))\n (begin\n (printf \" Warning: embed-crypto.c not found~n\")\n (system (format \"echo 'int embed_pbkdf2_sha256(void){return -1;} int embed_encrypt(void){return -1;} int embed_decrypt(void){return -1;} int embed_random_bytes(void){return -1;}' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))))))\n\n;; jerboa-ssh shim\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; Use standalone ed25519 backend (Rust libjerboa_native provides the symbols)\n (run-cmd (format \"~a -c -O2 ~a -DCHEZ_SSH_NO_OPENSSL -I'~a' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n ;; Generate empty .o since the symbols come from the Rust static lib\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n ;; bcrypt_pbkdf\n (let ([bcrypt-src (dep-file \"jerboa-ssh\" \"bcrypt_pbkdf.c\")])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags jerboa-ssh-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n ;; jerboa_ssh_crypto.c (SSH transport crypto + TCP — requires OpenSSL)\n (let ([crypto-src (dep-file \"jerboa-ssh\" \"jerboa_ssh_crypto.c\")])\n (if (file-exists? crypto-src)\n (run-cmd (format \"~a -c -O2 ~a -I'~a' -o '~a/jerboa-ssh-crypto.o' '~a' -Wall\"\n gcc harden-cflags openssl-include-dir build-dir crypto-src))\n (begin\n (printf \" Warning: jerboa_ssh_crypto.c not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))))))\n (begin\n (printf \" Warning: jerboa-ssh shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; ========== Step 7: Link static binary ==========\n\n(printf \"[7/7] Linking ~a binary...~n\" output-name)\n\n;; When both Rust archives are present, merge them into one to deduplicate Rust\n;; runtime symbols (rust_eh_personality, std::panicking, etc.) that appear as\n;; strong (T/S) symbols in both libjerboa_native.a and libjsh_coreutils.a.\n;;\n;; Strategy:\n;; 1. Extract native objects directly into merge-dir (strong originals kept).\n;; 2. Extract coreutils objects into a subdirectory, then use llvm-objcopy to\n;; weaken any symbol that is strongly defined in BOTH archives — so the\n;; linker picks the native copy and ignores the weak coreutils duplicate.\n;; 3. Copy weakened coreutils objects with a \"cu-\" prefix (avoiding name\n;; collisions with native objects) and re-archive everything.\n;; Python script to make duplicate global symbols local (clear N_EXT) in Mach-O .o files.\n;; llvm-objcopy --weaken-symbol doesn't work on macOS Mach-O; direct byte patching does.\n(define rust-dedup-py\n (let ([py-path (format \"~a/rust_dedup.py\" build-dir)])\n (call-with-output-file py-path\n (lambda (out)\n (display\n (string-append\n \"import sys, struct\\n\"\n \"filename = sys.argv[1]\\n\"\n \"target_syms = set(sys.argv[2:])\\n\"\n \"with open(filename, 'r+b') as f: data = bytearray(f.read())\\n\"\n \"if len(data) < 32: sys.exit(0)\\n\"\n \"magic = struct.unpack_from('<I', data, 0)[0]\\n\"\n \"if magic != 0xFEEDFACF: sys.exit(0) # not 64-bit MachO\\n\"\n \"ncmds = struct.unpack_from('<I', data, 16)[0]\\n\"\n \"cmdoff, symoff, stroff, nsyms = 32, None, None, 0\\n\"\n \"for _ in range(ncmds):\\n\"\n \" cmd, sz = struct.unpack_from('<II', data, cmdoff)\\n\"\n \" if cmd == 2:\\n\"\n \" symoff, nsyms, stroff, _ = struct.unpack_from('<IIII', data, cmdoff+8)\\n\"\n \" break\\n\"\n \" cmdoff += sz\\n\"\n \"if symoff is None: sys.exit(0)\\n\"\n \"N_EXT = 0x01\\n\"\n \"for i in range(nsyms):\\n\"\n \" off = symoff + i * 16\\n\"\n \" n_strx = struct.unpack_from('<I', data, off)[0]\\n\"\n \" n_type = data[off + 4]\\n\"\n \" if n_type & N_EXT:\\n\"\n \" nm = data[stroff+n_strx : data.index(b'\\\\x00', stroff+n_strx)].decode('ascii','replace')\\n\"\n \" if nm in target_syms:\\n\"\n \" data[off + 4] = n_type & ~N_EXT\\n\"\n \"with open(filename, 'wb') as f: f.write(data)\\n\")\n out)))\n py-path))\n\n(define combined-rust-lib\n (and has-native-lib? has-rust-coreutils?\n (let* ([merge-dir (format \"~a/rust-merge\" build-dir)]\n [cu-dir (format \"~a/cu\" merge-dir)]\n [combined (format \"~a/librust_combined.a\" build-dir)])\n (unless llvm-ar\n (error 'build-jsh-macos\n \"LLVM ar is required to read Rust archives on macOS; install llvm or set LLVM_AR\"\n rust-coreutils-lib-path))\n (run-cmd (format \"rm -rf '~a' && mkdir -p '~a' '~a'\" merge-dir merge-dir cu-dir))\n ;; Extract native objects into merge-dir\n (run-cmd (format \"cd '~a' && '~a' x '~a'\" merge-dir llvm-ar native-lib-path))\n ;; Extract coreutils objects into cu-dir\n (run-cmd (format \"cd '~a' && '~a' x '~a'\" cu-dir llvm-ar rust-coreutils-lib-path))\n ;; Find symbols defined (global T/S) in BOTH archives; clear N_EXT in the\n ;; coreutils objects to make them local — linker picks native's definitions.\n ;; Write a shell script to avoid nested-quote hell with bash -c '...awk...'\n (let ([sh-path (format \"~a/dedup.sh\" build-dir)])\n (call-with-output-file sh-path\n (lambda (out)\n (display \"#!/bin/bash\\nset -e\\n\" out)\n (display (format \"NAT=$(nm '~a' 2>/dev/null | awk '/ [TS] /{print $NF}' | sort -u)\\n\"\n native-lib-path) out)\n (display (format \"CU=$(nm '~a' 2>/dev/null | awk '/ [TS] /{print $NF}' | sort -u)\\n\"\n rust-coreutils-lib-path) out)\n (display \"DUPES=$(comm -12 <(echo \\\"$NAT\\\") <(echo \\\"$CU\\\"))\\n\" out)\n (display \"[ -z \\\"$DUPES\\\" ] && exit 0\\n\" out)\n (display (format \"for f in '~a'/*.o; do python3 '~a' \\\"$f\\\" $DUPES; done\\n\"\n cu-dir rust-dedup-py) out)))\n (run-cmd (format \"bash '~a'\" sh-path)))\n ;; Copy patched coreutils objects with \"cu-\" prefix to avoid name conflicts\n (run-cmd (format \"for f in '~a'/*.o; do cp \\\"$f\\\" '~a/cu-'\\\"$(basename $f)\\\"; done\"\n cu-dir merge-dir))\n ;; Build combined archive from all objects\n (run-cmd (format \"'~a' rcs '~a' '~a'/*.o\" llvm-ar combined merge-dir))\n combined)))\n\n;; macOS does not support fully static binaries — link dynamically against system libs.\n(let* ([objs (format \"~a/jsh_main_macos.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/coreutils-ffi.o ~a/landlock-shim.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o\"\n build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir build-dir)]\n ;; Use combined archive when both Rust libs present, else fall back individually\n [native-flag (cond [combined-rust-lib (format \" ~a\" combined-rust-lib)]\n [has-native-lib? (format \" ~a\" native-lib-path)]\n [else \"\"])]\n [coreutils-flag (if combined-rust-lib \"\" ; already in combined\n (if has-rust-coreutils? (format \" ~a\" rust-coreutils-lib-path) \"\"))]\n ;; libcrypto.a removed — vault/crypto.sls now uses ring via jerboa_native\n [cxx-libs (if has-native-lib? \" -lc++\" \"\")]\n [link-libs (format \"-L~a -L~a -L/opt/homebrew/lib -L/usr/local/lib -lssl -lcrypto -lkernel -llz4 -lz -lm -liconv -lncurses -lutil\"\n chez-ta6fb openssl-lib-dir)]\n [link-cmd (format \"~a -o ~a ~a~a~a~a ~a\"\n gcc output-name objs native-flag coreutils-flag\n cxx-libs link-libs)])\n (printf \" ~a~n\" link-cmd)\n (run-cmd link-cmd))\n\n;; ========== Hardening: strip symbols + compute integrity hash ==========\n\n(when (file-exists? output-name)\n (printf \"~n[harden] Stripping symbols...~n\")\n (let ([pre-size (file-length (open-file-input-port output-name))])\n (run-cmd (format \"strip ~a\" output-name))\n (let ([post-size (file-length (open-file-input-port output-name))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n ;; Compute SHA-256 integrity hash\n (printf \"[harden] Computing integrity hash...~n\")\n ;; macOS uses shasum -a 256; FreeBSD uses sha256 -q; Linux uses sha256sum\n (system (format \"shasum -a 256 ~a | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt 2>/dev/null || sha256sum ~a | cut -d' ' -f1 | tr -d '\\\\n' > /tmp/_jsh_hash.txt\"\n output-name output-name))\n (let ([hash-hex (call-with-input-file \"/tmp/_jsh_hash.txt\" get-string-all)])\n (system \"rm -f /tmp/_jsh_hash.txt\")\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port (string-append output-name \".sha256\")\n (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote ~a.sha256 (32 bytes)~n\" output-name)))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(system (format \"rm -rf '~a'\" coreutils-stage))\n(when enable-aws? (system (format \"rm -rf '~a'\" aws-stage)))\n(system (format \"rm -rf '~a'\" awk-stage))\n(system (format \"rm -rf '~a'\" sed-stage))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Binary created: ~a~n~n\" output-name)\n(system (format \"ls -lh ~a\" output-name))\n(printf \"~n\")\n(system (format \"file ~a\" output-name))\n(printf \"~n\")\n(system (format \"otool -L ~a 2>/dev/null || true\" output-name))\n(printf \"~nTest: ./~a -c 'echo Hello from jsh on macOS'~n\" output-name)\n"} {"text":";; FILE: jerboa-shell/signals.ss\n;;; signals.ss — Signal handling and traps for gsh\n\n(export #t)\n(import :std/sugar\n :std/format\n :std/sort\n (only (compat gambit) let/cc)\n :std/os/signal\n :jsh/ffi\n :jsh/util)\n\n;;; --- Trap table ---\n;; Maps signal names to actions:\n;; string -> command to execute\n;; 'ignore -> ignore the signal\n;; 'default -> restore default behavior\n\n(defstruct trap-entry (signal action) transparent: #t)\n\n;; Global trap table (managed by the shell environment)\n(def *trap-table* (make-hash-table))\n\n;; Well-known signal name -> number mapping\n(def *signal-names*\n (hash\n (\"HUP\" SIGHUP)\n (\"INT\" SIGINT)\n (\"QUIT\" SIGQUIT)\n (\"ILL\" SIGILL)\n (\"TRAP\" SIGTRAP)\n (\"ABRT\" SIGABRT)\n (\"FPE\" SIGFPE)\n (\"KILL\" SIGKILL)\n (\"SEGV\" SIGSEGV)\n (\"PIPE\" SIGPIPE)\n (\"ALRM\" SIGALRM)\n (\"TERM\" SIGTERM)\n (\"USR1\" SIGUSR1)\n (\"USR2\" SIGUSR2)\n (\"CHLD\" SIGCHLD)\n (\"CONT\" SIGCONT)\n (\"STOP\" SIGSTOP)\n (\"TSTP\" SIGTSTP)\n (\"TTIN\" SIGTTIN)\n (\"TTOU\" SIGTTOU)\n (\"WINCH\" SIGWINCH)\n (\"URG\" SIGURG)\n (\"IO\" SIGIO)\n (\"XCPU\" SIGXCPU)\n (\"XFSZ\" SIGXFSZ)\n (\"VTALRM\" SIGVTALRM)\n (\"PROF\" SIGPROF)\n (\"SYS\" SIGSYS)))\n\n;; Pseudo-signals (not real OS signals)\n(def *pseudo-signals* '(\"EXIT\" \"DEBUG\" \"RETURN\" \"ERR\"))\n\n;; Reverse mapping: signal number -> short name\n(def *signal-number-to-name* (make-hash-table))\n(hash-for-each (lambda (name num) (hash-put! *signal-number-to-name* num name)) *signal-names*)\n\n;; Normalize a signal argument to canonical short name (e.g. \"INT\", \"EXIT\")\n;; Handles: SIGINT -> INT, INT -> INT, 2 -> INT, 0 -> EXIT, etc.\n(def (normalize-signal-arg arg)\n (let ((uarg (string-upcase arg)))\n ;; Strip SIG prefix\n (let ((stripped (if (and (> (string-length uarg) 3)\n (string=? (substring uarg 0 3) \"SIG\"))\n (substring uarg 3 (string-length uarg))\n uarg)))\n ;; Check if it's a number\n (let ((num (string->number stripped)))\n (cond\n ;; Signal number: 0 = EXIT, others look up\n ((and num (= num 0)) \"EXIT\")\n ((and num (hash-get *signal-number-to-name* num))\n => (lambda (name) name))\n ;; Valid signal number but no name in table — return as-is\n ((and num (integer? num) (> num 0) (<= num 64))\n (number->string num))\n ;; Known signal name\n ((hash-get *signal-names* stripped) stripped)\n ;; Pseudo-signal\n ((member stripped *pseudo-signals*) stripped)\n ;; Unknown\n (else #f))))))\n\n;; Get canonical display name for trap -p output\n;; Pseudo signals: EXIT, DEBUG, RETURN, ERR (no SIG prefix)\n;; Real signals: SIGHUP, SIGINT, SIGTERM, etc.\n(def (signal-display-name short-name)\n (if (member short-name *pseudo-signals*)\n short-name\n (string-append \"SIG\" short-name)))\n\n;; Convert signal name to number (or #f for pseudo/unknown)\n(def (signal-name->number name)\n (let ((uname (string-upcase name)))\n ;; Strip SIG prefix if present\n (let ((stripped (if (and (> (string-length uname) 3)\n (string=? (substring uname 0 3) \"SIG\"))\n (substring uname 3 (string-length uname))\n uname)))\n (hash-get *signal-names* stripped))))\n\n;; Human-readable signal descriptions (matching strsignal/bash output)\n(def *signal-descriptions*\n (hash\n (\"HUP\" \"Hangup\") (\"INT\" \"Interrupt\") (\"QUIT\" \"Quit\")\n (\"ILL\" \"Illegal instruction\") (\"TRAP\" \"Trace/breakpoint trap\")\n (\"ABRT\" \"Aborted\") (\"FPE\" \"Floating point exception\")\n (\"KILL\" \"Killed\") (\"SEGV\" \"Segmentation fault\")\n (\"PIPE\" \"Broken pipe\") (\"ALRM\" \"Alarm clock\") (\"TERM\" \"Terminated\")\n (\"USR1\" \"User defined signal 1\") (\"USR2\" \"User defined signal 2\")\n (\"CHLD\" \"Child exited\") (\"CONT\" \"Continued\") (\"STOP\" \"Stopped (signal)\")\n (\"TSTP\" \"Stopped\") (\"TTIN\" \"Stopped (tty input)\")\n (\"TTOU\" \"Stopped (tty output)\")))\n\n;; Get human-readable description for a signal number\n(def (signal-description signum)\n (let ((name (signal-number->name signum)))\n (and name (hash-get *signal-descriptions* name))))\n\n;;; --- Trap operations ---\n\n;; Set a trap for a signal\n;; signal-name should be a normalized short name (e.g. \"INT\", \"EXIT\")\n;; action: string (command), \"\" or 'ignore (ignore), 'default or #f (reset)\n(def (trap-set! signal-name action)\n (let ((uname (or (normalize-signal-arg signal-name)\n (string-upcase signal-name))))\n (cond\n ;; Reset to default\n ((or (eq? action 'default) (not action) (string=? (if (string? action) action \"\") \"-\"))\n (hash-remove! *trap-table* uname)\n (hash-remove! *flag-trapped-signals* uname)\n (let ((signum (signal-name->number uname)))\n (when (and signum (not (hash-get *initially-ignored-signals* signum)))\n (ffi-signal-set-default signum)\n (with-catch (lambda (e) #!void) ;; ignore error if no handler installed\n (lambda () (remove-signal-handler! signum))))))\n ;; Ignore signal\n ((or (eq? action 'ignore) (and (string? action) (string=? action \"\")))\n (hash-put! *trap-table* uname 'ignore)\n (hash-remove! *flag-trapped-signals* uname)\n (let ((signum (signal-name->number uname)))\n (when (and signum (not (hash-get *initially-ignored-signals* signum)))\n (ffi-signal-set-ignore signum))))\n ;; Set command handler\n ((string? action)\n (hash-put! *trap-table* uname action)\n ;; For real signals, install a C-level signal flag handler.\n ;; This is synchronous (flag set immediately on signal delivery),\n ;; unlike Jerboa's async signalfd-based add-signal-handler! which\n ;; has timing issues with signal delivery.\n ;; POSIX: signals that were SIG_IGN at startup cannot be trapped\n (let ((signum (signal-name->number uname)))\n (when (and signum (not (hash-get *initially-ignored-signals* signum)))\n ;; Remove any existing Jerboa handler first\n (with-catch (lambda (e) #!void)\n (lambda () (remove-signal-handler! signum)))\n ;; Install C-level flag handler (also unblocks the signal)\n (ffi-signal-flag-install signum)\n ;; Track which signals use flag-based handling\n (hash-put! *flag-trapped-signals* uname signum))))\n (else\n (error #f (format \"trap: invalid action: ~a\" action))))))\n\n;; Get the trap action for a signal\n(def (trap-get signal-name)\n (let ((uname (or (normalize-signal-arg signal-name)\n (string-upcase signal-name))))\n (hash-get *trap-table* uname)))\n\n;; List all traps as alist of (signal-name . action), sorted.\n;; Bash sorts EXIT/ERR/DEBUG/RETURN first, then by signal number.\n(def (trap-list)\n (sort (hash->list *trap-table*)\n (lambda (a b)\n (let ((na (signal-name->number (car a)))\n (nb (signal-name->number (car b))))\n (cond\n ;; Pseudo-signals (EXIT, ERR, etc.) have no number — sort first\n ((and (not na) nb) #t)\n ((and na (not nb)) #f)\n ((and (not na) (not nb)) (string<? (car a) (car b)))\n (else (< na nb)))))))\n\n;; Check if any signal command traps are registered (not 'ignore, not EXIT/ERR/DEBUG)\n(def (has-signal-traps?)\n (let/cc return\n (hash-for-each\n (lambda (name action)\n (when (and (string? action) (not (string=? action \"\"))\n ;; Only real signals, not pseudo-signals\n (signal-name->number name))\n (return #t)))\n *trap-table*)\n #f))\n\n;;; --- Pending signal queue ---\n\n(def *pending-signals* [])\n\n;; Signals using C-level flag handlers (maps signal-name -> signum)\n(def *flag-trapped-signals* (make-hash-table))\n\n;; Check and clear pending signals, return list of signal names.\n;; Checks both the Jerboa signalfd-based queue and C-level signal flags.\n(def (pending-signals!)\n ;; First, check C-level signal flags (synchronous, no timing issues)\n (hash-for-each\n (lambda (name signum)\n (when (= 1 (ffi-signal-flag-check signum))\n (set! *pending-signals* (cons name *pending-signals*))))\n *flag-trapped-signals*)\n ;; Return combined pending list\n (let ((pending *pending-signals*))\n (set! *pending-signals* [])\n (reverse pending)))\n\n;; Remove a specific signal from the pending queue.\n;; Used when a foreground child was killed by a signal — the shell should\n;; NOT run the trap for that signal (bash behavior).\n(def (clear-pending-signal! sig-name)\n (set! *pending-signals*\n (filter (lambda (s) (not (string=? s sig-name))) *pending-signals*)))\n\n;; Map a signal number to its short name (e.g. 2 -> \"INT\")\n(def (signal-number->name num)\n (hash-get *signal-number-to-name* num))\n\n;;; --- Initially-ignored signals (POSIX) ---\n;; Signals that were SIG_IGN when the shell started.\n;; Non-interactive shells must not override these (POSIX requirement).\n;; Populated by setup-noninteractive-signal-handlers!\n(def *initially-ignored-signals* (make-hash-table))\n\n;;; --- Default signal setup for interactive shell ---\n\n(def (setup-default-signal-handlers!)\n ;; SIGINT: interrupt current command\n (add-signal-handler! SIGINT\n (lambda ()\n (set! *pending-signals* (cons \"INT\" *pending-signals*))))\n ;; SIGQUIT: ignore in interactive mode\n (ffi-signal-set-ignore SIGQUIT)\n ;; SIGTERM: flag for exit\n (add-signal-handler! SIGTERM\n (lambda ()\n (set! *pending-signals* (cons \"TERM\" *pending-signals*))))\n ;; SIGTSTP: ignore for the shell itself (children get it)\n (ffi-signal-set-ignore SIGTSTP)\n (ffi-signal-set-ignore SIGTTIN)\n (ffi-signal-set-ignore SIGTTOU)\n ;; SIGPIPE: ignore (let write fail with error)\n (ffi-signal-set-ignore SIGPIPE)\n ;; SIGXFSZ: install flag handler so write fails instead of killing process,\n ;; and the signal is recorded for script termination (exit 153 = 128+25)\n (ffi-signal-flag-install SIGXFSZ)\n (hash-put! *flag-trapped-signals* \"XFSZ\" SIGXFSZ)\n ;; SIGWINCH: record for terminal resize\n (add-signal-handler! SIGWINCH\n (lambda ()\n (set! *pending-signals* (cons \"WINCH\" *pending-signals*))))\n ;; SIGCHLD: record for job status updates\n (add-signal-handler! SIGCHLD\n (lambda ()\n (set! *pending-signals* (cons \"CHLD\" *pending-signals*)))))\n\n;;; --- Signal setup for non-interactive shell (scripts, -c) ---\n\n(def (setup-noninteractive-signal-handlers!)\n ;; Record which signals were SIG_IGN at startup (before Gambit).\n ;; POSIX: non-interactive shells must not override inherited SIG_IGN.\n (for-each\n (lambda (signum)\n (when (= (ffi-signal-was-ignored signum) 1)\n (hash-put! *initially-ignored-signals* signum #t)\n ;; Restore SIG_IGN that Gambit's startup overrode\n (ffi-signal-set-ignore signum)))\n (list SIGINT SIGQUIT SIGTERM SIGHUP))\n ;; SIGINT: record for processing between commands\n ;; Without this, Gambit's default handler terminates the process\n ;; and EXIT traps never fire.\n (unless (hash-get *initially-ignored-signals* SIGINT)\n (add-signal-handler! SIGINT\n (lambda ()\n (set! *pending-signals* (cons \"INT\" *pending-signals*)))))\n ;; SIGTERM: record for processing\n (unless (hash-get *initially-ignored-signals* SIGTERM)\n (add-signal-handler! SIGTERM\n (lambda ()\n (set! *pending-signals* (cons \"TERM\" *pending-signals*)))))\n ;; SIGPIPE: ignore (always, regardless of initial state)\n (add-signal-handler! SIGPIPE (lambda () #!void))\n ;; SIGXFSZ: install flag handler for proper handling (exit 153)\n (ffi-signal-flag-install SIGXFSZ)\n (hash-put! *flag-trapped-signals* \"XFSZ\" SIGXFSZ))\n\n;;; --- Signal context for command execution ---\n\n;; Run a thunk with appropriate signal handling for foreground command execution\n(def (with-signal-context thunk)\n ;; Clear pending signals before running\n (set! *pending-signals* [])\n (thunk))\n\n;;; --- Utility ---\n\n;; List all known signal names\n(def (signal-name-list)\n (sort! (hash-keys *signal-names*) string<?))\n"} {"text":";; FILE: jerboa-shell/build-all.ss\n#!chezscheme\n;; Build driver: imports all modules to trigger Chez compilation.\n;; Generates .so + .wpo files for the platform-specific binary builds\n;; (build-jsh-{macos,musl,freebsd,android}.ss).\n(parameterize ([compile-imported-libraries #t]\n [generate-wpo-files #t]\n [optimize-level 3]\n [generate-inspector-information #f])\n (eval '(import\n (jsh ast) (jsh registry) (jsh macros) (jsh util)\n (jsh environment) (jsh lexer) (jsh arithmetic) (jsh glob)\n (jsh fuzzy) (jsh history) (jsh parser) (jsh functions)\n (jsh signals) (jsh expander) (jsh redirect) (jsh control)\n (jsh jobs) (jsh builtins) (jsh pipeline) (jsh executor)\n (jsh completion) (jsh prompt) (jsh lineedit) (jsh fzf)\n (jsh script) (jsh startup) (jsh main) (jsh stage)\n (jsh worm))\n (interaction-environment)))\n"} {"text":";; FILE: jerboa-shell/arithmetic.ss\n;;; arithmetic.ss — Shell arithmetic evaluation for gsh\n;;; Implements $(( )) arithmetic with full operator precedence\n\n(export arith-eval arith-tokenize arith-token? arith-token-type arith-token-value\n arith-state? arith-state-tokens arith-state-pos arith-state-env-get arith-state-env-set\n arith-state-suppress-effects arith-state-nounset?)\n(import :std/sugar\n :std/format)\n\n;;; --- Public interface ---\n\n;; Evaluate a shell arithmetic expression string\n;; env-get-fn: (lambda (name) value-string-or-#f)\n;; env-set-fn: (lambda (name value) void)\n;; nounset?: when #t, referencing undefined variables is an error\n;; Returns: integer result\n(def (arith-eval expr env-get-fn env-set-fn (nounset? #f))\n (let* ((tokens (arith-tokenize expr))\n (state (make-arith-state tokens 0 env-get-fn env-set-fn #f nounset?)))\n (if (null? tokens)\n 0\n (let ((result (parse-comma-expr state)))\n ;; Validate all tokens were consumed\n (when (< (arith-state-pos state) (length tokens))\n (error #f (format \"arithmetic: syntax error: unexpected token '~a'\"\n (arith-token-value (list-ref tokens (arith-state-pos state))))))\n result))))\n\n;;; --- Tokenizer ---\n\n;; Added suppress-effects for short-circuit evaluation, nounset? for set -u\n(defstruct arith-state (tokens pos env-get env-set suppress-effects nounset?) transparent: #t)\n\n;; Arith token types: 'number 'name 'op\n(defstruct arith-token (type value) transparent: #t)\n\n(def (arith-tokenize expr)\n (let loop ((i 0) (tokens []))\n (cond\n ((>= i (string-length expr))\n (reverse tokens))\n ;; Skip whitespace\n ((char-whitespace? (string-ref expr i))\n (loop (+ i 1) tokens))\n ;; Numbers: decimal, hex (0x), octal (0), binary (0b), base-N (N#val)\n ((char-numeric? (string-ref expr i))\n (let-values (((num end) (read-number expr i)))\n (loop end (cons (make-arith-token 'number num) tokens))))\n ;; Names (variable references) — may also start base-N constant\n ((or (char-alphabetic? (string-ref expr i))\n (char=? (string-ref expr i) #\\_))\n (let-values (((name end) (read-name expr i)))\n (loop end (cons (make-arith-token 'name name) tokens))))\n ;; # is not a valid arithmetic operator — reject it early to prevent\n ;; side-effects from being committed before the syntax error is detected\n ((char=? (string-ref expr i) #\\#)\n (error #f (format \"arithmetic: syntax error: unexpected token '#'\")))\n ;; Multi-char operators\n (else\n (let-values (((op end) (read-operator expr i)))\n (loop end (cons (make-arith-token 'op op) tokens)))))))\n\n(def (read-number expr i)\n (let ((len (string-length expr)))\n (cond\n ;; Hex: 0x or 0X\n ((and (< (+ i 1) len)\n (char=? (string-ref expr i) #\\0)\n (or (char=? (string-ref expr (+ i 1)) #\\x)\n (char=? (string-ref expr (+ i 1)) #\\X)))\n (let loop ((j (+ i 2)))\n (if (and (< j len) (hex-digit? (string-ref expr j)))\n (loop (+ j 1))\n ;; Check for trailing alphanumeric (invalid hex like 0x1X)\n (begin\n (when (and (< j len)\n (let ((ch (string-ref expr j)))\n (or (char-alphabetic? ch) (char=? ch #\\_))))\n (error #f (format \"arithmetic: invalid hex constant: ~a\"\n (substring expr i (let lp ((k j))\n (if (and (< k len)\n (let ((c (string-ref expr k)))\n (or (arith-alnum? c) (char=? c #\\_))))\n (lp (+ k 1)) k))))))\n (let ((num (string->number (substring expr (+ i 2) j) 16)))\n (if num (values num j)\n (error #f (format \"arithmetic: invalid hex constant: ~a\"\n (substring expr i j)))))))))\n ;; Binary: 0b or 0B\n ((and (< (+ i 1) len)\n (char=? (string-ref expr i) #\\0)\n (or (char=? (string-ref expr (+ i 1)) #\\b)\n (char=? (string-ref expr (+ i 1)) #\\B)))\n (let loop ((j (+ i 2)))\n (if (and (< j len) (or (char=? (string-ref expr j) #\\0)\n (char=? (string-ref expr j) #\\1)))\n (loop (+ j 1))\n (values (string->number (substring expr (+ i 2) j) 2) j))))\n ;; Octal: starts with 0 followed by digit\n ((and (char=? (string-ref expr i) #\\0)\n (< (+ i 1) len)\n (char-numeric? (string-ref expr (+ i 1))))\n ;; Read all consecutive digits first, then validate\n (let loop ((j (+ i 1)))\n (if (and (< j len) (char-numeric? (string-ref expr j)))\n (loop (+ j 1))\n ;; Check that all digits are valid octal (0-7)\n (let check ((k (+ i 1)))\n (if (< k j)\n (if (octal-digit? (string-ref expr k))\n (check (+ k 1))\n (error #f (format \"arithmetic: invalid octal constant: ~a\"\n (substring expr i j))))\n ;; Check for trailing # (invalid base-N with leading 0: 02#xxx)\n (if (and (< j len) (char=? (string-ref expr j) #\\#))\n (let ((end (let lp ((k (+ j 1)))\n (if (and (< k len) (arith-alnum? (string-ref expr k)))\n (lp (+ k 1)) k))))\n (error #f (format \"arithmetic: invalid number: ~a\"\n (substring expr i end))))\n (let ((num (string->number (substring expr (+ i 1) j) 8)))\n (if num (values num j)\n (error #f (format \"arithmetic: invalid octal constant: ~a\"\n (substring expr i j)))))))))))\n ;; Decimal — possibly followed by #val for base-N\n (else\n (let loop ((j i))\n (if (and (< j len) (char-numeric? (string-ref expr j)))\n (loop (+ j 1))\n (cond\n ;; Float literal: reject\n ((and (< j len) (char=? (string-ref expr j) #\\.))\n (error #f \"arithmetic: invalid number (float not supported)\"))\n ;; Check for base#value pattern\n ((and (< j len) (char=? (string-ref expr j) #\\#))\n (let ((base (string->number (substring expr i j))))\n (if (and base (>= base 2) (<= base 64))\n ;; Read the value part (alphanumeric + _ + @)\n (let vloop ((k (+ j 1)) (val 0))\n (if (and (< k len) (base-n-digit? (string-ref expr k) base))\n (vloop (+ k 1) (+ (* val base) (base-n-digit-value (string-ref expr k))))\n ;; Check for trailing invalid digit (e.g. 2#A)\n (if (and (< k len)\n (let ((ch (string-ref expr k)))\n (or (arith-alnum? ch) (char=? ch #\\_) (char=? ch #\\@))))\n (error #f (format \"arithmetic: invalid base ~a constant: ~a\"\n base (substring expr i (let lp ((kk k))\n (if (and (< kk len)\n (let ((c (string-ref expr kk)))\n (or (arith-alnum? c) (char=? c #\\_) (char=? c #\\@))))\n (lp (+ kk 1)) kk)))))\n (if (= k (+ j 1))\n ;; No digits at all after base# — error\n (error #f (format \"arithmetic: invalid constant: ~a\"\n (substring expr i (+ j 1))))\n (values val k)))))\n (error #f (format \"arithmetic: invalid base: ~a\"\n (substring expr i j))))))\n ;; Check for trailing alphabetic chars (e.g. 42x) — invalid constant\n ((and (< j len)\n (let ((ch (string-ref expr j)))\n (or (char-alphabetic? ch) (char=? ch #\\_))))\n ;; Read the full invalid token\n (let lp ((k j))\n (if (and (< k len)\n (let ((ch (string-ref expr k)))\n (or (arith-alnum? ch) (char=? ch #\\_))))\n (lp (+ k 1))\n (error #f (format \"arithmetic: invalid constant: ~a\"\n (substring expr i k))))))\n (else\n (values (string->number (substring expr i j)) j)))))))))\n\n(def (read-name expr i)\n (let ((len (string-length expr)))\n (let loop ((j i))\n (if (and (< j len)\n (let ((ch (string-ref expr j)))\n (or (char-alphabetic? ch) (char-numeric? ch) (char=? ch #\\_))))\n (loop (+ j 1))\n (values (substring expr i j) j)))))\n\n(def (read-operator expr i)\n (let ((len (string-length expr))\n (ch (string-ref expr i)))\n (cond\n ;; Three-char operators\n ((and (< (+ i 2) len)\n (string=? (substring expr i (+ i 3)) \"<<=\"))\n (values \"<<=\" (+ i 3)))\n ((and (< (+ i 2) len)\n (string=? (substring expr i (+ i 3)) \">>=\"))\n (values \">>=\" (+ i 3)))\n ;; Two-char operators\n ((< (+ i 1) len)\n (let ((two (substring expr i (+ i 2))))\n (cond\n ((member two '(\"==\" \"!=\" \"<=\" \">=\" \"&&\" \"||\" \"<<\" \">>\"\n \"+=\" \"-=\" \"*=\" \"/=\" \"%=\" \"&=\" \"^=\" \"|=\"\n \"++\" \"--\" \"**\"))\n (values two (+ i 2)))\n (else\n (values (string ch) (+ i 1))))))\n (else\n (values (string ch) (+ i 1))))))\n\n(def (arith-alnum? ch)\n (or (char-alphabetic? ch) (char-numeric? ch)))\n\n(def (hex-digit? ch)\n (or (char-numeric? ch)\n (and (char>=? ch #\\a) (char<=? ch #\\f))\n (and (char>=? ch #\\A) (char<=? ch #\\F))))\n\n(def (octal-digit? ch)\n (and (char>=? ch #\\0) (char<=? ch #\\7)))\n\n;; Check if a character is a valid digit for base N\n;; Bases 2-36: 0-9 a-z (case insensitive)\n;; Bases 37-62: 0-9 a-z A-Z\n;; Bases 63-64: 0-9 a-z A-Z _ @\n(def (base-n-digit? ch base)\n (let ((v (base-n-digit-value-raw ch)))\n (and v (< v base))))\n\n(def (base-n-digit-value ch)\n (or (base-n-digit-value-raw ch) 0))\n\n(def (base-n-digit-value-raw ch)\n (cond\n ((and (char>=? ch #\\0) (char<=? ch #\\9))\n (- (char->integer ch) (char->integer #\\0)))\n ((and (char>=? ch #\\a) (char<=? ch #\\z))\n (+ 10 (- (char->integer ch) (char->integer #\\a))))\n ((and (char>=? ch #\\A) (char<=? ch #\\Z))\n (+ 36 (- (char->integer ch) (char->integer #\\A))))\n ((char=? ch #\\@) 62)\n ((char=? ch #\\_) 63)\n (else #f)))\n\n;;; --- Recursive descent parser (operator precedence) ---\n\n(def (arith-peek state)\n (if (>= (arith-state-pos state) (length (arith-state-tokens state)))\n #f\n (list-ref (arith-state-tokens state) (arith-state-pos state))))\n\n(def (arith-advance! state)\n (set! (arith-state-pos state) (+ 1 (arith-state-pos state))))\n\n(def (arith-expect-op! state expected)\n (let ((tok (arith-peek state)))\n (if (and tok (eq? (arith-token-type tok) 'op)\n (string=? (arith-token-value tok) expected))\n (begin (arith-advance! state) #t)\n (error #f (format \"arithmetic: expected ~a\" expected)))))\n\n(def (arith-match-op? state op)\n (let ((tok (arith-peek state)))\n (and tok (eq? (arith-token-type tok) 'op)\n (string=? (arith-token-value tok) op))))\n\n(def (arith-consume-op! state op)\n (if (arith-match-op? state op)\n (begin (arith-advance! state) #t)\n #f))\n\n;; Get variable value as integer, with recursive name resolution\n;; If the value of a variable is another variable name, resolve it\n;; For dynamic arithmetic resolution: e=1+2; echo $((e+3)) → 6\n(def (arith-get-var state name)\n (let resolve ((name name) (depth 0))\n (if (> depth 10) 0 ;; prevent infinite loops\n (let ((val ((arith-state-env-get state) name)))\n (cond\n ((not val)\n ;; Check nounset — if nounset? is a procedure, call it (raises exception)\n ;; If it's #t, raise generic error. If #f, silently return 0.\n (let ((nu (arith-state-nounset? state)))\n (cond\n ((procedure? nu) (nu name))\n (nu (error #f (format \"arithmetic: ~a: unbound variable\" name)))))\n 0)\n (else\n (let ((trimmed (arith-string-trim val)))\n (if (string=? trimmed \"\") 0\n (let ((num (string->number trimmed)))\n (if num num\n ;; Try to resolve as an arithmetic expression\n ;; First check if it's a plain variable name\n (if (and (> (string-length trimmed) 0)\n (or (char-alphabetic? (string-ref trimmed 0))\n (char=? (string-ref trimmed 0) #\\_))\n (let check ((j 0))\n (or (>= j (string-length trimmed))\n (let ((ch (string-ref trimmed j)))\n (and (or (char-alphabetic? ch) (char-numeric? ch)\n (char=? ch #\\_))\n (check (+ j 1)))))))\n ;; Plain variable name — resolve through variable chain\n (resolve trimmed (+ depth 1))\n ;; Contains operators — evaluate as arithmetic expression\n (arith-eval trimmed\n (arith-state-env-get state)\n (arith-state-env-set state)))))))))))))\n\n(def (arith-string-trim s)\n (let* ((len (string-length s))\n (start (let loop ((i 0))\n (if (and (< i len) (char-whitespace? (string-ref s i)))\n (loop (+ i 1)) i)))\n (end (let loop ((i len))\n (if (and (> i start) (char-whitespace? (string-ref s (- i 1))))\n (loop (- i 1)) i))))\n (substring s start end)))\n\n;; Set variable value (respects suppress-effects for short-circuit)\n(def (arith-set-var! state name value)\n (unless (arith-state-suppress-effects state)\n ((arith-state-env-set state) name (number->string value)))\n value)\n\n;; Resolve name through dynamic variable references (for assignment targets)\n(def (arith-resolve-name state name)\n (let resolve ((name name) (depth 0))\n (if (> depth 10) name\n (let ((val ((arith-state-env-get state) name)))\n (if (not val) name\n (let ((num (string->number val)))\n (if num name ;; value is numeric, stop\n ;; Value looks like a variable name, follow it\n (if (and (> (string-length val) 0)\n (or (char-alphabetic? (string-ref val 0))\n (char=? (string-ref val 0) #\\_))\n (let check ((j 0))\n (or (>= j (string-length val))\n (let ((ch (string-ref val j)))\n (and (or (char-alphabetic? ch) (char-numeric? ch)\n (char=? ch #\\_))\n (check (+ j 1)))))))\n (resolve val (+ depth 1))\n name))))))))\n\n;;; --- Precedence levels (lowest to highest) ---\n\n;; Level 1: Comma (sequence)\n(def (parse-comma-expr state)\n (let loop ((result (parse-assignment-expr state)))\n (if (arith-consume-op! state \",\")\n (loop (parse-assignment-expr state))\n result)))\n\n;; Level 2: Assignment\n(def (parse-assignment-expr state)\n (let ((tok (arith-peek state)))\n (if (and tok (eq? (arith-token-type tok) 'name))\n ;; Peek ahead for assignment operator (possibly past array index)\n (let* ((name (arith-token-value tok))\n (saved-pos (arith-state-pos state)))\n (arith-advance! state)\n ;; Check for array indexing: name[expr]\n (let-values (((final-name is-array?) (parse-array-index state name)))\n (let ((op-tok (arith-peek state)))\n (if (and op-tok (eq? (arith-token-type op-tok) 'op)\n (member (arith-token-value op-tok)\n '(\"=\" \"+=\" \"-=\" \"*=\" \"/=\" \"%=\" \"<<=\" \">>=\" \"&=\" \"^=\" \"|=\")))\n (let ((op (arith-token-value op-tok))\n ;; For dynamic names, resolve to final target\n (target (if is-array? final-name (arith-resolve-name state final-name))))\n (arith-advance! state)\n (let ((rhs (parse-assignment-expr state)))\n (cond\n ((string=? op \"=\") (arith-set-var! state target rhs))\n ((string=? op \"+=\") (arith-set-var! state target (+ (arith-get-var state target) rhs)))\n ((string=? op \"-=\") (arith-set-var! state target (- (arith-get-var state target) rhs)))\n ((string=? op \"*=\") (arith-set-var! state target (* (arith-get-var state target) rhs)))\n ((string=? op \"/=\") (arith-set-var! state target (quotient (arith-get-var state target) rhs)))\n ((string=? op \"%=\") (arith-set-var! state target (remainder (arith-get-var state target) rhs)))\n ((string=? op \"<<=\") (arith-set-var! state target (arithmetic-shift (arith-get-var state target) rhs)))\n ((string=? op \">>=\") (arith-set-var! state target (arithmetic-shift (arith-get-var state target) (- rhs))))\n ((string=? op \"&=\") (arith-set-var! state target (bitwise-and (arith-get-var state target) rhs)))\n ((string=? op \"^=\") (arith-set-var! state target (bitwise-xor (arith-get-var state target) rhs)))\n ((string=? op \"|=\") (arith-set-var! state target (bitwise-ior (arith-get-var state target) rhs)))\n (else (error #f \"unknown assignment op\" op)))))\n ;; Not an assignment, backtrack\n (begin\n (set! (arith-state-pos state) saved-pos)\n (parse-ternary-expr state))))))\n (parse-ternary-expr state))))\n\n;; Parse optional array index after a name: name[expr]\n;; Returns (values effective-name is-array?)\n(def (parse-array-index state name)\n (if (arith-consume-op! state \"[\")\n (let ((idx (parse-comma-expr state)))\n (arith-expect-op! state \"]\")\n (values (string-append name \"[\" (number->string idx) \"]\") #t))\n (values name #f)))\n\n;; Level 3: Ternary ?:\n(def (parse-ternary-expr state)\n (let ((cond-val (parse-logical-or state)))\n (if (arith-consume-op! state \"?\")\n (if (not (= cond-val 0))\n ;; Condition true: evaluate then-branch, suppress else-branch\n (let ((then-val (parse-comma-expr state)))\n (arith-expect-op! state \":\")\n (let ((saved (arith-state-suppress-effects state)))\n (set! (arith-state-suppress-effects state) #t)\n (parse-ternary-expr state)\n (set! (arith-state-suppress-effects state) saved))\n then-val)\n ;; Condition false: suppress then-branch, evaluate else-branch\n (begin\n (let ((saved (arith-state-suppress-effects state)))\n (set! (arith-state-suppress-effects state) #t)\n (parse-comma-expr state)\n (set! (arith-state-suppress-effects state) saved))\n (arith-expect-op! state \":\")\n (parse-ternary-expr state)))\n cond-val)))\n\n;; Level 4: Logical OR || (short-circuit)\n(def (parse-logical-or state)\n (let loop ((result (parse-logical-and state)))\n (if (arith-consume-op! state \"||\")\n (if (not (= result 0))\n ;; Short-circuit: parse but suppress side effects on RHS\n (let ((saved (arith-state-suppress-effects state)))\n (set! (arith-state-suppress-effects state) #t)\n (parse-logical-and state)\n (set! (arith-state-suppress-effects state) saved)\n (loop 1))\n (let ((rhs (parse-logical-and state)))\n (loop (if (not (= rhs 0)) 1 0))))\n result)))\n\n;; Level 5: Logical AND && (short-circuit)\n(def (parse-logical-and state)\n (let loop ((result (parse-bitwise-or state)))\n (if (arith-consume-op! state \"&&\")\n (if (= result 0)\n ;; Short-circuit: parse but suppress side effects on RHS\n (let ((saved (arith-state-suppress-effects state)))\n (set! (arith-state-suppress-effects state) #t)\n (parse-bitwise-or state)\n (set! (arith-state-suppress-effects state) saved)\n (loop 0))\n (let ((rhs (parse-bitwise-or state)))\n (loop (if (not (= rhs 0)) 1 0))))\n result)))\n\n;; Level 6: Bitwise OR |\n(def (parse-bitwise-or state)\n (let loop ((result (parse-bitwise-xor state)))\n (if (arith-consume-op! state \"|\")\n (loop (bitwise-ior result (parse-bitwise-xor state)))\n result)))\n\n;; Level 7: Bitwise XOR ^\n(def (parse-bitwise-xor state)\n (let loop ((result (parse-bitwise-and state)))\n (if (arith-consume-op! state \"^\")\n (loop (bitwise-xor result (parse-bitwise-and state)))\n result)))\n\n;; Level 8: Bitwise AND &\n(def (parse-bitwise-and state)\n (let loop ((result (parse-equality state)))\n (if (arith-consume-op! state \"&\")\n (loop (bitwise-and result (parse-equality state)))\n result)))\n\n;; Level 9: Equality == !=\n(def (parse-equality state)\n (let loop ((result (parse-comparison state)))\n (cond\n ((arith-consume-op! state \"==\")\n (loop (if (= result (parse-comparison state)) 1 0)))\n ((arith-consume-op! state \"!=\")\n (loop (if (not (= result (parse-comparison state))) 1 0)))\n (else result))))\n\n;; Level 10: Comparison < <= > >=\n(def (parse-comparison state)\n (let loop ((result (parse-shift state)))\n (cond\n ((arith-consume-op! state \"<=\")\n (loop (if (<= result (parse-shift state)) 1 0)))\n ((arith-consume-op! state \">=\")\n (loop (if (>= result (parse-shift state)) 1 0)))\n ((arith-consume-op! state \"<\")\n (loop (if (< result (parse-shift state)) 1 0)))\n ((arith-consume-op! state \">\")\n (loop (if (> result (parse-shift state)) 1 0)))\n (else result))))\n\n;; Level 11: Bit shift << >>\n;; Bash allows negative shifts (implementation-defined behavior)\n;; We emulate 64-bit C behavior: mask shift amount to 0-63\n(def (parse-shift state)\n (let loop ((result (parse-additive state)))\n (cond\n ((arith-consume-op! state \"<<\")\n (let* ((amt (parse-additive state))\n ;; Emulate 64-bit C: mask to 6 bits (0-63)\n (effective-amt (bitwise-and amt 63)))\n (loop (arith-truncate-64 (arithmetic-shift result effective-amt)))))\n ((arith-consume-op! state \">>\")\n (let* ((amt (parse-additive state))\n (effective-amt (bitwise-and amt 63)))\n (loop (arith-truncate-64 (arithmetic-shift (arith-to-signed-64 result) (- effective-amt))))))\n (else result))))\n\n;; Truncate to 64-bit signed integer range (emulate C int64_t)\n(def (arith-truncate-64 n)\n (let ((masked (bitwise-and n #xFFFFFFFFFFFFFFFF)))\n (if (> masked #x7FFFFFFFFFFFFFFF)\n (- masked #x10000000000000000)\n masked)))\n\n;; Convert to 64-bit signed representation\n(def (arith-to-signed-64 n)\n (let ((masked (bitwise-and n #xFFFFFFFFFFFFFFFF)))\n (if (> masked #x7FFFFFFFFFFFFFFF)\n (- masked #x10000000000000000)\n masked)))\n\n;; Level 12: Addition + -\n(def (parse-additive state)\n (let loop ((result (parse-multiplicative state)))\n (cond\n ((arith-consume-op! state \"+\")\n (loop (+ result (parse-multiplicative state))))\n ((arith-consume-op! state \"-\")\n (loop (- result (parse-multiplicative state))))\n (else result))))\n\n;; Level 13: Multiplication * / %\n;; Use quotient and remainder for C-style semantics\n(def (parse-multiplicative state)\n (let loop ((result (parse-exponent state)))\n (cond\n ((arith-consume-op! state \"*\")\n (loop (* result (parse-exponent state))))\n ((arith-consume-op! state \"/\")\n (let ((divisor (parse-exponent state)))\n (when (= divisor 0) (error #f \"arithmetic: division by zero\"))\n (loop (quotient result divisor))))\n ((arith-consume-op! state \"%\")\n (let ((divisor (parse-exponent state)))\n (when (= divisor 0) (error #f \"arithmetic: division by zero\"))\n (loop (remainder result divisor))))\n (else result))))\n\n;; Level 14: Exponentiation ** (right-associative)\n(def (parse-exponent state)\n (let ((base (parse-unary state)))\n (if (arith-consume-op! state \"**\")\n (let ((exp (parse-exponent state))) ;; right-associative\n (when (< exp 0) (error #f \"arithmetic: exponent less than 0\"))\n (expt base exp))\n base)))\n\n;; Level 15: Unary ! ~ + - (prefix)\n(def (parse-unary state)\n (cond\n ((arith-consume-op! state \"!\")\n (if (= (parse-unary state) 0) 1 0))\n ((arith-consume-op! state \"~\")\n (bitwise-not (parse-unary state)))\n ((arith-consume-op! state \"-\")\n (- (parse-unary state)))\n ((arith-consume-op! state \"+\")\n (parse-unary state))\n ;; Pre-increment/decrement\n ((arith-consume-op! state \"++\")\n (let ((tok (arith-peek state)))\n (if (and tok (eq? (arith-token-type tok) 'name))\n (let* ((name (arith-token-value tok)))\n (arith-advance! state)\n (let-values (((target is-array?) (parse-array-index state name)))\n (let ((resolved (if is-array? target (arith-resolve-name state target))))\n (arith-set-var! state resolved (+ (arith-get-var state resolved) 1)))))\n (error #f \"arithmetic: ++ requires variable\"))))\n ((arith-consume-op! state \"--\")\n (let ((tok (arith-peek state)))\n (if (and tok (eq? (arith-token-type tok) 'name))\n (let* ((name (arith-token-value tok)))\n (arith-advance! state)\n (let-values (((target is-array?) (parse-array-index state name)))\n (let ((resolved (if is-array? target (arith-resolve-name state target))))\n (arith-set-var! state resolved (- (arith-get-var state resolved) 1)))))\n (error #f \"arithmetic: -- requires variable\"))))\n (else (parse-postfix state))))\n\n;; Level 16: Postfix ++ --\n(def (parse-postfix state)\n (let ((val (parse-primary state)))\n ;; Check for postfix ++ or -- (only valid after a name)\n val))\n\n;; Primary: number, variable (with optional array index), or (expr)\n(def (parse-primary state)\n (let ((tok (arith-peek state)))\n (cond\n ((not tok) (error #f \"arithmetic: unexpected end of expression\"))\n ((eq? (arith-token-type tok) 'number)\n (arith-advance! state)\n (arith-token-value tok))\n ((eq? (arith-token-type tok) 'name)\n (arith-advance! state)\n (let ((name (arith-token-value tok)))\n ;; Check for array indexing: name[expr]\n (let-values (((effective-name is-array?) (parse-array-index state name)))\n ;; Check for postfix ++ --\n (cond\n ((arith-consume-op! state \"++\")\n (let* ((target (if is-array? effective-name (arith-resolve-name state effective-name)))\n (val (arith-get-var state target)))\n (arith-set-var! state target (+ val 1))\n val)) ;; return old value\n ((arith-consume-op! state \"--\")\n (let* ((target (if is-array? effective-name (arith-resolve-name state effective-name)))\n (val (arith-get-var state target)))\n (arith-set-var! state target (- val 1))\n val)) ;; return old value\n (else (arith-get-var state effective-name))))))\n ((and (eq? (arith-token-type tok) 'op)\n (string=? (arith-token-value tok) \"(\"))\n (arith-advance! state)\n (let ((result (parse-comma-expr state)))\n (arith-expect-op! state \")\")\n result))\n (else\n (error #f (format \"arithmetic: unexpected token ~a\" (arith-token-value tok)))))))\n"} {"text":";; FILE: jerboa-shell/bash-compatibility.md\n# Shell Compatibility Report\n\nGenerated: 2026-05-13\n\n## Summary\n\n| Shell | Pass | Total | Rate |\n|-------|------|-------|------|\n| bash | 901 | 1179 | 76% |\n| jsh-macos | 1149 | 1179 | 97% |\n\n## Results by Tier\n\n### Tier 0 — Core\n\n| Suite | Description | bash | jsh-macos |\n|-------|-------------|-----|-----|\n| smoke | Basic shell operations | 14/18 | **18/18** |\n| pipeline | Pipe operator and pipelines | 17/26 | 24/26 |\n| redirect | I/O redirection (>, <, >>, etc.) | 32/41 | **41/41** |\n| redirect-multi | Multiple and complex redirections | 11/13 | **13/13** |\n| builtin-eval-source | eval and source/. builtins | 20/23 | **23/23** |\n| command-sub | Command substitution $() and `` | 28/30 | **30/30** |\n| comments | Shell comments | **2/2** | **2/2** |\n| exit-status | Exit status and $? | 9/11 | **11/11** |\n\n### Tier 1 — Expansion & Variables\n\n| Suite | Description | bash | jsh-macos |\n|-------|-------------|-----|-----|\n| here-doc | Here-documents (<<, <<-, <<< ) | 35/36 | **36/36** |\n| quote | Quoting (single, double, $'...') | 28/35 | 34/35 |\n| word-eval | Word evaluation and expansion | **8/8** | **8/8** |\n| word-split | IFS word splitting | 47/55 | **55/55** |\n| var-sub | Variable substitution ($var, ${var}) | 4/6 | **6/6** |\n| var-sub-quote | Variable substitution in quoting contexts | 39/41 | **41/41** |\n| var-num | Numeric/special variables ($#, $?, $$, etc.) | **7/7** | **7/7** |\n| var-op-test | Variable operators (${var:-default}, etc.) | 26/37 | 35/37 |\n| var-op-strip | Variable pattern stripping (${var#pat}, etc.) | 27/29 | 28/29 |\n| var-op-len | Variable length ${#var} | 3/9 | 7/9 |\n| assign | Variable assignment | 33/48 | **48/48** |\n| tilde | Tilde expansion (~, ~user) | 8/14 | 12/14 |\n\n### Tier 2 — Builtins & Advanced\n\n| Suite | Description | bash | jsh-macos |\n|-------|-------------|-----|-----|\n| arith | Arithmetic expansion $(( )) and (( )) | 61/74 | **74/74** |\n| glob | Filename globbing (*, ?, [...]) | 35/39 | 38/39 |\n| brace-expansion | Brace expansion ({a,b}, {1..5}) | 36/55 | 52/55 |\n| case_ | case statement | 11/13 | **13/13** |\n| if_ | if/elif/else statement | **5/5** | **5/5** |\n| loop | while, until, for loops | 23/29 | **29/29** |\n| for-expr | C-style for ((i=0; ...)) | **9/9** | **9/9** |\n| subshell | Subshell execution (...) | **2/2** | **2/2** |\n| sh-func | Shell functions | 10/12 | **12/12** |\n| builtin-echo | echo builtin | 15/27 | **27/27** |\n| builtin-printf | printf builtin | 38/63 | 58/63 |\n| builtin-read | read builtin | 52/64 | **64/64** |\n| builtin-cd | cd builtin | 23/30 | 28/30 |\n| builtin-set | set and shopt builtins | **24/24** | **24/24** |\n| builtin-type | type/command/which builtins | 2/6 | 5/6 |\n| builtin-trap | trap builtin | 30/33 | **33/33** |\n| builtin-bracket | [[ ]] and [ ] test operators | 48/52 | **52/52** |\n| builtin-misc | Misc builtins (true, false, colon, etc.) | 2/7 | 5/7 |\n| builtin-process | Process builtins (kill, wait, ulimit, etc.) | 16/26 | 25/26 |\n| background | Background jobs (&, wait, jobs) | 19/27 | 25/27 |\n| command-parsing | Command parsing edge cases | 4/5 | **5/5** |\n| var-op-bash | Bash-specific variable operations | 6/27 | **27/27** |\n| var-op-slice | Variable slicing ${var:offset:length} | 13/22 | **22/22** |\n| assign-extended | declare/typeset/local/export | 19/39 | 36/39 |\n\n## Failing Tests — jsh-macos\n\nTests where jsh-macos fails but bash passes.\n\n### Tier 2 — Builtins & Advanced\n\n| Suite | # | Test | Reason |\n|-------|---|------|--------|\n| brace-expansion | 53 | Side effect in expansion | stdout mismatch |\n| background | 8 | wait for N parallel jobs and check failure | stdout mismatch |\n| background | 13 | Wait for job and PIPESTATUS | stdout mismatch |\n\n## Bonus: Tests where jsh-macos passes but bash fails\n\n| Suite | # | Test |\n|-------|---|------|\n| smoke | 4 | pipeline |\n| smoke | 5 | pipeline with builtin |\n| smoke | 12 | Here doc with redirect |\n| smoke | 15 | failed command |\n| pipeline | 4 | Redirect in Pipeline |\n| pipeline | 11 | |& |\n| pipeline | 19 | Evaluation of argv[0] in pipeline occurs in child |\n| pipeline | 20 | bash/dash/mksh run the last command is run in its own process |\n| pipeline | 21 | shopt -s lastpipe (always on in OSH) |\n| pipeline | 22 | shopt -s lastpipe (always on in OSH) |\n| pipeline | 26 | shopt -s lastpipe and shopt -s no_last_fork interaction |\n| redirect | 9 | Descriptor redirect with filename |\n| redirect | 10 | Redirect echo to stderr, and then redirect all of stdout somewhere. |\n| redirect | 11 | Named file descriptor |\n| redirect | 28 | 1>&2- (Bash bug: fail to restore closed fd) |\n| redirect | 31 | &>> appends stdout and stderr |\n| redirect | 33 | can't mention big file descriptor |\n| redirect | 37 | exec {fd}>&- (OSH regression: fails to close fd) |\n| redirect | 38 | noclobber can still write to non-regular files like /dev/null |\n| redirect | 40 | Parsing of x={myvar} and related cases |\n| redirect-multi | 3 | ysh behavior when glob doesn't match |\n| redirect-multi | 11 | Non-file redirects don't respect glob args (we differe from bash) |\n| builtin-eval-source | 5 | eval YSH block with 'break continue return error' |\n| builtin-eval-source | 14 | Source with syntax error |\n| builtin-eval-source | 15 | Eval with syntax error |\n| command-sub | 2 | case in subshell |\n| command-sub | 29 | Syntax errors with double quotes within backticks |\n| exit-status | 3 | subshell OverflowError https://github.com/oilshell/oil/issues/996 |\n| exit-status | 4 | func subshell OverflowError https://github.com/oilshell/oil/issues/996 |\n| here-doc | 7 | Here doc with bad comsub delimiter |\n| quote | 20 | $? split over multiple lines |\n| quote | 21 | Unterminated single quote |\n| quote | 22 | Unterminated double quote |\n| quote | 29 | $'' octal escapes with fewer than 3 chars |\n| quote | 34 | $'' supports \\cA escape for Ctrl-A - mask with 0x1f |\n| quote | 35 | \\c' is an escape, unlike bash |\n| word-split | 20 | empty literals are not elided |\n| word-split | 40 | IFS='' with ${!prefix@} and ${!prefix*} (bug #627) |\n| word-split | 41 | IFS='' with ${!a[@]} and ${!a[*]} (bug #627) |\n| word-split | 42 | Bug #628 split on : with : in literal word |\n| word-split | 49 | IFS=x and '' and $@ - same bug as spec/toysh-posix case #12 |\n| word-split | 50 | IFS=x and '' and $@ (#2) |\n| word-split | 51 | IFS=x and '' and $@ (#3) |\n| word-split | 52 | \"\"$A\"\" - empty string on both sides - derived from spec/toysh-posix #15 |\n| var-sub | 1 | Bad var sub |\n| var-sub | 2 | Braced block inside ${} |\n| var-sub-quote | 33 | \"${undef-'c d'}\" and \"${foo%'c d'}\" are parsed differently |\n| var-sub-quote | 39 | Right Brace as argument (similar to #702) |\n| var-op-test | 5 | Quoted with array as default value |\n| var-op-test | 13 | \"${array[@]} with set -u (bash is outlier) |\n| var-op-test | 16 | Nix idiom ${!hooksSlice+\"${!hooksSlice}\"} - was workaround for obsolete bash 4.3 bug |\n| var-op-test | 22 | $* (\"\" \"\") and - and + (IFS=) |\n| var-op-test | 23 | \"$*\" (\"\" \"\") and - and + (IFS=) |\n| var-op-test | 25 | Error when empty |\n| var-op-test | 26 | Error when unset |\n| var-op-test | 34 | op-test for ${a[@]} and ${a[*]} |\n| var-op-test | 36 | op-test for ${!array} with array=\"a[@]\" or array=\"a[*]\" |\n| var-op-strip | 11 | Strip unicode prefix |\n| var-op-len | 2 | Unicode string length (UTF-8) |\n| var-op-len | 7 | Length of undefined variable with nounset |\n| var-op-len | 8 | Length operator can't be followed by test operator |\n| var-op-len | 9 | ${#s} respects LC_ALL - length in bytes or code points |\n| assign | 3 | Env binding can use preceding bindings, but not subsequent ones |\n| assign | 15 | Env binding in readonly/declare is NOT exported! (pitfall) |\n| assign | 17 | dynamic local variables (and splitting) |\n| assign | 19 | 'local x' does not set variable |\n| assign | 20 | 'local -a x' does not set variable |\n| assign | 25 | Reveal existence of \"temp frame\" (All shells disagree here!!!) |\n| assign | 27 | Using ${x-default} after unsetting local shadowing a global |\n| assign | 28 | Using ${x-default} after unsetting a temp binding shadowing a global |\n| assign | 31 | assignment using dynamic keyword (splits in most shells, not in zsh/osh) |\n| assign | 32 | assignment using dynamic var names doesn't split |\n| assign | 35 | readonly $x where x='b c' |\n| assign | 41 | redirect after bare assignment |\n| assign | 44 | declare -A dict does not remove existing arrays (OSH regression) |\n| assign | 45 | \"readonly -a arr\" and \"readonly -A dict\" should not not remove existing arrays |\n| assign | 46 | \"declare -a arr\" and \"readonly -a a\" creates an empty array (OSH) |\n| tilde | 4 | No tilde expansion in word that looks like assignment but isn't |\n| tilde | 5 | tilde expansion of word after redirect |\n| tilde | 12 | x=${undef-~:~} |\n| tilde | 13 | strict tilde |\n| arith | 8 | Constant with quotes like '1' |\n| arith | 12 | Invalid string to int with strict_arith |\n| arith | 21 | Increment undefined variables with nounset |\n| arith | 29 | No floating point |\n| arith | 41 | nounset with arithmetic |\n| arith | 44 | Invalid LValue |\n| arith | 45 | Invalid LValue that looks like array |\n| arith | 46 | Invalid LValue: two sets of brackets |\n| arith | 51 | Comment not allowed in the middle of multiline arithmetic |\n| arith | 66 | Invalid constant |\n| arith | 69 | Negative numbers with bit shift |\n| arith | 71 | undef[0] with nounset |\n| arith | 74 | s[0] with string '12 34' |\n| glob | 24 | set -o noglob |\n| glob | 31 | Glob unicode char |\n| glob | 38 | pattern starting with . does not return . and .. |\n| brace-expansion | 12 | double expansion with simple var -- bash bug |\n| brace-expansion | 14 | double expansion with literal and simple var |\n| brace-expansion | 18 | { in expansion |\n| brace-expansion | 32 | Number range expansion |\n| brace-expansion | 33 | Ascending number range expansion with negative step is invalid |\n| brace-expansion | 34 | regression: -1 step disallowed |\n| brace-expansion | 35 | regression: 0 step disallowed |\n| brace-expansion | 36 | Descending number range expansion with positive step is invalid |\n| brace-expansion | 37 | Descending number range expansion with negative step |\n| brace-expansion | 38 | Singleton ranges |\n| brace-expansion | 39 | Singleton char ranges with steps |\n| brace-expansion | 41 | Char range expansion with step |\n| brace-expansion | 42 | Char ranges with steps of the wrong sign |\n| brace-expansion | 44 | Descending char range expansion |\n| brace-expansion | 45 | Fixed width number range expansion |\n| brace-expansion | 46 | Inconsistent fixed width number range expansion |\n| brace-expansion | 47 | Inconsistent fixed width number range expansion |\n| case_ | 2 | Case statement with ;;& |\n| case_ | 3 | Case statement with ;& |\n| loop | 3 | for loop with invalid identifier |\n| loop | 15 | continue in subshell |\n| loop | 16 | continue in subshell aborts with errexit |\n| loop | 17 | bad arg to break |\n| loop | 18 | too many args to continue |\n| loop | 24 | top-level break/continue/return (without strict_control_flow) |\n| sh-func | 9 | return \"\" (a lot of disagreement) |\n| sh-func | 12 | Scope of global variable when sourced in function (Shell Functions aren't Closures) |\n| builtin-echo | 10 | echo -e with C escapes |\n| builtin-echo | 16 | echo -e with 4 digit unicode escape |\n| builtin-echo | 17 | echo -e with 8 digit unicode escape |\n| builtin-echo | 18 | \\0377 is the highest octal byte |\n| builtin-echo | 19 | \\0400 is one more than the highest octal byte |\n| builtin-echo | 20 | \\0777 is out of range |\n| builtin-echo | 21 | incomplete hex escape |\n| builtin-echo | 22 | \\x |\n| builtin-echo | 23 | incomplete octal escape |\n| builtin-echo | 24 | incomplete unicode escape |\n| builtin-echo | 25 | \\u6 |\n| builtin-echo | 26 | \\0 \\1 \\8 |\n| builtin-printf | 4 | printf -v a[1] |\n| builtin-printf | 7 | dynamic declare instead of %q |\n| builtin-printf | 17 | %06s is no-op |\n| builtin-printf | 26 | Unicode char with ' |\n| builtin-printf | 27 | Invalid UTF-8 |\n| builtin-printf | 39 | printf %c unicode - prints the first BYTE of a string - it does not respect UTF-8 |\n| builtin-printf | 41 | printf %q |\n| builtin-printf | 42 | printf %6q (width) |\n| builtin-printf | 43 | printf negative numbers |\n| builtin-printf | 46 | Runtime error for invalid integer |\n| builtin-printf | 47 | %(strftime format)T |\n| builtin-printf | 48 | %(strftime format)T doesn't respect TZ if not exported |\n| builtin-printf | 49 | %(strftime format)T TZ in environ but not in shell's memory |\n| builtin-printf | 50 | %10.5(strftime format)T |\n| builtin-printf | 53 | printf positive integer overflow |\n| builtin-printf | 54 | printf negative integer overflow |\n| builtin-printf | 56 | printf %b unicode escapes |\n| builtin-printf | 59 | printf %b with truncated octal escapes |\n| builtin-printf | 62 | leading spaces are accepted in value given to %d %X, but not trailing spaces |\n| builtin-printf | 63 | Arbitrary base 64#a is rejected (unlike in shell arithmetic) |\n| builtin-read | 12 | read -n with invalid arg |\n| builtin-read | 15 | read -n vs. -N |\n| builtin-read | 16 | read -N ignores delimiters |\n| builtin-read | 34 | read -t 0 tests if input is available |\n| builtin-read | 36 | read -t -0.5 is invalid |\n| builtin-read | 38 | read -u syntax error |\n| builtin-read | 41 | read -u 3 -d b -N 6 |\n| builtin-read | 42 | read -N doesn't respect delimiter, while read -n does |\n| builtin-read | 44 | read usage |\n| builtin-read | 49 | mapfile from directory (bash doesn't handle errors) |\n| builtin-read | 50 | read -n 0 |\n| builtin-read | 64 | read bash bug |\n| builtin-cd | 3 | cd with 2 or more args - with strict_arg_parse |\n| builtin-cd | 26 | What happens when inherited $PWD and current dir disagree? |\n| builtin-cd | 27 | Survey of getcwd() syscall |\n| builtin-cd | 28 | chdir is a synonym for cd - busybox ash |\n| builtin-cd | 30 | pwd errors out on args with strict_arg_parse |\n| builtin-type | 3 | type of relative path |\n| builtin-type | 5 | special builtins are called out |\n| builtin-type | 6 | more special builtins |\n| builtin-trap | 1 | traps are not active inside subshells $() () trap | cat |\n| builtin-trap | 17 | exit 1 when trap code string is invalid |\n| builtin-trap | 33 | trap with command.NoOp - check internal invariant |\n| builtin-bracket | 31 | [ -t invalid ] |\n| builtin-bracket | 39 | -v to test variable (bash) |\n| builtin-bracket | 43 | Overflow error |\n| builtin-bracket | 51 | Looks like octal, but digit is too big |\n| builtin-misc | 1 | history builtin usage |\n| builtin-misc | 4 | time pipeline |\n| builtin-misc | 7 | Invalid shift argument |\n| builtin-process | 8 | Exit builtin with invalid arg |\n| builtin-process | 9 | Exit builtin with too many args |\n| builtin-process | 10 | time with brace group argument |\n| builtin-process | 12 | ulimit too many args |\n| builtin-process | 14 | ulimit negative arg |\n| builtin-process | 15 | ulimit -a doesn't take arg |\n| builtin-process | 16 | ulimit doesn't accept multiple flags - reduce confusion between shells |\n| builtin-process | 20 | ulimit that is 64 bits |\n| builtin-process | 22 | ulimit -f 1 prevents files larger 512 bytes |\n| background | 2 | wait -n with arguments - arguments are respected |\n| background | 3 | wait -n with nothing to wait for |\n| background | 6 | wait with invalid arg |\n| background | 18 | wait -n |\n| background | 22 | jobs prints one line per job |\n| background | 23 | jobs -p prints one line per job |\n| background | 25 | YSH wait --all |\n| background | 26 | YSH wait --verbose |\n| command-parsing | 1 | Prefix env on assignment |\n| var-op-bash | 1 | Lower Case with , and ,, |\n| var-op-bash | 2 | Upper Case with ^ and ^^ |\n| var-op-bash | 3 | Case folding - Unicode characters |\n| var-op-bash | 5 | Case folding that depends on locale (not enabled, requires Turkish locale) |\n| var-op-bash | 6 | Lower Case with constant string (VERY WEIRD) |\n| var-op-bash | 7 | Lower Case glob |\n| var-op-bash | 8 | ${x@u} U L - upper / lower case (bash 5.1 feature) |\n| var-op-bash | 9 | ${x@Q} |\n| var-op-bash | 10 | ${array@Q} and ${array[@]@Q} |\n| var-op-bash | 13 | ${var@a} for attributes |\n| var-op-bash | 14 | ${var@a} error conditions |\n| var-op-bash | 15 | undef and @P @Q @a |\n| var-op-bash | 16 | argv array and @P @Q @a |\n| var-op-bash | 17 | assoc array and @P @Q @a |\n| var-op-bash | 19 | ${#var@X} is a parse error |\n| var-op-bash | 21 | undef vs. empty string in var ops |\n| var-op-bash | 23 | ${a[0]@a} and ${a@a} |\n| var-op-bash | 24 | ${!r@a} with r='a[0]' (attribute for indirect expansion of an array element) |\n| var-op-bash | 25 | Array expansion with nullary var op @Q |\n| var-op-bash | 26 | Array expansion with nullary var op @P |\n| var-op-bash | 27 | Array expansion with nullary var op @a |\n| var-op-slice | 7 | Negative second arg is position, not length! |\n| var-op-slice | 8 | Negative start index respects unicode |\n| var-op-slice | 10 | Slice undefined |\n| var-op-slice | 12 | Slice string with invalid UTF-8 results in empty string and warning |\n| var-op-slice | 13 | Slice string with invalid UTF-8 with strict_word_eval |\n| var-op-slice | 16 | Simple ${@:offset} |\n| var-op-slice | 17 | ${@:offset} and ${*:offset} |\n| var-op-slice | 18 | ${@:offset:length} and ${*:offset:length} |\n| var-op-slice | 19 | ${@:0:1} |\n| assign-extended | 5 | declare -F with shopt -s extdebug prints more info |\n| assign-extended | 8 | declare |\n| assign-extended | 9 | declare -p |\n| assign-extended | 11 | declare -p var |\n| assign-extended | 12 | declare -p arr |\n| assign-extended | 14 | declare -pnrx |\n| assign-extended | 15 | declare -paA |\n| assign-extended | 16 | declare -pnrx var |\n| assign-extended | 17 | declare -pg |\n| assign-extended | 18 | declare -pg var |\n| assign-extended | 20 | declare -p and value.Undef |\n| assign-extended | 25 | typeset -r makes a string readonly |\n| assign-extended | 26 | typeset -ar makes it readonly |\n| assign-extended | 29 | Env bindings shouldn't contain array assignments |\n| assign-extended | 31 | declare -g (bash-specific; bash-completion uses it) |\n| assign-extended | 33 | dynamic array parsing is not allowed |\n| assign-extended | 36 | typeset +r removes read-only attribute (TODO: documented in bash to do nothing) |\n"} -{"text":";; FILE: jerboa-shell/build-jsh-android.ss\n#!chezscheme\n;;; build-jsh-android.ss — Build jsh binary on Android/Termux (aarch64, Bionic)\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:<stubs> < build-jsh-android.ss\n;;;\n;;; This script:\n;;; 1. Compiles jsh program with WPO\n;;; 2. Creates libs-only boot file\n;;; 3. Generates C files with embedded boot data\n;;; 4. Compiles C with cc (clang)\n;;; 5. Links binary with libkernel.a (static Chez) + shared Bionic libc\n;;;\n;;; The resulting jsh-android binary is a self-contained ELF for aarch64 Android.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table))\n\n;; Suppress format warnings during compilation (Chez warns about ~<space>\n;; directives in format strings used by jsh code)\n(define (with-warnings-suppressed thunk)\n (with-exception-handler\n (lambda (c) (if (warning? c) (void) (raise-continuable c)))\n thunk))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/data/data/com.termux/files/home\"))\n\n;; All deps are vendored inside the repo\n(define vendor-dir\n (or (getenv \"VENDOR\")\n (format \"~a/vendor\" (current-directory))))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (format \"~a/jerboa/lib\" vendor-dir)))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (format \"~a/jerboa\" vendor-dir)))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (format \"~a/jerboa-ssh/src\" vendor-dir)))\n\n(define jerboa-ssh-shim\n (format \"~a/jerboa-ssh/jerboa_ssh_shim.c\" vendor-dir))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jsqlite/src\" home-dir)))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls)\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (format \"~a/jerboa-crypto/src\" vendor-dir)))\n\n(define jerboa-crypto-shim\n (format \"~a/jerboa-crypto/jerboa_crypto_shim.c\" vendor-dir))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (format \"~a/jerboa-coreutils/lib\" vendor-dir)))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (format \"~a/jerboa-awk/lib\" vendor-dir)))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (format \"~a/jerboa-sed/lib\" vendor-dir)))\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (format \"~a/jerboa-aws/lib\" vendor-dir)))\n\n(define has-aws? (file-exists? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; Staged vendor directories (compiled .sls→.so by build-jsh-android.sh step 1b)\n(define stage-dir\n (or (getenv \"STAGE\")\n (format \"~a/android-stage\" (current-directory))))\n\n(define stage-jerboa-crypto (format \"~a/jerboa-crypto\" stage-dir))\n(define stage-jerboa-ssh (format \"~a/jerboa-ssh\" stage-dir))\n(define stage-jerboa-aws (format \"~a/jerboa-aws\" stage-dir))\n(define stage-jerboa-fuse (format \"~a/jerboa-fuse\" stage-dir))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/vault.sls\" stage-jerboa-fuse)))\n\n;; Chez Scheme static installation\n(define chez-tarm64le\n (or (getenv \"CHEZ_TARM64LE\")\n (let ([prefix \"/data/data/com.termux/files/usr/lib\"])\n (let ([dirs (directory-list prefix)])\n (let ([csv-dir (find (lambda (d)\n (and (> (string-length d) 3)\n (string=? \"csv\" (substring d 0 3))))\n dirs)])\n (if csv-dir\n (format \"~a/~a/tarm64le\" prefix csv-dir)\n (error 'build \"Cannot find Chez tarm64le directory\")))))))\n\n(define scheme-h-dir chez-tarm64le)\n(define petite-boot-path (format \"~a/petite.boot\" chez-tarm64le))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-tarm64le))\n\n(printf \"Chez static: ~a~n\" chez-tarm64le)\n(printf \"Jerboa: ~a~n\" jerboa-dir)\n(printf \"~n\")\n\n;; Rust native library (libjerboa_native.a — crypto, TLS, integrity, etc.)\n;; Built by build-jsh-android.sh or manually: cd ~/jerboa/jerboa-native-rs && cargo build --release\n(define native-lib-path\n (let ([env-path (getenv \"JERBOA_NATIVE_LIB\")]\n [vendor-path (format \"~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a\" vendor-dir)]\n [home-path (format \"~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a\" home-dir)])\n (cond\n [(and env-path (file-exists? env-path)) env-path]\n [(file-exists? vendor-path) vendor-path]\n [(file-exists? home-path) home-path]\n [else (error 'build-jsh-android\n \"libjerboa_native.a not found. Build it: cd ~/jerboa/jerboa-native-rs && cargo build --release\")])))\n(printf \"Native lib: ~a~n\" native-lib-path)\n\n;; Rust coreutils (libjsh_coreutils.a — ls, cat, grep, etc.)\n;; Built by build-jsh-android.sh or manually: cd rust-coreutils && cargo build --release\n(define rust-coreutils-lib-path\n (let ([env-path (getenv \"JSH_COREUTILS_LIB\")]\n [local-path (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" (current-directory))])\n (cond\n [(and env-path (file-exists? env-path)) env-path]\n [(file-exists? local-path) local-path]\n [else (error 'build-jsh-android\n \"libjsh_coreutils.a not found. Build it: cd rust-coreutils && cargo build --release\")])))\n(printf \"Coreutils: ~a~n\" rust-coreutils-lib-path)\n\n;; ========== Helper functions ==========\n\n(define (file->c-header input-path output-path array-name size-name)\n (let* ([port (open-file-input-port input-path)]\n [data (get-bytevector-all port)]\n [size (bytevector-length data)])\n (close-port port)\n (call-with-output-file output-path\n (lambda (out)\n (fprintf out \"/* Auto-generated — do not edit */~n\")\n (fprintf out \"static const unsigned char ~a[] = {~n\" array-name)\n (let loop ([i 0])\n (when (< i size)\n (when (= 0 (modulo i 16)) (fprintf out \" \"))\n (fprintf out \"0x~2,'0x\" (bytevector-u8-ref data i))\n (when (< (+ i 1) size) (fprintf out \",\"))\n (when (= 15 (modulo i 16)) (fprintf out \"~n\"))\n (loop (+ i 1))))\n (fprintf out \"~n};~n\")\n (fprintf out \"static const unsigned int ~a = ~a;~n\" size-name size))\n 'replace)\n (printf \" ~a: ~a bytes~n\" output-path size)))\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-android \"Command failed\" cmd)))\n\n(define (existing-sos dir modules)\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" dir m)) modules)))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 1: Compile jsh program ==========\n\n;; Generate jsh-generated.ss from jsh.ss (injecting the feature manifest).\n(unless (file-exists? \"jsh-generated.ss\")\n (let ([source-file \"jsh.ss\"])\n (printf \" Generating jsh-generated.ss from ~a~n\" source-file)\n (unless (file-exists? source-file)\n (error 'build-jsh-android \"Program source not found\" source-file))\n (let ([text (call-with-input-file source-file get-string-all)])\n (call-with-output-file \"jsh-generated.ss\"\n (lambda (out) (display text out))\n 'replace))))\n\n(printf \"[1/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(with-warnings-suppressed\n (lambda ()\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 3])\n (compile-program \"jsh-generated.ss\"))))\n\n(unless (file-exists? \"jsh.so\")\n (fprintf (current-error-port) \"FATAL: jsh.so not created~n\")\n (exit 1))\n\n;; ========== Step 2: Skip WPO (use jsh.so directly) ==========\n\n(printf \"[2/7] Using jsh.so (skipping WPO for Android build)...~n\")\n(define program-so \"jsh.so\")\n\n;; ========== Step 3: Create libs-only boot file ==========\n\n(printf \"[3/7] Creating libs-only boot file...~n\")\n\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (append\n ;; Jerboa runtime + stdlib\n (existing-sos jerboa-dir\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\"\n \"std/regex\"\n \"std/match2\" \"std/sugar\" \"std/result\"\n \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\" \"std/typed\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\"\n \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\" \"std/text/json\"\n \"std/net/tcp\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; Local compat layer\n (filter file-exists? (list \"src/compat/gambit.so\"))\n ;; Coreutils shim\n (filter file-exists? (list \"src/jsh/coreutils-shim.so\"))\n ;; jerboa-crypto (compiled in staging dir)\n (existing-sos stage-jerboa-crypto '(\"jerboa-crypto\"))\n ;; jerboa-ssh (sub-libraries must come before main jerboa-ssh.so)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" stage-jerboa-ssh))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" stage-jerboa-ssh m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" stage-jerboa-ssh)))\n '())\n ;; jsqlite is pure Jerboa and is compiled through normal imports.\n ;; jerboa-coreutils\n (existing-sos coreutils-dir\n '(\"jerboa-coreutils/common\" \"jerboa-coreutils/common/version\"\n \"jerboa-coreutils/common/security\"\n \"jerboa-coreutils/basename\" \"jerboa-coreutils/dirname\"\n \"jerboa-coreutils/link\" \"jerboa-coreutils/unlink\"\n \"jerboa-coreutils/yes\" \"jerboa-coreutils/printenv\"\n \"jerboa-coreutils/sleep\" \"jerboa-coreutils/whoami\"\n \"jerboa-coreutils/logname\" \"jerboa-coreutils/hostname\"\n \"jerboa-coreutils/nproc\" \"jerboa-coreutils/tty\"\n \"jerboa-coreutils/sync\" \"jerboa-coreutils/hostid\"\n \"jerboa-coreutils/cat\" \"jerboa-coreutils/head\"\n \"jerboa-coreutils/tail\" \"jerboa-coreutils/tac\"\n \"jerboa-coreutils/tee\" \"jerboa-coreutils/wc\"\n \"jerboa-coreutils/nl\" \"jerboa-coreutils/fold\"\n \"jerboa-coreutils/expand\" \"jerboa-coreutils/unexpand\"\n \"jerboa-coreutils/fmt\"\n \"jerboa-coreutils/cut\" \"jerboa-coreutils/paste\"\n \"jerboa-coreutils/join\" \"jerboa-coreutils/comm\"\n \"jerboa-coreutils/sort\" \"jerboa-coreutils/uniq\"\n \"jerboa-coreutils/tr\" \"jerboa-coreutils/numfmt\"\n \"jerboa-coreutils/mkdir\" \"jerboa-coreutils/rmdir\"\n \"jerboa-coreutils/mktemp\" \"jerboa-coreutils/touch\"\n \"jerboa-coreutils/readlink\" \"jerboa-coreutils/realpath\"\n \"jerboa-coreutils/ln\" \"jerboa-coreutils/cp\"\n \"jerboa-coreutils/mv\" \"jerboa-coreutils/rm\"\n \"jerboa-coreutils/install\" \"jerboa-coreutils/shred\"\n \"jerboa-coreutils/ls\" \"jerboa-coreutils/chmod\"\n \"jerboa-coreutils/chown\" \"jerboa-coreutils/chgrp\"\n \"jerboa-coreutils/stat\" \"jerboa-coreutils/du\"\n \"jerboa-coreutils/df\" \"jerboa-coreutils/pathchk\"\n \"jerboa-coreutils/date\" \"jerboa-coreutils/id\"\n \"jerboa-coreutils/groups\" \"jerboa-coreutils/who\"\n \"jerboa-coreutils/users\" \"jerboa-coreutils/pinky\"\n \"jerboa-coreutils/uptime\" \"jerboa-coreutils/uname\"\n \"jerboa-coreutils/arch\"\n \"jerboa-coreutils/seq\" \"jerboa-coreutils/expr\"\n \"jerboa-coreutils/basenc\" \"jerboa-coreutils/base64\"\n \"jerboa-coreutils/base32\" \"jerboa-coreutils/od\"\n \"jerboa-coreutils/cksum\" \"jerboa-coreutils/md5sum\"\n \"jerboa-coreutils/sha1sum\" \"jerboa-coreutils/sha224sum\"\n \"jerboa-coreutils/sha256sum\" \"jerboa-coreutils/sha384sum\"\n \"jerboa-coreutils/sha512sum\" \"jerboa-coreutils/b2sum\"\n \"jerboa-coreutils/sum\"\n \"jerboa-coreutils/env\" \"jerboa-coreutils/timeout\"\n \"jerboa-coreutils/nice\" \"jerboa-coreutils/nohup\"\n \"jerboa-coreutils/chroot\" \"jerboa-coreutils/stdbuf\"\n \"jerboa-coreutils/truncate\" \"jerboa-coreutils/mkfifo\"\n \"jerboa-coreutils/mknod\" \"jerboa-coreutils/split\"\n \"jerboa-coreutils/csplit\" \"jerboa-coreutils/dd\"\n \"jerboa-coreutils/dircolors\"\n \"jerboa-coreutils/tsort\" \"jerboa-coreutils/shuf\"\n \"jerboa-coreutils/factor\" \"jerboa-coreutils/pr\"\n \"jerboa-coreutils/ptx\" \"jerboa-coreutils/stty\"\n \"jerboa-coreutils/chcon\" \"jerboa-coreutils/runcon\"\n \"jerboa-coreutils/dir\" \"jerboa-coreutils/vdir\"\n \"jerboa-coreutils/rev\"\n \"jerboa-coreutils/grep/pcre2\" \"jerboa-coreutils/grep\"))\n ;; jerboa-awk\n (existing-sos awk-dir\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed\n (existing-sos sed-dir\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-aws (compiled in staging dir)\n (if has-aws?\n (existing-sos stage-jerboa-aws\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\"\n \"jerboa-aws/time\" \"jerboa-aws/crypto\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/creds\" \"jerboa-aws/request\"\n \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jerboa-fuse (vault) — if available\n (if has-jerboa-fuse?\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" stage-jerboa-fuse m))\n '(\"chez/vault/format\"\n \"chez/fuse/constants\" \"chez/fuse/types\" \"chez/fuse/mount\"\n \"chez/fuse/codec\" \"chez/fuse/secmem\" \"chez/fuse/access\"\n \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\")))\n '())\n ;; jsh modules\n (existing-sos \"src/jsh\"\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\"\n \"mux-transport\" \"mux-relay\" \"mux-router\"\n \"mux-server\" \"mux-client\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"config\" \"startup\" \"sandbox\"\n \"rl\" \"limits\" \"harden\" \"main\"\n \"coreutils\"))))\n\n;; ========== Step 4: Generate C headers with embedded data ==========\n\n(printf \"[4/7] Embedding boot files + program as C headers...~n\")\n(file->c-header program-so \"jsh_program.h\"\n \"jsh_program_data\" \"jsh_program_data_len\")\n(file->c-header petite-boot-path \"jsh_petite_boot.h\"\n \"petite_boot_data\" \"petite_boot_size\")\n(file->c-header scheme-boot-path \"jsh_scheme_boot.h\"\n \"scheme_boot_data\" \"scheme_boot_size\")\n(file->c-header \"jsh.boot\" \"jsh_jsh_boot.h\"\n \"jsh_boot_data\" \"jsh_boot_size\")\n\n;; ========== Step 5: Generate static_boot.c and main C ==========\n\n(printf \"[5/7] Generating C source files...~n\")\n\n(define build-dir\n (format \"~a/jsh-android-build\" (or (getenv \"TMPDIR\") \"/tmp\")))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(define program-c (format \"~a/jsh_main_android.c\" build-dir))\n\n(define gcc \"cc\")\n(define harden-cflags \"-fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2\")\n\n;; static_boot.c — registers embedded boot files with Chez\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"jsh_petite_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_scheme_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_jsh_boot.h\\\"\\n\" out)\n (display \"\\nvoid static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", (void *)petite_boot_data, petite_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", (void *)scheme_boot_data, scheme_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", (void *)jsh_boot_data, jsh_boot_size);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; jsh_main_android.c — main entry point\n(call-with-output-file program-c\n (lambda (out)\n (display \"#define _GNU_SOURCE\\n\" out)\n (display \"#include <stdio.h>\\n#include <stdlib.h>\\n#include <string.h>\\n\" out)\n (display \"#include <unistd.h>\\n#include <fcntl.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"jsh_program.h\\\"\\n\\n\" out)\n (display \"extern void static_boot_init(void);\\n\" out)\n (display \"extern void register_ffi_symbols(void);\\n\\n\" out)\n\n ;; Forward declarations for FFI\n (display \"/* FFI forward declarations */\\n\" out)\n (display \"extern void ffi_ensure_std_fds(void);\\n\\n\" out)\n\n ;; register_ffi_symbols — pre-register all FFI symbols for static builds\n (display \"void register_ffi_symbols(void) {\\n\" out)\n\n ;; Android/Termux: not a fully static build — symbols resolved via dlopen at runtime.\n ;; We don't need to pre-register POSIX or FFI symbols since the binary is dynamically linked.\n ;; Only register symbols that the Scheme code looks up by name via foreign-procedure.\n (display \" /* Android: dynamically linked — most symbols resolved via dlopen */\\n\" out)\n (display \"}\\n\\n\" out)\n\n ;; main\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args as env vars\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Resolve exe path via /proc/self/exe\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" ssize_t len = readlink(\\\"/proc/self/exe\\\", exe_buf, sizeof(exe_buf) - 1);\\n\" out)\n (display \" if (len > 0) { exe_buf[len] = '\\\\0'; setenv(\\\"JSH_EXE\\\", exe_buf, 1); }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; Load program via temp file (memfd /proc/self/fd paths are blocked by SELinux on Android)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" char prog_path[4096];\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-prog-XXXXXX\\\", tmpdir);\\n\" out)\n (display \" int fd = mkstemp(prog_path);\\n\" out)\n (display \" if (fd < 0) { perror(\\\"mkstemp\\\"); return 1; }\\n\" out)\n (display \" if (write(fd, jsh_program_data, jsh_program_data_len) != (ssize_t)jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"write tmpfile\\\"); close(fd); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" close(fd);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Copy C headers to build dir\n(system (format \"cp jsh_program.h jsh_petite_boot.h jsh_scheme_boot.h jsh_jsh_boot.h '~a/'\" build-dir))\n\n;; Generate Android compat header (explicit_bzero not in Bionic's <string.h>)\n(call-with-output-file (format \"~a/android-compat.h\" build-dir)\n (lambda (out)\n (display \"#ifndef ANDROID_COMPAT_H\\n#define ANDROID_COMPAT_H\\n\" out)\n (display \"#ifndef _GNU_SOURCE\\n#define _GNU_SOURCE\\n#endif\\n\" out)\n (display \"#include <string.h>\\n\" out)\n ;; explicit_bzero is not in Bionic's <string.h> on Termux —\n ;; provide an inline fallback unconditionally when missing.\n (display \"#if defined(__ANDROID__)\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n (display \"static inline void explicit_bzero(void *b, size_t l) {\\n\" out)\n (display \" memset(b, 0, l);\\n\" out)\n (display \" __asm__ __volatile__(\\\"\\\" ::: \\\"memory\\\");\\n\" out)\n (display \"}\\n\" out)\n (display \"#endif\\n\" out)\n (display \"#endif\\n\" out)))\n 'replace)\n\n(define android-compat (format \"-include '~a/android-compat.h'\" build-dir))\n\n;; ========== Step 6: Compile C with clang ==========\n\n(printf \"[6/7] Compiling C with clang...~n\")\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags android-compat scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_android.c\n(run-cmd (format \"~a -c -O2 ~a ~a -I'~a' -o '~a/jsh_main_android.o' '~a'\"\n gcc harden-cflags android-compat scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags android-compat build-dir))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; Landlock: ffi_landlock_* provided by ffi-shim.c (returns -1 if syscall unavailable)\n;; jerboa_landlock_* provided by libjerboa_native.a\n;; No separate shim needed.\n\n;; jerboa-ssh shim\n;; -DCHEZ_SSH_NO_OPENSSL on jerboa_ssh_shim.c: use standalone ed25519 from Rust\n;; (libjerboa_native.a provides ed25519_*_standalone symbols)\n;; jerboa_ssh_crypto.c is compiled separately as jerboa-ssh-crypto.o; it still uses\n;; OpenSSL EVP for SSH transport crypto (HMAC/SHA/X25519/ChaCha20-Poly1305).\n;; Termux ships libcrypto.so/libssl.so so we link against those system libs.\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; -DCHEZ_SSH_NO_OPENSSL: use standalone ed25519/AES from Rust, not OpenSSL\n (run-cmd (format \"~a -c -O2 ~a ~a -DCHEZ_SSH_NO_OPENSSL -I'~a/jerboa-ssh' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (let ([crypto-src (format \"~a/jerboa-ssh/jerboa_ssh_crypto.c\" vendor-dir)])\n (if (file-exists? crypto-src)\n ;; -I. for embed-crypto.h (project root)\n (run-cmd (format \"~a -c -O2 ~a ~a -I'~a/jerboa-ssh' -I'~a' -o '~a/ed25519-standalone.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir (current-directory) build-dir crypto-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))))\n (let ([bcrypt-src (format \"~a/jerboa-ssh/bcrypt_pbkdf.c\" vendor-dir)])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a ~a -I'~a/jerboa-ssh' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir)))))\n (begin\n (printf \" Warning: jerboa-ssh shim not found, building without SSH agent~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-crypto shim\n(if (file-exists? jerboa-crypto-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/jerboa-crypto-shim.o' '~a' -Wall\"\n gcc harden-cflags build-dir jerboa-crypto-shim))\n (begin\n (printf \" Warning: jerboa-crypto shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-crypto-shim.o' -\" gcc build-dir))))\n\n;; coreutils FFI shim\n(let ([cu-src (format \"~a/jerboa-coreutils/support/libcoreutils.c\" vendor-dir)])\n (if (file-exists? cu-src)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir cu-src))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir)))))\n\n;; ========== Step 7: Link binary ==========\n\n(printf \"[7/7] Linking jsh-android binary...~n\")\n\n(let* ([objs (format \"~a/jsh_main_android.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o ~a/jerboa-crypto-shim.o ~a/coreutils-ffi.o\"\n build-dir build-dir build-dir build-dir\n build-dir build-dir build-dir build-dir build-dir build-dir)]\n ;; Rust static libs must be wrapped in --whole-archive so all symbols\n ;; are included (Chez resolves them at runtime via dlsym, not at link time)\n [native-flag (format \" -Wl,--whole-archive ~a -Wl,--no-whole-archive\" native-lib-path)]\n [coreutils-flag (format \" -Wl,--whole-archive ~a -Wl,--no-whole-archive\" rust-coreutils-lib-path)]\n ;; Android/Termux link: Chez static libs + shared Bionic + Rust deps\n ;; libssl removed — TLS now via jerboa_tls_* (rustls)\n ;; libcrypto: still needed by vendor/jerboa-ssh/jerboa_ssh_crypto.c and\n ;; vendor/jerboa-crypto/jerboa_crypto_shim.c (EVP_*, HMAC, scrypt).\n ;; The JERBOA_SSH_NO_OPENSSL fallback requires updated vendor sources which\n ;; live outside git tracking; link dynamically against Termux libcrypto.\n [link-libs (format \"-L~a -lkernel ~a/libz.a ~a/liblz4.a -lm -ldl -lpthread -lutil -liconv -lncursesw -lc++_shared -lunwind -lcrypto\"\n chez-tarm64le chez-tarm64le chez-tarm64le)]\n [link-cmd (format \"~a -pie ~a -rdynamic -o jsh-android ~a~a~a ~a -Wl,--allow-multiple-definition\"\n gcc harden-cflags objs native-flag coreutils-flag link-libs)])\n (run-cmd link-cmd))\n\n;; ========== Hardening ==========\n\n(when (file-exists? \"jsh-android\")\n (printf \"~n[harden] Stripping debug symbols (preserving dynamic symbols)...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-android\"))])\n (system \"strip --strip-debug jsh-android\")\n (let ([post-size (file-length (open-file-input-port \"jsh-android\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n (printf \"[harden] Computing integrity hash...~n\")\n (let ([hash-file (format \"~a/_jsh_hash.txt\" (or (getenv \"TMPDIR\") \"/tmp\"))])\n (system (format \"sha256sum jsh-android | cut -d' ' -f1 | tr -d '\\\\n' > '~a'\" hash-file))\n (let ([hash-hex (call-with-input-file hash-file get-string-all)])\n (system (format \"rm -f '~a'\" hash-file))\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-android.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-android.sha256 (32 bytes)~n\"))))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(for-each (lambda (f) (when (file-exists? f) (delete-file f)))\n '(\"jsh_program.h\" \"jsh_petite_boot.h\" \"jsh_scheme_boot.h\" \"jsh_jsh_boot.h\"\n \"jsh.so\" \"jsh.wpo\" \"jsh.boot\"))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Binary created: jsh-android~n~n\")\n(system \"ls -lh jsh-android\")\n(printf \"~n\")\n(system \"file jsh-android\")\n(printf \"~nTest: ./jsh-android -c 'echo Hello from jsh on Android'~n\")\n"} +{"text":";; FILE: jerboa-shell/build-jsh-android.ss\n#!chezscheme\n;;; build-jsh-android.ss — Build jsh binary on Android/Termux (aarch64, Bionic)\n;;;\n;;; Usage: scheme -q --libdirs src:<jerboa-lib>:<stubs> < build-jsh-android.ss\n;;;\n;;; This script:\n;;; 1. Compiles jsh program with WPO\n;;; 2. Creates libs-only boot file\n;;; 3. Generates C files with embedded boot data\n;;; 4. Compiles C with cc (clang)\n;;; 5. Links binary with libkernel.a (static Chez) + shared Bionic libc\n;;;\n;;; The resulting jsh-android binary is a self-contained ELF for aarch64 Android.\n\n(import\n (except (chezscheme) void box box? unbox set-box!\n andmap ormap iota last-pair find\n 1+ 1- fx/ fx1+ fx1-\n error error? raise with-exception-handler identifier?\n hash-table? make-hash-table))\n\n;; Suppress format warnings during compilation (Chez warns about ~<space>\n;; directives in format strings used by jsh code)\n(define (with-warnings-suppressed thunk)\n (with-exception-handler\n (lambda (c) (if (warning? c) (void) (raise-continuable c)))\n thunk))\n\n;; ========== Locate directories ==========\n\n(define home-dir (or (getenv \"HOME\") \"/data/data/com.termux/files/home\"))\n\n;; All deps are vendored inside the repo\n(define vendor-dir\n (or (getenv \"VENDOR\")\n (format \"~a/vendor\" (current-directory))))\n\n(define jerboa-dir\n (or (getenv \"JERBOA_DIR\")\n (format \"~a/jerboa/lib\" vendor-dir)))\n\n(define jerboa-dir-base\n (or (getenv \"JERBOA_BASE_DIR\")\n (format \"~a/jerboa\" vendor-dir)))\n\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE any compile so only the patched source loads.\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss (format \"~a/std/net/allow-proxy.ss\" jerboa-dir)]\n [ap-sls (format \"~a/std/net/allow-proxy.sls\" jerboa-dir)]\n [ap-so (format \"~a/std/net/allow-proxy.so\" jerboa-dir)]\n [ap-wpo (format \"~a/std/net/allow-proxy.wpo\" jerboa-dir)])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \" applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n(define jerboa-ssh-dir\n (or (getenv \"JERBOA_SSH_DIR\")\n (format \"~a/jerboa-ssh/src\" vendor-dir)))\n\n(define jerboa-ssh-shim\n (format \"~a/jerboa-ssh/jerboa_ssh_shim.c\" vendor-dir))\n\n(define jsqlite-dir\n (or (getenv \"JSQLITE_DIR\")\n (format \"~a/mine/jerboa-sqlite/src\" home-dir)))\n\n;; jerboa-ssl/jerboa-https removed — TLS/HTTPS now via (std net request) (rustls)\n\n(define jerboa-crypto-dir\n (or (getenv \"JERBOA_CRYPTO_DIR\")\n (format \"~a/jerboa-crypto/src\" vendor-dir)))\n\n(define jerboa-crypto-shim\n (format \"~a/jerboa-crypto/jerboa_crypto_shim.c\" vendor-dir))\n\n(define coreutils-dir\n (or (getenv \"COREUTILS_DIR\")\n (format \"~a/jerboa-coreutils/lib\" vendor-dir)))\n\n(define awk-dir\n (or (getenv \"AWK_DIR\")\n (format \"~a/jerboa-awk/lib\" vendor-dir)))\n\n(define sed-dir\n (or (getenv \"SED_DIR\")\n (format \"~a/jerboa-sed/lib\" vendor-dir)))\n\n(define aws-dir\n (or (getenv \"AWS_DIR\")\n (format \"~a/jerboa-aws/lib\" vendor-dir)))\n\n(define has-aws? (file-exists? (format \"~a/jerboa-aws\" aws-dir)))\n\n;; Staged vendor directories (compiled .sls→.so by build-jsh-android.sh step 1b)\n(define stage-dir\n (or (getenv \"STAGE\")\n (format \"~a/android-stage\" (current-directory))))\n\n(define stage-jerboa-crypto (format \"~a/jerboa-crypto\" stage-dir))\n(define stage-jerboa-ssh (format \"~a/jerboa-ssh\" stage-dir))\n(define stage-jerboa-aws (format \"~a/jerboa-aws\" stage-dir))\n(define stage-jerboa-fuse (format \"~a/jerboa-fuse\" stage-dir))\n\n(define has-jerboa-fuse?\n (file-exists? (format \"~a/chez/vault.sls\" stage-jerboa-fuse)))\n\n;; Chez Scheme static installation\n(define chez-tarm64le\n (or (getenv \"CHEZ_TARM64LE\")\n (let ([prefix \"/data/data/com.termux/files/usr/lib\"])\n (let ([dirs (directory-list prefix)])\n (let ([csv-dir (find (lambda (d)\n (and (> (string-length d) 3)\n (string=? \"csv\" (substring d 0 3))))\n dirs)])\n (if csv-dir\n (format \"~a/~a/tarm64le\" prefix csv-dir)\n (error 'build \"Cannot find Chez tarm64le directory\")))))))\n\n(define scheme-h-dir chez-tarm64le)\n(define petite-boot-path (format \"~a/petite.boot\" chez-tarm64le))\n(define scheme-boot-path (format \"~a/scheme.boot\" chez-tarm64le))\n\n(printf \"Chez static: ~a~n\" chez-tarm64le)\n(printf \"Jerboa: ~a~n\" jerboa-dir)\n(printf \"~n\")\n\n;; Rust native library (libjerboa_native.a — crypto, TLS, integrity, etc.)\n;; Built by build-jsh-android.sh or manually: cd ~/jerboa/jerboa-native-rs && cargo build --release\n(define native-lib-path\n (let ([env-path (getenv \"JERBOA_NATIVE_LIB\")]\n [vendor-path (format \"~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a\" vendor-dir)]\n [home-path (format \"~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a\" home-dir)])\n (cond\n [(and env-path (file-exists? env-path)) env-path]\n [(file-exists? vendor-path) vendor-path]\n [(file-exists? home-path) home-path]\n [else (error 'build-jsh-android\n \"libjerboa_native.a not found. Build it: cd ~/jerboa/jerboa-native-rs && cargo build --release\")])))\n(printf \"Native lib: ~a~n\" native-lib-path)\n\n;; Rust coreutils (libjsh_coreutils.a — ls, cat, grep, etc.)\n;; Built by build-jsh-android.sh or manually: cd rust-coreutils && cargo build --release\n(define rust-coreutils-lib-path\n (let ([env-path (getenv \"JSH_COREUTILS_LIB\")]\n [local-path (format \"~a/rust-coreutils/target/release/libjsh_coreutils.a\" (current-directory))])\n (cond\n [(and env-path (file-exists? env-path)) env-path]\n [(file-exists? local-path) local-path]\n [else (error 'build-jsh-android\n \"libjsh_coreutils.a not found. Build it: cd rust-coreutils && cargo build --release\")])))\n(printf \"Coreutils: ~a~n\" rust-coreutils-lib-path)\n\n;; ========== Helper functions ==========\n\n(define (file->c-header input-path output-path array-name size-name)\n (let* ([port (open-file-input-port input-path)]\n [data (get-bytevector-all port)]\n [size (bytevector-length data)])\n (close-port port)\n (call-with-output-file output-path\n (lambda (out)\n (fprintf out \"/* Auto-generated — do not edit */~n\")\n (fprintf out \"static const unsigned char ~a[] = {~n\" array-name)\n (let loop ([i 0])\n (when (< i size)\n (when (= 0 (modulo i 16)) (fprintf out \" \"))\n (fprintf out \"0x~2,'0x\" (bytevector-u8-ref data i))\n (when (< (+ i 1) size) (fprintf out \",\"))\n (when (= 15 (modulo i 16)) (fprintf out \"~n\"))\n (loop (+ i 1))))\n (fprintf out \"~n};~n\")\n (fprintf out \"static const unsigned int ~a = ~a;~n\" size-name size))\n 'replace)\n (printf \" ~a: ~a bytes~n\" output-path size)))\n\n(define (run-cmd cmd)\n (printf \" ~a~n\" cmd)\n (unless (= 0 (system cmd))\n (error 'build-jsh-android \"Command failed\" cmd)))\n\n(define (existing-sos dir modules)\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" dir m)) modules)))\n\n;; ========== Feature resolution ==========\n;; Derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; ========== Step 1: Compile jsh program ==========\n\n;; Generate jsh-generated.ss from jsh.ss (injecting the feature manifest).\n(unless (file-exists? \"jsh-generated.ss\")\n (let ([source-file \"jsh.ss\"])\n (printf \" Generating jsh-generated.ss from ~a~n\" source-file)\n (unless (file-exists? source-file)\n (error 'build-jsh-android \"Program source not found\" source-file))\n (let ([text (call-with-input-file source-file get-string-all)])\n (call-with-output-file \"jsh-generated.ss\"\n (lambda (out) (display text out))\n 'replace))))\n\n(printf \"[1/7] Compiling jsh-generated.ss (~a, optimize-level 3)...~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(with-warnings-suppressed\n (lambda ()\n (parameterize ([compile-imported-libraries #t]\n [optimize-level 3])\n (compile-program \"jsh-generated.ss\"))))\n\n(unless (file-exists? \"jsh.so\")\n (fprintf (current-error-port) \"FATAL: jsh.so not created~n\")\n (exit 1))\n\n;; ========== Step 2: Skip WPO (use jsh.so directly) ==========\n\n(printf \"[2/7] Using jsh.so (skipping WPO for Android build)...~n\")\n(define program-so \"jsh.so\")\n\n;; ========== Step 3: Create libs-only boot file ==========\n\n(printf \"[3/7] Creating libs-only boot file...~n\")\n\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (append\n ;; Jerboa runtime + stdlib\n (existing-sos jerboa-dir\n '(\"jerboa/core\" \"jerboa/runtime\"\n \"std/error\" \"std/error/conditions\" \"std/format\" \"std/sort\" \"std/pregexp\"\n \"std/regex\"\n \"std/match2\" \"std/sugar\" \"std/result\"\n \"std/misc/string\" \"std/misc/string-more\" \"std/misc/list\" \"std/misc/alist\" \"std/misc/thread\"\n \"std/stm\" \"std/foreign\" \"std/os/path\" \"std/os/platform\" \"std/os/posix\" \"std/os/limits\" \"std/os/supervise\" \"std/os/limits/sandbox\" \"std/os/tracefs\" \"std/net/allowlist\" \"std/os/signal\" \"std/os/fdio\"\n \"std/transducer\" \"std/log\" \"std/typed\"\n \"std/capability\" \"std/capability/sandbox\" \"std/security/capsicum\"\n \"std/os/landlock\" \"std/os/sandbox\"\n \"std/security/landlock\" \"std/security/seatbelt\" \"std/security/cage\" \"std/security/seccomp\"\n \"std/misc/lru-cache\" \"std/misc/trie\" \"std/text/glob\" \"std/misc/process\"\n \"std/gambit-compat\"\n \"std/misc/guardian-pool\" \"std/misc/diff\" \"std/misc/fmt\" \"std/misc/terminal\"\n \"std/misc/custodian\" \"std/misc/profile\" \"std/misc/memoize\" \"std/misc/config\"\n \"std/actor/mpsc\" \"std/actor/core\" \"std/net/tcp-raw\"\n \"std/crypto/native\" \"std/crypto/random\" \"std/crypto/native-rust\"\n \"std/actor/transport\"\n \"std/cli/getopt\" \"std/misc/ports\" \"std/crypto/digest\"\n \"std/srfi/srfi-13\" \"std/srfi/srfi-115\" \"std/text/base64\" \"std/text/json\"\n \"std/net/tcp\" \"std/net/tls-rustls\" \"std/net/request\"\n \"std/net/websocket\" \"std/net/socks5-server\"\n \"std/debug/timetravel\"))\n ;; Local compat layer\n (filter file-exists? (list \"src/compat/gambit.so\"))\n ;; Coreutils shim\n (filter file-exists? (list \"src/jsh/coreutils-shim.so\"))\n ;; jerboa-crypto (compiled in staging dir)\n (existing-sos stage-jerboa-crypto '(\"jerboa-crypto\"))\n ;; jerboa-ssh (sub-libraries must come before main jerboa-ssh.so)\n (if (file-exists? (format \"~a/jerboa-ssh.so\" stage-jerboa-ssh))\n (append\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" stage-jerboa-ssh m))\n '(\"jerboa-ssh/crypto\"\n \"ssh/wire\" \"ssh/known-hosts\" \"ssh/transport\" \"ssh/kex\"\n \"ssh/auth\" \"ssh/channel\" \"ssh/session\" \"ssh/sftp\"\n \"ssh/forward\" \"ssh/client\")))\n (list (format \"~a/jerboa-ssh.so\" stage-jerboa-ssh)))\n '())\n ;; jsqlite is pure Jerboa and is compiled through normal imports.\n ;; jerboa-coreutils\n (existing-sos coreutils-dir\n '(\"jerboa-coreutils/common\" \"jerboa-coreutils/common/version\"\n \"jerboa-coreutils/common/security\"\n \"jerboa-coreutils/basename\" \"jerboa-coreutils/dirname\"\n \"jerboa-coreutils/link\" \"jerboa-coreutils/unlink\"\n \"jerboa-coreutils/yes\" \"jerboa-coreutils/printenv\"\n \"jerboa-coreutils/sleep\" \"jerboa-coreutils/whoami\"\n \"jerboa-coreutils/logname\" \"jerboa-coreutils/hostname\"\n \"jerboa-coreutils/nproc\" \"jerboa-coreutils/tty\"\n \"jerboa-coreutils/sync\" \"jerboa-coreutils/hostid\"\n \"jerboa-coreutils/cat\" \"jerboa-coreutils/head\"\n \"jerboa-coreutils/tail\" \"jerboa-coreutils/tac\"\n \"jerboa-coreutils/tee\" \"jerboa-coreutils/wc\"\n \"jerboa-coreutils/nl\" \"jerboa-coreutils/fold\"\n \"jerboa-coreutils/expand\" \"jerboa-coreutils/unexpand\"\n \"jerboa-coreutils/fmt\"\n \"jerboa-coreutils/cut\" \"jerboa-coreutils/paste\"\n \"jerboa-coreutils/join\" \"jerboa-coreutils/comm\"\n \"jerboa-coreutils/sort\" \"jerboa-coreutils/uniq\"\n \"jerboa-coreutils/tr\" \"jerboa-coreutils/numfmt\"\n \"jerboa-coreutils/mkdir\" \"jerboa-coreutils/rmdir\"\n \"jerboa-coreutils/mktemp\" \"jerboa-coreutils/touch\"\n \"jerboa-coreutils/readlink\" \"jerboa-coreutils/realpath\"\n \"jerboa-coreutils/ln\" \"jerboa-coreutils/cp\"\n \"jerboa-coreutils/mv\" \"jerboa-coreutils/rm\"\n \"jerboa-coreutils/install\" \"jerboa-coreutils/shred\"\n \"jerboa-coreutils/ls\" \"jerboa-coreutils/chmod\"\n \"jerboa-coreutils/chown\" \"jerboa-coreutils/chgrp\"\n \"jerboa-coreutils/stat\" \"jerboa-coreutils/du\"\n \"jerboa-coreutils/df\" \"jerboa-coreutils/pathchk\"\n \"jerboa-coreutils/date\" \"jerboa-coreutils/id\"\n \"jerboa-coreutils/groups\" \"jerboa-coreutils/who\"\n \"jerboa-coreutils/users\" \"jerboa-coreutils/pinky\"\n \"jerboa-coreutils/uptime\" \"jerboa-coreutils/uname\"\n \"jerboa-coreutils/arch\"\n \"jerboa-coreutils/seq\" \"jerboa-coreutils/expr\"\n \"jerboa-coreutils/basenc\" \"jerboa-coreutils/base64\"\n \"jerboa-coreutils/base32\" \"jerboa-coreutils/od\"\n \"jerboa-coreutils/cksum\" \"jerboa-coreutils/md5sum\"\n \"jerboa-coreutils/sha1sum\" \"jerboa-coreutils/sha224sum\"\n \"jerboa-coreutils/sha256sum\" \"jerboa-coreutils/sha384sum\"\n \"jerboa-coreutils/sha512sum\" \"jerboa-coreutils/b2sum\"\n \"jerboa-coreutils/sum\"\n \"jerboa-coreutils/env\" \"jerboa-coreutils/timeout\"\n \"jerboa-coreutils/nice\" \"jerboa-coreutils/nohup\"\n \"jerboa-coreutils/chroot\" \"jerboa-coreutils/stdbuf\"\n \"jerboa-coreutils/truncate\" \"jerboa-coreutils/mkfifo\"\n \"jerboa-coreutils/mknod\" \"jerboa-coreutils/split\"\n \"jerboa-coreutils/csplit\" \"jerboa-coreutils/dd\"\n \"jerboa-coreutils/dircolors\"\n \"jerboa-coreutils/tsort\" \"jerboa-coreutils/shuf\"\n \"jerboa-coreutils/factor\" \"jerboa-coreutils/pr\"\n \"jerboa-coreutils/ptx\" \"jerboa-coreutils/stty\"\n \"jerboa-coreutils/chcon\" \"jerboa-coreutils/runcon\"\n \"jerboa-coreutils/dir\" \"jerboa-coreutils/vdir\"\n \"jerboa-coreutils/rev\"\n \"jerboa-coreutils/grep/pcre2\" \"jerboa-coreutils/grep\"))\n ;; jerboa-awk\n (existing-sos awk-dir\n '(\"jerboa-awk/ast\" \"jerboa-awk/value\" \"jerboa-awk/lexer\"\n \"jerboa-awk/parser\" \"jerboa-awk/runtime\"\n \"jerboa-awk/builtins/string\" \"jerboa-awk/builtins/math\"\n \"jerboa-awk/builtins/io\" \"jerboa-awk/main\"))\n ;; jerboa-sed\n (existing-sos sed-dir\n '(\"sed/pcre2\" \"sed/ast\" \"sed/parser\" \"sed/engine\" \"sed/main\"))\n ;; jerboa-aws (compiled in staging dir)\n (if has-aws?\n (existing-sos stage-jerboa-aws\n '(\"jerboa-aws/json\" \"jerboa-aws/xml\" \"jerboa-aws/uri\"\n \"jerboa-aws/time\" \"jerboa-aws/crypto\" \"jerboa-aws/sigv4\"\n \"jerboa-aws/creds\" \"jerboa-aws/request\"\n \"jerboa-aws/api\" \"jerboa-aws/json-api\"\n \"jerboa-aws/ec2/xml\" \"jerboa-aws/ec2/params\" \"jerboa-aws/ec2/api\"\n \"jerboa-aws/ec2/instances\" \"jerboa-aws/ec2/security-groups\"\n \"jerboa-aws/ec2/vpcs\" \"jerboa-aws/ec2/subnets\"\n \"jerboa-aws/ec2/volumes\" \"jerboa-aws/ec2/snapshots\"\n \"jerboa-aws/ec2/addresses\" \"jerboa-aws/ec2/key-pairs\"\n \"jerboa-aws/ec2/network-interfaces\" \"jerboa-aws/ec2/images\"\n \"jerboa-aws/ec2/regions\" \"jerboa-aws/ec2/internet-gateways\"\n \"jerboa-aws/ec2/nat-gateways\" \"jerboa-aws/ec2/route-tables\"\n \"jerboa-aws/ec2/launch-templates\" \"jerboa-aws/ec2/tags\"\n \"jerboa-aws/s3/xml\" \"jerboa-aws/s3/api\"\n \"jerboa-aws/s3/buckets\" \"jerboa-aws/s3/objects\"\n \"jerboa-aws/sts/api\" \"jerboa-aws/sts/operations\"\n \"jerboa-aws/iam/api\" \"jerboa-aws/iam/users\" \"jerboa-aws/iam/groups\"\n \"jerboa-aws/iam/roles\" \"jerboa-aws/iam/policies\" \"jerboa-aws/iam/access-keys\"\n \"jerboa-aws/lambda/api\" \"jerboa-aws/lambda/functions\"\n \"jerboa-aws/dynamodb/api\" \"jerboa-aws/dynamodb/operations\"\n \"jerboa-aws/logs/api\" \"jerboa-aws/logs/operations\"\n \"jerboa-aws/sns/api\" \"jerboa-aws/sns/operations\"\n \"jerboa-aws/sqs/api\" \"jerboa-aws/sqs/operations\"\n \"jerboa-aws/ssm/api\" \"jerboa-aws/ssm/operations\" \"jerboa-aws/pssm\"\n \"jerboa-aws/rds/api\" \"jerboa-aws/rds/db-instances\"\n \"jerboa-aws/elbv2/api\" \"jerboa-aws/elbv2/operations\"\n \"jerboa-aws/cfn/api\" \"jerboa-aws/cfn/stacks\"\n \"jerboa-aws/cloudwatch/api\" \"jerboa-aws/cloudwatch/operations\"\n \"jerboa-aws/compute-optimizer/api\" \"jerboa-aws/compute-optimizer/operations\"\n \"jerboa-aws/cost-optimization-hub/api\" \"jerboa-aws/cost-optimization-hub/operations\"\n \"jerboa-aws/cli/format\" \"jerboa-aws/cli/main\"))\n '())\n ;; jerboa-fuse (vault) — if available\n (if has-jerboa-fuse?\n (filter file-exists?\n (map (lambda (m) (format \"~a/~a.so\" stage-jerboa-fuse m))\n '(\"chez/vault/format\"\n \"chez/fuse/constants\" \"chez/fuse/types\" \"chez/fuse/mount\"\n \"chez/fuse/codec\" \"chez/fuse/secmem\" \"chez/fuse/access\"\n \"chez/vault/crypto\" \"chez/vault/blockstore\"\n \"chez/fuse\" \"chez/vault\")))\n '())\n ;; jsh modules\n (existing-sos \"src/jsh\"\n '(\"ffi\" \"embed-data\" \"embed\"\n \"pregexp-compat\" \"stage\" \"static-compat\"\n \"conditions\" \"ast\" \"registry\" \"macros\" \"util\"\n \"lexer\" \"arithmetic\" \"glob\" \"fuzzy\" \"history\"\n \"recording-index\" \"recorder\" \"player\"\n \"environment\"\n \"parser\" \"functions\" \"signals\" \"expander\"\n \"redirect\" \"control\" \"jobs\" \"builtins\"\n \"pipeline\" \"executor\" \"completion\" \"prompt\" \"procwatch\"\n \"mux-proto\" \"mux-auth\" \"vt\" \"mux-session\" \"mux-screen\"\n \"mux-transport\" \"mux-relay\" \"mux-router\"\n \"mux-server\" \"mux-client\"\n \"aws\"\n \"worm\"\n \"pass\"\n \"lineedit\" \"fzf\" \"script\" \"config\" \"startup\" \"sandbox\"\n \"rl\" \"limits\" \"harden\" \"main\"\n \"coreutils\"))))\n\n;; ========== Step 4: Generate C headers with embedded data ==========\n\n(printf \"[4/7] Embedding boot files + program as C headers...~n\")\n(file->c-header program-so \"jsh_program.h\"\n \"jsh_program_data\" \"jsh_program_data_len\")\n(file->c-header petite-boot-path \"jsh_petite_boot.h\"\n \"petite_boot_data\" \"petite_boot_size\")\n(file->c-header scheme-boot-path \"jsh_scheme_boot.h\"\n \"scheme_boot_data\" \"scheme_boot_size\")\n(file->c-header \"jsh.boot\" \"jsh_jsh_boot.h\"\n \"jsh_boot_data\" \"jsh_boot_size\")\n\n;; ========== Step 5: Generate static_boot.c and main C ==========\n\n(printf \"[5/7] Generating C source files...~n\")\n\n(define build-dir\n (format \"~a/jsh-android-build\" (or (getenv \"TMPDIR\") \"/tmp\")))\n(system (format \"rm -rf '~a' && mkdir -p '~a'\" build-dir build-dir))\n\n(define static-boot-c (format \"~a/static_boot.c\" build-dir))\n(define program-c (format \"~a/jsh_main_android.c\" build-dir))\n\n(define gcc \"cc\")\n(define harden-cflags \"-fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2\")\n\n;; static_boot.c — registers embedded boot files with Chez\n(call-with-output-file static-boot-c\n (lambda (out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"jsh_petite_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_scheme_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_jsh_boot.h\\\"\\n\" out)\n (display \"\\nvoid static_boot_init(void) {\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", (void *)petite_boot_data, petite_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", (void *)scheme_boot_data, scheme_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh\\\", (void *)jsh_boot_data, jsh_boot_size);\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; jsh_main_android.c — main entry point\n(call-with-output-file program-c\n (lambda (out)\n (display \"#define _GNU_SOURCE\\n\" out)\n (display \"#include <stdio.h>\\n#include <stdlib.h>\\n#include <string.h>\\n\" out)\n (display \"#include <unistd.h>\\n#include <fcntl.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"jsh_program.h\\\"\\n\\n\" out)\n (display \"extern void static_boot_init(void);\\n\" out)\n (display \"extern void register_ffi_symbols(void);\\n\\n\" out)\n\n ;; Forward declarations for FFI\n (display \"/* FFI forward declarations */\\n\" out)\n (display \"extern void ffi_ensure_std_fds(void);\\n\\n\" out)\n\n ;; register_ffi_symbols — pre-register all FFI symbols for static builds\n (display \"void register_ffi_symbols(void) {\\n\" out)\n\n ;; Android/Termux: not a fully static build — symbols resolved via dlopen at runtime.\n ;; We don't need to pre-register POSIX or FFI symbols since the binary is dynamically linked.\n ;; Only register symbols that the Scheme code looks up by name via foreign-procedure.\n (display \" /* Android: dynamically linked — most symbols resolved via dlopen */\\n\" out)\n (display \"}\\n\\n\" out)\n\n ;; main\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" /* Tell jerboa stdlib libraries (std/net/tcp, std/net/udp, std/net/io,\\n\" out)\n (display \" * std/os/epoll-native, etc.) that we are statically linked. Without this,\\n\" out)\n (display \" * library visit-time top-level code calls (load-shared-object #f), which\\n\" out)\n (display \" * raises \\\"not supported\\\" in a static binary and breaks lazy imports such\\n\" out)\n (display \" * as (std net request) -> (std net tcp). MUST be set before Sscheme_init. */\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; Save args as env vars\n (display \" char buf[32];\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", buf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" snprintf(buf, sizeof(buf), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(buf, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Resolve exe path via /proc/self/exe\n (display \" {\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" ssize_t len = readlink(\\\"/proc/self/exe\\\", exe_buf, sizeof(exe_buf) - 1);\\n\" out)\n (display \" if (len > 0) { exe_buf[len] = '\\\\0'; setenv(\\\"JSH_EXE\\\", exe_buf, 1); }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Chez init\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" static_boot_init();\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n ;; Load program via temp file (memfd /proc/self/fd paths are blocked by SELinux on Android)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\");\\n\" out)\n (display \" if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" char prog_path[4096];\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-prog-XXXXXX\\\", tmpdir);\\n\" out)\n (display \" int fd = mkstemp(prog_path);\\n\" out)\n (display \" if (fd < 0) { perror(\\\"mkstemp\\\"); return 1; }\\n\" out)\n (display \" if (write(fd, jsh_program_data, jsh_program_data_len) != (ssize_t)jsh_program_data_len) {\\n\" out)\n (display \" perror(\\\"write tmpfile\\\"); close(fd); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" close(fd);\\n\\n\" out)\n (display \" const char *script_args[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_script(prog_path, 1, script_args);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n 'replace)\n\n;; Copy C headers to build dir\n(system (format \"cp jsh_program.h jsh_petite_boot.h jsh_scheme_boot.h jsh_jsh_boot.h '~a/'\" build-dir))\n\n;; Generate Android compat header (explicit_bzero not in Bionic's <string.h>)\n(call-with-output-file (format \"~a/android-compat.h\" build-dir)\n (lambda (out)\n (display \"#ifndef ANDROID_COMPAT_H\\n#define ANDROID_COMPAT_H\\n\" out)\n (display \"#ifndef _GNU_SOURCE\\n#define _GNU_SOURCE\\n#endif\\n\" out)\n (display \"#include <string.h>\\n\" out)\n ;; explicit_bzero is not in Bionic's <string.h> on Termux —\n ;; provide an inline fallback unconditionally when missing.\n (display \"#if defined(__ANDROID__)\\n\" out)\n (display \"#include <stddef.h>\\n\" out)\n (display \"static inline void explicit_bzero(void *b, size_t l) {\\n\" out)\n (display \" memset(b, 0, l);\\n\" out)\n (display \" __asm__ __volatile__(\\\"\\\" ::: \\\"memory\\\");\\n\" out)\n (display \"}\\n\" out)\n (display \"#endif\\n\" out)\n (display \"#endif\\n\" out)))\n 'replace)\n\n(define android-compat (format \"-include '~a/android-compat.h'\" build-dir))\n\n;; ========== Step 6: Compile C with clang ==========\n\n(printf \"[6/7] Compiling C with clang...~n\")\n\n;; static_boot.c\n(run-cmd (format \"~a -c -O2 ~a ~a -I'~a' -o '~a/static_boot.o' '~a'\"\n gcc harden-cflags android-compat scheme-h-dir build-dir static-boot-c))\n\n;; jsh_main_android.c\n(run-cmd (format \"~a -c -O2 ~a ~a -I'~a' -o '~a/jsh_main_android.o' '~a'\"\n gcc harden-cflags android-compat scheme-h-dir build-dir program-c))\n\n;; ffi-shim.c\n(run-cmd (format \"~a -c -O2 ~a ~a -o '~a/ffi-shim.o' ffi-shim.c -Wall\"\n gcc harden-cflags android-compat build-dir))\n\n;; embed-crypto.c was hand-rolled C (ChaCha20-Poly1305 / PBKDF2 / SHA-256).\n;; W-1 / L-1: the same symbols (embed_pbkdf2_sha256, embed_encrypt,\n;; embed_decrypt, embed_random_bytes, embed_read_passphrase) now come\n;; from libjerboa_native.a (ring-backed). Emit an empty .o so the\n;; linker picks up the Rust definitions without duplicate-symbol noise.\n(printf \" [skip] embed-crypto.c — symbols provided by libjerboa_native.a~n\")\n(system (format \"echo '' | ~a -c -x c -o '~a/embed-crypto.o' -\" gcc build-dir))\n\n;; Landlock: ffi_landlock_* provided by ffi-shim.c (returns -1 if syscall unavailable)\n;; jerboa_landlock_* provided by libjerboa_native.a\n;; No separate shim needed.\n\n;; jerboa-ssh shim\n;; -DCHEZ_SSH_NO_OPENSSL on jerboa_ssh_shim.c: use standalone ed25519 from Rust\n;; (libjerboa_native.a provides ed25519_*_standalone symbols)\n;; jerboa_ssh_crypto.c is compiled separately as jerboa-ssh-crypto.o; it still uses\n;; OpenSSL EVP for SSH transport crypto (HMAC/SHA/X25519/ChaCha20-Poly1305).\n;; Termux ships libcrypto.so/libssl.so so we link against those system libs.\n(if (file-exists? jerboa-ssh-shim)\n (begin\n ;; -DCHEZ_SSH_NO_OPENSSL: use standalone ed25519/AES from Rust, not OpenSSL\n (run-cmd (format \"~a -c -O2 ~a ~a -DCHEZ_SSH_NO_OPENSSL -I'~a/jerboa-ssh' -o '~a/jerboa-ssh-shim.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir build-dir jerboa-ssh-shim))\n ;; ed25519-standalone — provided by Rust libjerboa_native.a (ed25519-dalek)\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (let ([crypto-src (format \"~a/jerboa-ssh/jerboa_ssh_crypto.c\" vendor-dir)])\n (if (file-exists? crypto-src)\n ;; -I. for embed-crypto.h (project root)\n (run-cmd (format \"~a -c -O2 ~a ~a -I'~a/jerboa-ssh' -I'~a' -o '~a/ed25519-standalone.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir (current-directory) build-dir crypto-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))))\n (let ([bcrypt-src (format \"~a/jerboa-ssh/bcrypt_pbkdf.c\" vendor-dir)])\n (if (file-exists? bcrypt-src)\n (run-cmd (format \"~a -c -O2 ~a ~a -I'~a/jerboa-ssh' -o '~a/bcrypt_pbkdf.o' '~a' -Wall\"\n gcc harden-cflags android-compat vendor-dir build-dir bcrypt-src))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir)))))\n (begin\n (printf \" Warning: jerboa-ssh shim not found, building without SSH agent~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-shim.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-ssh-crypto.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/ed25519-standalone.o' -\" gcc build-dir))\n (system (format \"echo '' | ~a -c -x c -o '~a/bcrypt_pbkdf.o' -\" gcc build-dir))))\n\n;; jerboa-crypto shim\n(if (file-exists? jerboa-crypto-shim)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/jerboa-crypto-shim.o' '~a' -Wall\"\n gcc harden-cflags build-dir jerboa-crypto-shim))\n (begin\n (printf \" Warning: jerboa-crypto shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/jerboa-crypto-shim.o' -\" gcc build-dir))))\n\n;; coreutils FFI shim\n(let ([cu-src (format \"~a/jerboa-coreutils/support/libcoreutils.c\" vendor-dir)])\n (if (file-exists? cu-src)\n (run-cmd (format \"~a -c -O2 ~a -o '~a/coreutils-ffi.o' '~a' -Wall\"\n gcc harden-cflags build-dir cu-src))\n (begin\n (printf \" Warning: coreutils FFI shim not found~n\")\n (system (format \"echo '' | ~a -c -x c -o '~a/coreutils-ffi.o' -\" gcc build-dir)))))\n\n;; ========== Step 7: Link binary ==========\n\n(printf \"[7/7] Linking jsh-android binary...~n\")\n\n(let* ([objs (format \"~a/jsh_main_android.o ~a/static_boot.o ~a/ffi-shim.o ~a/embed-crypto.o ~a/jerboa-ssh-shim.o ~a/jerboa-ssh-crypto.o ~a/ed25519-standalone.o ~a/bcrypt_pbkdf.o ~a/jerboa-crypto-shim.o ~a/coreutils-ffi.o\"\n build-dir build-dir build-dir build-dir\n build-dir build-dir build-dir build-dir build-dir build-dir)]\n ;; Rust static libs must be wrapped in --whole-archive so all symbols\n ;; are included (Chez resolves them at runtime via dlsym, not at link time)\n [native-flag (format \" -Wl,--whole-archive ~a -Wl,--no-whole-archive\" native-lib-path)]\n [coreutils-flag (format \" -Wl,--whole-archive ~a -Wl,--no-whole-archive\" rust-coreutils-lib-path)]\n ;; Android/Termux link: Chez static libs + shared Bionic + Rust deps\n ;; libssl removed — TLS now via jerboa_tls_* (rustls)\n ;; libcrypto: still needed by vendor/jerboa-ssh/jerboa_ssh_crypto.c and\n ;; vendor/jerboa-crypto/jerboa_crypto_shim.c (EVP_*, HMAC, scrypt).\n ;; The JERBOA_SSH_NO_OPENSSL fallback requires updated vendor sources which\n ;; live outside git tracking; link dynamically against Termux libcrypto.\n [link-libs (format \"-L~a -lkernel ~a/libz.a ~a/liblz4.a -lm -ldl -lpthread -lutil -liconv -lncursesw -lc++_shared -lunwind -lcrypto\"\n chez-tarm64le chez-tarm64le chez-tarm64le)]\n [link-cmd (format \"~a -pie ~a -rdynamic -o jsh-android ~a~a~a ~a -Wl,--allow-multiple-definition\"\n gcc harden-cflags objs native-flag coreutils-flag link-libs)])\n (run-cmd link-cmd))\n\n;; ========== Hardening ==========\n\n(when (file-exists? \"jsh-android\")\n (printf \"~n[harden] Stripping debug symbols (preserving dynamic symbols)...~n\")\n (let ([pre-size (file-length (open-file-input-port \"jsh-android\"))])\n (system \"strip --strip-debug jsh-android\")\n (let ([post-size (file-length (open-file-input-port \"jsh-android\"))])\n (printf \" Stripped: ~a → ~a bytes (~a% reduction)~n\"\n pre-size post-size\n (inexact->exact (round (* 100 (/ (- pre-size post-size) pre-size)))))))\n\n (printf \"[harden] Computing integrity hash...~n\")\n (let ([hash-file (format \"~a/_jsh_hash.txt\" (or (getenv \"TMPDIR\") \"/tmp\"))])\n (system (format \"sha256sum jsh-android | cut -d' ' -f1 | tr -d '\\\\n' > '~a'\" hash-file))\n (let ([hash-hex (call-with-input-file hash-file get-string-all)])\n (system (format \"rm -f '~a'\" hash-file))\n (printf \" SHA-256: ~a~n\" hash-hex)\n (when (= (string-length hash-hex) 64)\n (let ([bv (make-bytevector 32)])\n (do ([i 0 (+ i 1)])\n ((= i 32))\n (bytevector-u8-set! bv i\n (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))\n (let ([port (open-file-output-port \"jsh-android.sha256\" (file-options no-fail))])\n (put-bytevector port bv)\n (close-port port))\n (printf \" Wrote jsh-android.sha256 (32 bytes)~n\"))))))\n\n;; Cleanup\n(system (format \"rm -rf '~a'\" build-dir))\n(for-each (lambda (f) (when (file-exists? f) (delete-file f)))\n '(\"jsh_program.h\" \"jsh_petite_boot.h\" \"jsh_scheme_boot.h\" \"jsh_jsh_boot.h\"\n \"jsh.so\" \"jsh.wpo\" \"jsh.boot\"))\n\n;; Summary\n(printf \"~n========================================~n\")\n(printf \"Binary created: jsh-android~n~n\")\n(system \"ls -lh jsh-android\")\n(printf \"~n\")\n(system \"file jsh-android\")\n(printf \"~nTest: ./jsh-android -c 'echo Hello from jsh on Android'~n\")\n"} {"text":";; FILE: jerboa-shell/functions.ss\n;;; functions.ss — Shell functions and aliases for gsh\n\n(export #t)\n(import :std/sugar\n :std/format\n :std/iter\n :jsh/ast\n :jsh/environment\n :jsh/ffi)\n\n;;; --- Shell functions ---\n\n(defstruct shell-function (name body redirections lineno source-file) transparent: #t)\n\n;; Define a shell function\n(def (function-define! env name body (redirections []) (lineno #f) (source-file #f))\n (hash-put! (shell-environment-functions env) name\n (make-shell-function name body redirections lineno source-file)))\n\n;; Look up a function by name\n(def (function-lookup env name)\n (hash-get (shell-environment-functions env) name))\n\n;; Unset a function\n(def (function-unset! env name)\n (hash-remove! (shell-environment-functions env) name))\n\n;; List all function names\n(def (function-list env)\n (hash-keys (shell-environment-functions env)))\n\n;; Call a function with arguments\n;; execute-fn is the executor callback to avoid circular dependency\n;; Returns exit status\n(def (function-call func args env execute-fn)\n (let* ((child-env (env-push-scope env))\n ;; Set positional parameters\n (_ (env-set-positional! child-env args))\n ;; Set FUNCNAME\n (_ (env-set! child-env \"FUNCNAME\" (shell-function-name func))))\n ;; Execute the function body\n ;; break/continue/return are handled by the caller\n (let ((status\n (with-catch\n (lambda (e)\n (if (return-exception? e)\n (return-exception-status e)\n (raise e)))\n (lambda ()\n (let ((s (execute-fn (shell-function-body func) child-env)))\n ;; Copy last-status back to parent\n (env-set-last-status! env s)\n s)))))\n ;; Clean up exported locals: when a local variable was exported in the\n ;; child scope, restore the OS environment to match the parent scope\n (cleanup-exported-locals! child-env env)\n status)))\n\n;; After function return, restore OS environment for variables that were\n;; local+exported in the child scope but shouldn't persist in parent\n(def (cleanup-exported-locals! child-env parent-env)\n (for-each\n (lambda (pair)\n (let ((name (car pair))\n (var (cdr pair)))\n (when (and (shell-var-local? var) (shell-var-exported? var))\n ;; Check if parent scope has this variable\n (let ((parent-var (env-get-raw-var parent-env name)))\n (if (and parent-var (shell-var-exported? parent-var))\n ;; Parent has it exported — restore parent's value\n (let ((v (shell-var-scalar-value parent-var)))\n (if v (setenv name v) (ffi-unsetenv name)))\n ;; Parent doesn't have it exported — remove from OS env\n (ffi-unsetenv name))))))\n (hash->list (shell-environment-vars child-env))))\n\n;;; --- Return exception ---\n;; Used to implement 'return' from functions\n\n(defstruct return-exception (status) transparent: #t)\n\n(def (shell-return! (status 0))\n (raise (make-return-exception status)))\n\n;;; --- Break/Continue exceptions ---\n;; Used to implement 'break' and 'continue' in loops\n\n(defstruct break-exception (levels) transparent: #t)\n(defstruct continue-exception (levels) transparent: #t)\n\n;; Track loop nesting depth — break/continue are only valid inside loops\n(def *loop-depth* (make-parameter 0))\n\n(def (shell-break! (levels 1))\n (if (> (*loop-depth*) 0)\n (raise (make-break-exception levels))\n (begin\n (fprintf (current-error-port) \"break: only meaningful in a `for', `while', or `until' loop~n\")\n ;; In bash, break/continue outside a loop just warns (doesn't abort subshell)\n 0)))\n\n(def (shell-continue! (levels 1))\n (if (> (*loop-depth*) 0)\n (raise (make-continue-exception levels))\n (begin\n (fprintf (current-error-port) \"continue: only meaningful in a `for', `while', or `until' loop~n\")\n ;; In bash, break/continue outside a loop just warns (doesn't abort subshell)\n 0)))\n\n;;; --- Errexit exception ---\n;; Raised when set -e is active and a command fails outside a condition context\n\n(defstruct errexit-exception (status) transparent: #t)\n\n;;; --- Nounset exception ---\n;; Raised when set -u is active and an unbound variable is referenced\n\n(defstruct nounset-exception (status) transparent: #t)\n\n;;; --- Subshell exit exception ---\n;; Raised by `exit` builtin when running inside a subshell\n\n(defstruct subshell-exit-exception (status) transparent: #t)\n\n;;; --- Aliases ---\n\n;; Set an alias\n(def (alias-set! env name value)\n (hash-put! (shell-environment-aliases env) name value))\n\n;; Get an alias value\n(def (alias-get env name)\n (hash-get (shell-environment-aliases env) name))\n\n;; Remove an alias\n(def (alias-unset! env name)\n (hash-remove! (shell-environment-aliases env) name))\n\n;; Remove all aliases\n(def (alias-clear! env)\n ;; Replace with new empty table\n (for-each\n (lambda (pair)\n (hash-remove! (shell-environment-aliases env) (car pair)))\n (hash->list (shell-environment-aliases env))))\n\n;; List all aliases as alist\n(def (alias-list env)\n (hash->list (shell-environment-aliases env)))\n\n;; Expand aliases in a word (first word of simple command)\n;; Returns expanded string or #f if no alias\n(def (alias-expand env word)\n (let ((val (alias-get env word)))\n (if val\n ;; If alias ends with space, the next word should also be checked\n val\n #f)))\n\n;; Check if alias value ends with space (triggers next-word expansion)\n(def (alias-continues? value)\n (and (> (string-length value) 0)\n (char=? (string-ref value (- (string-length value) 1)) #\\space)))\n"} {"text":";; FILE: jerboa-shell/build-jsh-freebsd-cross.ss\n#!chezscheme\n;;; build-jsh-freebsd-cross.ss — Cross-compile jsh from macOS arm64 to FreeBSD 14 amd64\n;;;\n;;; Usage:\n;;; JERBOA_HOME=/Users/user/mine/jerboa scheme --libdirs <libs> \\\n;;; --script build-jsh-freebsd-cross.ss\n;;;\n;;; Sibling to build-jsh-cross.ss (Linux x86_64 musl). Uses:\n;;; - $JERBOA_HOME/.chez-cross-ta6fb/ — cross-built Chez install for FreeBSD\n;;; - $JERBOA_HOME/build/chez/xc-ta6fb/s/xpatch — host compiler → ta6fb emit\n;;; - $JERBOA_HOME/support/cross-cc-freebsd-amd64 — macOS clang+lld wrapper\n;;;\n;;; Produces: jsh-freebsd-amd64 (dynamic FreeBSD x86_64 ELF, depends on\n;;; libc.so.7, libm.so, libthr.so, libutil.so from the host system)\n;;;\n;;; WPO-as-program pattern, same as build-jsh-cross.ss.\n\n(import (chezscheme))\n\n;; ── Params ──────────────────────────────────────────────────────────────────\n(define jerboa-home\n (or (getenv \"JERBOA_HOME\") \"/Users/user/mine/jerboa\"))\n\n(define cross-prefix (format \"~a/.chez-cross-ta6fb\" jerboa-home))\n(define xpatch (format \"~a/build/chez/xc-ta6fb/s/xpatch\" jerboa-home))\n(define cross-cc (or (getenv \"CROSS_CC\")\n (format \"~a/support/cross-cc-freebsd-amd64\" jerboa-home)))\n\n(define output \"jsh-freebsd-amd64\")\n(define source-script \"jsh.ss\")\n(define entry-script \"jsh-generated.ss\")\n(define ffi-shim \"ffi-shim.c\")\n\n;; Feature resolution — derive *enabled-features* from JSH_FEATURES env var.\n;; \"\"/\"none\" → '() (minimal build)\n;; \"all\" → all known optional features\n;; \"foo,bar\" → '(foo bar)\n(define *enabled-features*\n (let ([env (or (getenv \"JSH_FEATURES\") \"\")])\n (cond\n [(or (string=? env \"\") (string=? env \"none\")) '()]\n [(string=? env \"all\")\n '(coreutils mux ssh aws worm vault record sandbox cage rl profiler proxy procwatch embed pass)]\n [else\n (let split ([i 0] [start 0] [acc '()])\n (cond\n [(= i (string-length env))\n (let ([s (substring env start i)])\n (if (string=? s \"\") (reverse acc)\n (reverse (cons (string->symbol s) acc))))]\n [(char=? (string-ref env i) #\\,)\n (let ([s (substring env start i)])\n (split (+ i 1) (+ i 1)\n (if (string=? s \"\") acc (cons (string->symbol s) acc))))]\n [else (split (+ i 1) start acc)]))])))\n\n;; jerboa-native Rust static lib — FreeBSD cross-built.\n(define jerboa-native-a\n (or (getenv \"JERBOA_NATIVE_A\")\n (format \"~a/jerboa-native-rs/target/x86_64-unknown-freebsd/release/libjerboa_native.a\"\n jerboa-home)))\n\n(define cross-csv-dir\n (let ([lib (format \"~a/lib\" cross-prefix)])\n (unless (file-directory? lib)\n (error 'build-jsh-freebsd-cross \"cross prefix lib dir missing\" lib))\n (let* ([entries (directory-list lib)]\n [csvs (filter (lambda (e)\n (and (>= (string-length e) 3)\n (string=? (substring e 0 3) \"csv\")))\n entries)])\n (when (null? csvs)\n (error 'build-jsh-freebsd-cross \"no csv* in cross lib\" lib))\n (format \"~a/~a/ta6fb\" lib (car csvs)))))\n\n(define (require-file p)\n (unless (file-exists? p)\n (error 'build-jsh-freebsd-cross \"missing file\" p)))\n\n(require-file xpatch)\n(require-file (format \"~a/libkernel.a\" cross-csv-dir))\n(require-file (format \"~a/scheme.h\" cross-csv-dir))\n(require-file (format \"~a/petite.boot\" cross-csv-dir))\n(require-file (format \"~a/scheme.boot\" cross-csv-dir))\n(require-file source-script)\n(require-file ffi-shim)\n(require-file jerboa-native-a)\n\n(printf \"==> build-jsh-freebsd-cross~n\")\n(printf \" JERBOA_HOME: ~a~n\" jerboa-home)\n(printf \" cross csv-dir: ~a~n\" cross-csv-dir)\n(printf \" xpatch: ~a~n\" xpatch)\n(printf \" cross-cc: ~a~n\" cross-cc)\n(printf \" output: ~a~n\" output)\n(printf \"~n\")\n\n;; ── Stage 1: load xpatch (target=ta6fb emit mode) ──────────────────────────\n(define orig-libdirs (library-directories))\n(printf \"==> [1/6] loading xpatch (compiler -> ta6fb emit mode)~n\")\n(load xpatch)\n(library-directories orig-libdirs)\n\n(compile-imported-libraries #t)\n(generate-wpo-files #t)\n\n;; ── Stage 1.5: apply vendor patches ────────────────────────────────────────\n;; allow-proxy.ss: the vendored HTTP CONNECT proxy had a thread-unsafe\n;; port-eof? polling loop in `tunnel` that mutated Chez ports concurrently\n;; (peek = mutate), corrupting TLS bytes (\"wrong version number\"). The\n;; patched copy uses mutex-guarded done flags. vendor/ is gitignored &\n;; re-cloned, so overlay patches/allow-proxy.ss over both .ss and .sls and\n;; wipe stale .so/.wpo BEFORE compile-program so the WPO pulls the patched\n;; source (allow-proxy is imported transitively, not via libs-to-bundle).\n(let ([ap-patch (format \"~a/patches/allow-proxy.ss\" (current-directory))]\n [ap-ss \"vendor/jerboa/lib/std/net/allow-proxy.ss\"]\n [ap-sls \"vendor/jerboa/lib/std/net/allow-proxy.sls\"]\n [ap-so \"vendor/jerboa/lib/std/net/allow-proxy.so\"]\n [ap-wpo \"vendor/jerboa/lib/std/net/allow-proxy.wpo\"])\n (when (file-exists? ap-patch)\n (system (format \"cp '~a' '~a'\" ap-patch ap-ss))\n (system (format \"cp '~a' '~a'\" ap-patch ap-sls))\n (system (format \"rm -f '~a' '~a'\" ap-so ap-wpo))\n (printf \"==> [1.5/6] applied patches/allow-proxy.ss -> std/net/allow-proxy.{ss,sls}~n\")))\n\n;; ── Stage 2: generate jsh-generated.ss with feature manifest ───────────────\n;; jsh.ss carries a placeholder `(define *jsh-enabled-features* '())`.\n;; jsh-generate.ss rewrites that line based on *enabled-features* so the\n;; runtime `,features` command reports what was actually built.\n(printf \"==> [2/6] generate-jsh-program (features: ~a)~n\"\n (if (null? *enabled-features*) \"minimal\" \"full\"))\n(load \"features.def\")\n(load \"jsh-generate.ss\")\n(generate-jsh-program *enabled-features*)\n\n(printf \"==> [2.5/6] compile-program ~a~n\" entry-script)\n(compile-program entry-script)\n\n;; ── Stage 3: compile-whole-program → wpo .so ───────────────────────────────\n(define wpo-output (string-append output \".wp.so\"))\n(printf \"==> [3/6] compile-whole-program jsh-generated.wpo -> ~a~n\" wpo-output)\n(compile-whole-program \"jsh-generated.wpo\" wpo-output #t)\n\n;; ── Stage 3.5: ensure lazy-imported libs are compiled, build jsh-libs.boot ─\n(define jerboa-lib-dir \"vendor/jerboa/lib\")\n(define libs-to-bundle\n '(\"jerboa/core\"\n \"jerboa/runtime\"\n \"std/error/conditions\"\n \"std/error\"\n \"std/format\"\n \"std/sort\"\n \"std/pregexp\"\n \"std/sugar\"\n \"std/misc/alist\"\n \"std/misc/string\"\n \"std/misc/string-more\"\n \"std/misc/list\"\n \"std/misc/lru-cache\"\n \"std/misc/trie\"\n \"std/misc/thread\"\n \"std/text/glob\"\n \"std/os/path\"\n \"std/os/platform\"\n \"std/os/posix\"\n \"std/os/limits\"\n \"std/os/supervise\"\n \"std/os/limits/sandbox\"\n \"std/os/tracefs\"\n \"std/net/allowlist\"))\n\n(printf \"==> [3.5/6] fill in missing .so files for boot bundle~n\")\n(for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" jerboa-lib-dir m)]\n [so (format \"~a/~a.so\" jerboa-lib-dir m)])\n (cond\n [(file-exists? so)\n (printf \" keep ~a~n\" so)]\n [(file-exists? sls)\n (printf \" compile-library ~a (missing .so)~n\" sls)\n (compile-library sls)]\n [else\n (printf \" skip (no .sls): ~a~n\" sls)])))\n libs-to-bundle)\n\n(define libs-boot \"jsh-libs.boot\")\n(define libs-boot-inputs\n (let loop ([ms libs-to-bundle] [acc '()])\n (cond\n [(null? ms) (reverse acc)]\n [else\n (let ([so (format \"~a/~a.so\" jerboa-lib-dir (car ms))])\n (loop (cdr ms) (if (file-exists? so) (cons so acc) acc)))])))\n(printf \" make-boot-file ~a (~a libs)~n\" libs-boot (length libs-boot-inputs))\n(apply make-boot-file libs-boot '(\"petite\" \"scheme\") libs-boot-inputs)\n\n;; ── Stage 4: write helpers ─────────────────────────────────────────────────\n(define (embed-as-c-array in-path var-name out-path)\n (let* ([bv (call-with-port (open-file-input-port in-path) get-bytevector-all)]\n [n (bytevector-length bv)])\n (call-with-port (open-file-output-port out-path\n (file-options no-fail)\n (buffer-mode block)\n (native-transcoder))\n (lambda (out)\n (display (format \"static const unsigned char ~a[] = {\\n\" var-name) out)\n (let loop ([i 0])\n (when (< i n)\n (display (format \"0x~2,'0x,\" (bytevector-u8-ref bv i)) out)\n (when (= (mod (+ i 1) 16) 0) (newline out))\n (loop (+ i 1))))\n (when (positive? n) (newline out))\n (display \"};\\n\" out)\n (display (format \"static const unsigned int ~a_size = sizeof(~a);\\n\"\n var-name var-name)\n out)))\n (printf \" embed ~a (~a bytes) -> ~a~n\" in-path n out-path)))\n\n(printf \"==> [4/6] embed boot files + program as C arrays~n\")\n(embed-as-c-array (format \"~a/petite.boot\" cross-csv-dir) \"petite_boot\" \"petite_boot.h\")\n(embed-as-c-array (format \"~a/scheme.boot\" cross-csv-dir) \"scheme_boot\" \"scheme_boot.h\")\n(embed-as-c-array libs-boot \"jsh_libs_boot\" \"jsh_libs_boot.h\")\n(embed-as-c-array wpo-output \"jsh_program\" \"jsh_program.h\")\n\n;; ── Stage 5: generate main.c (FreeBSD flavor) ──────────────────────────────\n(define main-c-path (string-append output \"-main.c\"))\n\n(define (read-symbol-list path)\n (call-with-input-file path\n (lambda (p)\n (let loop ([acc '()])\n (let ([line (get-line p)])\n (cond\n [(eof-object? line) (reverse acc)]\n [(or (zero? (string-length line))\n (char=? (string-ref line 0) #\\;)\n (char=? (string-ref line 0) #\\#))\n (loop acc)]\n [else (loop (cons line acc))]))))))\n\n(define ffi-shim-symbols\n (read-symbol-list \"ffi-shim-symbols.list\"))\n\n;; libjerboa_native.a exports for x86_64-unknown-freebsd.\n;; Differs from the Linux list — Linux-only symbols (epoll, inotify, eventfd,\n;; landlock, seccomp) are not built on FreeBSD and are emitted as C stubs\n;; in freebsd-stub-symbols below.\n(define jerboa-native-symbols\n '(\"jerboa_aead_open\" \"jerboa_aead_seal\"\n \"jerboa_antidebug_check_all\" \"jerboa_antidebug_check_breakpoint\"\n \"jerboa_antidebug_check_ld_preload\" \"jerboa_antidebug_check_tracer\"\n \"jerboa_antidebug_ptrace\" \"jerboa_antidebug_timing_check\"\n \"jerboa_aproc_close\" \"jerboa_aproc_dup\" \"jerboa_aproc_killpg\"\n \"jerboa_aproc_set_nonblock\" \"jerboa_aproc_spawn\" \"jerboa_aproc_spawn_pty\"\n \"jerboa_aproc_wait4\"\n \"jerboa_argon2id_hash\" \"jerboa_argon2id_verify\"\n \"jerboa_chacha20_open\" \"jerboa_chacha20_seal\"\n \"jerboa_deflate\" \"jerboa_inflate\" \"jerboa_gzip\" \"jerboa_gunzip\"\n \"jerboa_freebsd_is_traced\" \"jerboa_freebsd_process_count\"\n \"jerboa_hkdf_sha256\" \"jerboa_hmac_sha256\" \"jerboa_hmac_sha256_verify\"\n \"jerboa_integrity_hash_file\" \"jerboa_integrity_hash_region\"\n \"jerboa_integrity_hash_self\" \"jerboa_integrity_sign_verify\"\n \"jerboa_integrity_verify_hash\"\n \"jerboa_kill_probe\"\n \"jerboa_last_error\" \"jerboa_md5\" \"jerboa_mlockall\"\n \"jerboa_pbkdf2_derive\" \"jerboa_pbkdf2_verify\"\n \"jerboa_prctl_set_name\" \"jerboa_proc_self_exe\"\n \"jerboa_random_bytes\"\n \"jerboa_regex_captures\" \"jerboa_regex_compile\" \"jerboa_regex_compile_ex\"\n \"jerboa_regex_find\" \"jerboa_regex_find_at\" \"jerboa_regex_free\"\n \"jerboa_regex_group_count\" \"jerboa_regex_is_match\" \"jerboa_regex_replace_all\"\n \"jerboa_scrypt\"\n \"jerboa_secure_alloc\" \"jerboa_secure_free\" \"jerboa_secure_random_fill\"\n \"jerboa_secure_wipe\"\n \"jerboa_setproctitle\"\n \"jerboa_sha1\" \"jerboa_sha256\" \"jerboa_sha384\" \"jerboa_sha512\"\n \"jerboa_socks5_server_port\" \"jerboa_socks5_server_start\"\n \"jerboa_socks5_server_stats\" \"jerboa_socks5_server_stop\"\n \"jerboa_timing_safe_equal\"\n \"jerboa_x25519_diffie_hellman\" \"jerboa_x25519_generate_keypair\"\n \"jerboa_x25519_public_from_private\"))\n\n;; Linux-only jerboa_* symbols — emit returning-error C stubs on FreeBSD.\n;; Mirrors build-jsh-freebsd.ss's pattern.\n(define freebsd-stub-symbols\n '(\"jerboa_epoll_create\" \"jerboa_epoll_ctl\" \"jerboa_epoll_wait\" \"jerboa_epoll_close\"\n \"jerboa_eventfd_create\" \"jerboa_eventfd_drain\" \"jerboa_eventfd_signal\"\n \"jerboa_inotify_init\" \"jerboa_inotify_add_watch\" \"jerboa_inotify_rm_watch\"\n \"jerboa_inotify_read\" \"jerboa_inotify_close\"\n \"jerboa_landlock_abi_version\" \"jerboa_landlock_create_ruleset\"\n \"jerboa_landlock_add_path_rule\" \"jerboa_landlock_add_net_rule\"\n \"jerboa_landlock_enforce\"\n \"jerboa_seccomp_available\" \"jerboa_seccomp_lock\" \"jerboa_seccomp_lock_strict\"))\n\n;; POSIX libc functions called directly via foreign-procedure (no shim).\n;; Note: __errno_location is Linux glibc — replaced by freebsd_errno_location\n;; wrapper below (which also satisfies FreeBSD's __error).\n(define posix-symbols\n '(\"fork\" \"_exit\" \"close\" \"dup\" \"dup2\" \"read\" \"write\" \"lseek\" \"access\"\n \"unlink\" \"getpid\" \"getppid\" \"kill\" \"sysconf\" \"waitpid\"\n \"setpgid\" \"getpgid\" \"tcsetpgrp\" \"tcgetpgrp\" \"setsid\"\n \"getuid\" \"geteuid\" \"getegid\" \"isatty\" \"unsetenv\"\n \"chdir\" \"chmod\" \"chown\" \"chroot\" \"getgid\" \"gethostid\"\n \"lchown\" \"link\" \"lstat\" \"nice\" \"rename\" \"rmdir\"\n \"signal\" \"symlink\" \"time\" \"truncate\" \"utime\"\n \"setpriority\"\n \"socket\" \"bind\" \"setsockopt\" \"getsockname\"\n \"htons\" \"inet_pton\"\n \"listen\" \"accept\" \"connect\"\n \"flock\" \"fsync\" \"ftruncate\" \"getcwd\" \"getpagesize\"\n \"mmap\" \"mprotect\" \"munmap\" \"msync\" \"madvise\"\n \"pread\" \"pwrite\" \"readlink\" \"realpath\" \"strerror\"\n \"usleep\" \"sleep\" \"nanosleep\" \"mkstemp\" \"mkdtemp\" \"fdopen\"))\n\n(define posix-wrapped-symbols '(\"open\" \"fcntl\" \"mkfifo\" \"umask\" \"mkdir\"))\n\n;; Same weak-stub list as the Linux cross — symbols Scheme may dlsym at runtime\n;; but that are not statically linked into this build.\n(define weak-stub-symbols\n '(\"jerboa_ssh_agent_load_openssh_key\" \"jerboa_ssh_agent_load_ed25519\"\n \"jerboa_ssh_key_is_encrypted\"\n \"jerboa_ssh_agent_load_openssh_key_with_pass\"\n \"jerboa_ssh_agent_load_key_prompted\"\n \"jerboa_ssh_agent_key_count\"\n \"jerboa_ssh_agent_get_pubkey_blob\" \"jerboa_ssh_agent_get_comment\"\n \"jerboa_ssh_agent_get_seed\" \"jerboa_ssh_agent_get_dir\"\n \"jerboa_ssh_agent_remove_key\" \"jerboa_ssh_agent_remove_all\"\n \"jerboa_ssh_agent_start\" \"jerboa_ssh_agent_get_socket_path\"\n \"jerboa_ssh_agent_is_running\" \"jerboa_ssh_agent_stop\"\n \"jerboa_ssl_init\" \"jerboa_ssl_cleanup\"\n \"jerboa_ssl_connect\" \"jerboa_ssl_write\" \"jerboa_ssl_read\"\n \"jerboa_ssl_read_all\" \"jerboa_ssl_free_buf\" \"jerboa_ssl_close\"\n \"jerboa_ssl_memcpy\"\n \"jerboa_tcp_listen\" \"jerboa_tcp_accept\"\n \"jerboa_tcp_connect\" \"jerboa_tcp_close\"\n \"jerboa_tcp_read\" \"jerboa_tcp_write\" \"jerboa_tcp_read_all\"\n \"jerboa_tcp_set_timeout\"\n \"jerboa_ssl_server_ctx\" \"jerboa_ssl_server_accept\" \"jerboa_ssl_server_ctx_free\"\n \"jerboa_tcp_conn_wrap\" \"jerboa_conn_write\" \"jerboa_conn_read\"\n \"jerboa_fuse_secmem_alloc\" \"jerboa_fuse_secmem_free\" \"jerboa_fuse_secmem_zero\"\n \"jerboa_fuse_secmem_copy_in\" \"jerboa_fuse_secmem_copy_out\"\n \"jerboa_fuse_getpid\" \"jerboa_fuse_getppid_of\"\n \"jerboa_fuse_open_device\" \"jerboa_fuse_get_errno\"\n \"jerboa_fuse_block_signal\" \"jerboa_fuse_unblock_signal\"\n \"jerboa_fuse_mount\" \"jerboa_fuse_unmount\" \"jerboa_fuse_unmount_lazy\"\n \"jsh_coreutils_init\"\n \"jsh_ls\" \"jsh_dir\" \"jsh_vdir\" \"jsh_stat\" \"jsh_du\" \"jsh_df\"\n \"jsh_dircolors\" \"jsh_pathchk\"\n \"jsh_cat\" \"jsh_cp\" \"jsh_mv\" \"jsh_rm\" \"jsh_ln\"\n \"jsh_mkdir\" \"jsh_rmdir\" \"jsh_mktemp\" \"jsh_touch\"\n \"jsh_link\" \"jsh_unlink\" \"jsh_readlink\" \"jsh_cu_realpath\"\n \"jsh_install\" \"jsh_shred\" \"jsh_truncate\" \"jsh_mkfifo\" \"jsh_mknod\" \"jsh_dd\"\n \"jsh_chmod\" \"jsh_chown\" \"jsh_chgrp\"\n \"jsh_head\" \"jsh_tail\" \"jsh_tac\" \"jsh_tee\" \"jsh_wc\" \"jsh_nl\"\n \"jsh_fold\" \"jsh_expand\" \"jsh_unexpand\" \"jsh_fmt\"\n \"jsh_cut\" \"jsh_paste\" \"jsh_join\" \"jsh_comm\"\n \"jsh_sort\" \"jsh_uniq\" \"jsh_tr\" \"jsh_numfmt\"\n \"jsh_grep\"\n \"jsh_id\" \"jsh_whoami\" \"jsh_hostname\" \"jsh_uname\" \"jsh_uptime\"\n \"jsh_who\" \"jsh_groups\" \"jsh_users\" \"jsh_pinky\" \"jsh_logname\"\n \"jsh_arch\" \"jsh_nproc\" \"jsh_tty\" \"jsh_hostid\" \"jsh_date\"\n \"jsh_seq\" \"jsh_expr\" \"jsh_factor\"\n \"jsh_base64\" \"jsh_base32\" \"jsh_basenc\" \"jsh_od\"\n \"jsh_cksum\" \"jsh_md5sum\" \"jsh_sha1sum\" \"jsh_sha224sum\"\n \"jsh_sha256sum\" \"jsh_sha384sum\" \"jsh_sha512sum\" \"jsh_b2sum\" \"jsh_sum\"\n \"jsh_env\" \"jsh_timeout\" \"jsh_nice\" \"jsh_nohup\" \"jsh_chroot\"\n \"jsh_kill\"\n \"jsh_echo\" \"jsh_printf\" \"jsh_sleep\" \"jsh_yes\" \"jsh_printenv\"\n \"jsh_pwd\" \"jsh_sync\" \"jsh_test\" \"jsh_shuf\" \"jsh_split\" \"jsh_csplit\"\n \"jsh_tsort\" \"jsh_stty\" \"jsh_pr\" \"jsh_ptx\"\n \"jsh_basename\" \"jsh_dirname\"\n \"jsh_syscall4\" \"jsh_syscall5\" \"jsh_open_path\" \"jsh_close_fd\"\n \"jsh_prctl5\" \"jsh_errno_location\" \"jsh_realpath\"\n \"jerboa_tls_connect\" \"jerboa_tls_connect_pinned\"\n \"jerboa_tls_server_new\" \"jerboa_tls_server_new_pem\" \"jerboa_tls_accept\"\n \"jerboa_tls_read\" \"jerboa_tls_write\" \"jerboa_tls_flush\"\n \"jerboa_tls_close\" \"jerboa_tls_server_free\"\n \"jerboa_tls_set_nonblock\" \"jerboa_tls_get_fd\"\n \"jerboa_tls_server_new_mtls\" \"jerboa_tls_server_new_mtls_pem\"\n \"jerboa_tls_connect_mtls\" \"jerboa_tls_connect_mtls_mem\"\n \"jerboa_tls_connect_mtls_pem_ca\"\n \"jerboa_x509_generate_self_signed\" \"jerboa_x509_generate_self_signed_mem\"\n \"jerboa_x509_generate_signed_by_ca_mem\" \"jerboa_x509_cert_fingerprint\"\n \"jerboa_landlock_sandbox\"\n \"jerboa_landlock_sandbox_ex\"\n \"pcre2_compile_8\" \"pcre2_match_8\"\n \"pcre2_match_data_create_from_pattern_8\" \"pcre2_match_data_free_8\"\n \"pcre2_get_ovector_pointer_8\" \"pcre2_get_ovector_count_8\"\n \"pcre2_code_free_8\"\n \"EVP_CIPHER_CTX_ctrl\" \"EVP_CIPHER_CTX_free\" \"EVP_CIPHER_CTX_new\"\n \"EVP_DecryptFinal_ex\" \"EVP_DecryptInit_ex\" \"EVP_DecryptUpdate\"\n \"EVP_EncryptFinal_ex\" \"EVP_EncryptInit_ex\" \"EVP_EncryptUpdate\"\n \"EVP_aes_256_gcm\" \"EVP_sha256\" \"PKCS5_PBKDF2_HMAC\" \"RAND_bytes\"\n \"jerboa_ssh_aes256_ctr_free\" \"jerboa_ssh_aes256_ctr_init\"\n \"jerboa_ssh_aes256_ctr_process\"\n \"jerboa_ssh_chacha20_poly1305_decrypt\"\n \"jerboa_ssh_chacha20_poly1305_decrypt_length\"\n \"jerboa_ssh_chacha20_poly1305_encrypt\"\n \"jerboa_ssh_curve25519_keygen\" \"jerboa_ssh_curve25519_shared_secret\"\n \"jerboa_ssh_ed25519_derive_pubkey\" \"jerboa_ssh_ed25519_sign\"\n \"jerboa_ssh_ed25519_verify\" \"jerboa_ssh_hmac_sha256\"\n \"jerboa_ssh_random_bytes\" \"jerboa_ssh_sha256\"\n \"jerboa_ssh_tcp_accept\" \"jerboa_ssh_tcp_close\" \"jerboa_ssh_tcp_connect\"\n \"jerboa_ssh_tcp_listen\" \"jerboa_ssh_tcp_read\" \"jerboa_ssh_tcp_set_nodelay\"\n \"jerboa_ssh_tcp_write\"\n \"coreutils_gid_to_name\" \"coreutils_uid_to_name\"\n \"coreutils_ls_lstat\" \"coreutils_ls_readlink\" \"coreutils_ls_stat_get\"\n \"coreutils_raw_mode_enter\" \"coreutils_raw_mode_exit\"\n \"coreutils_terminal_height\" \"coreutils_terminal_width\"\n \"coreutils_time_format\"\n \"sandbox_init\" \"sandbox_free_error\"))\n\n(define (emit-c out)\n (display \"/* Generated by build-jsh-freebsd-cross.ss — do not edit by hand. */\\n\" out)\n (display \"#include <stdlib.h>\\n\" out)\n (display \"#include <string.h>\\n\" out)\n (display \"#include <stdio.h>\\n\" out)\n (display \"#include <unistd.h>\\n\" out)\n (display \"#include <sys/types.h>\\n\" out)\n (display \"#include <sys/stat.h>\\n\" out)\n (display \"#include <sys/wait.h>\\n\" out)\n (display \"#include <sys/resource.h>\\n\" out)\n (display \"#include <sys/mman.h>\\n\" out)\n (display \"#include <sys/socket.h>\\n\" out)\n (display \"#include <sys/sysctl.h>\\n\" out)\n (display \"#include <netinet/in.h>\\n\" out)\n (display \"#include <arpa/inet.h>\\n\" out)\n (display \"#include <fcntl.h>\\n\" out)\n (display \"#include <sys/file.h>\\n\" out)\n (display \"#include <signal.h>\\n\" out)\n (display \"#include <time.h>\\n\" out)\n (display \"#include <utime.h>\\n\" out)\n (display \"#include <errno.h>\\n\" out)\n (display \"#include \\\"scheme.h\\\"\\n\" out)\n (display \"#include \\\"petite_boot.h\\\"\\n\" out)\n (display \"#include \\\"scheme_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_libs_boot.h\\\"\\n\" out)\n (display \"#include \\\"jsh_program.h\\\"\\n\\n\" out)\n ;; Dynamic-linked FreeBSD binary: keep libc's real dlopen/dlsym/dlerror.\n ;; (load-shared-object #f) returns the main exe handle, dlsym(RTLD_DEFAULT, ...)\n ;; finds symbols thanks to -Wl,--export-dynamic on the final link.\n ;; FreeBSD errno compatibility: __errno_location is glibc; __error is FreeBSD libc.\n ;; Define a wrapper that returns &errno and register it under both names.\n (display \"/* FreeBSD errno compatibility */\\n\" out)\n (display \"static int *freebsd_errno_location(void) { return &errno; }\\n\\n\" out)\n ;; extern decls for ffi-shim.c symbols\n (display \"/* ffi-shim.c — auto-generated from ffi-shim-symbols.list */\\n\" out)\n (for-each (lambda (n) (fprintf out \"extern void ~a();\\n\" n)) ffi-shim-symbols)\n ;; extern decls for libjerboa_native.a symbols (those that exist for FreeBSD)\n (display \"\\n/* libjerboa_native.a — crypto feature on (FreeBSD subset) */\\n\" out)\n (for-each (lambda (n) (fprintf out \"extern void ~a();\\n\" n)) jerboa-native-symbols)\n ;; Linux-only jerboa_* — C stubs returning -1.\n (display \"\\n/* Linux-only jerboa_* — stubs return -1 on FreeBSD */\\n\" out)\n (for-each (lambda (n)\n (fprintf out \"static int ~a() { return -1; }\\n\" n))\n freebsd-stub-symbols)\n ;; POSIX wrappers\n (display \"\\n/* Wrappers for variadic/macro POSIX */\\n\" out)\n (display \"static int wrap_open(const char *p, int f, int m) { return open(p, f, m); }\\n\" out)\n (display \"static int wrap_fcntl(int fd, int c, int a) { return fcntl(fd, c, a); }\\n\" out)\n (display \"static int wrap_mkfifo(const char *p, int m) { return mkfifo(p, (mode_t)m); }\\n\" out)\n (display \"static int wrap_umask(int m) { return (int)umask((mode_t)m); }\\n\" out)\n (display \"static int wrap_mkdir(const char *p, int m) { return mkdir(p, (mode_t)m); }\\n\\n\" out)\n ;; weak stubs for symbols not linked into the cross build\n (display \"/* Weak stubs for symbols not linked into cross build */\\n\" out)\n (for-each (lambda (n)\n (fprintf out\n \"__attribute__((weak)) long ~a() { fprintf(stderr, \\\"[jsh-freebsd-amd64-weak] ~a\\\\n\\\"); fflush(stderr); return 0; }\\n\"\n n n))\n weak-stub-symbols)\n ;; embed_* — defined in ffi-shim.c via embed-crypto.h\n (display \"\\n/* embed_* — defined in ffi-shim.c via embed-crypto.h */\\n\" out)\n (display \"extern void embed_encrypt();\\n\" out)\n (display \"extern void embed_decrypt();\\n\" out)\n (display \"extern void embed_pbkdf2_sha256();\\n\" out)\n (display \"extern void embed_random_bytes();\\n\" out)\n (display \"extern void embed_read_passphrase();\\n\" out)\n ;; register all symbols at startup\n (newline out)\n (display \"static void register_ffi_symbols(void) {\\n\" out)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n ffi-shim-symbols)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n jerboa-native-symbols)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n freebsd-stub-symbols)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n posix-symbols)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)wrap_~a);\\n\" n n))\n posix-wrapped-symbols)\n ;; FreeBSD errno: register under both glibc and libc names\n (display \" Sforeign_symbol(\\\"__errno_location\\\", (void*)freebsd_errno_location);\\n\" out)\n (display \" Sforeign_symbol(\\\"__error\\\", (void*)freebsd_errno_location);\\n\" out)\n (for-each (lambda (n)\n (fprintf out \" Sforeign_symbol(\\\"~a\\\", (void*)~a);\\n\" n n))\n weak-stub-symbols)\n (display \" Sforeign_symbol(\\\"embed_encrypt\\\", (void*)embed_encrypt);\\n\" out)\n (display \" Sforeign_symbol(\\\"embed_decrypt\\\", (void*)embed_decrypt);\\n\" out)\n (display \" Sforeign_symbol(\\\"embed_pbkdf2_sha256\\\", (void*)embed_pbkdf2_sha256);\\n\" out)\n (display \" Sforeign_symbol(\\\"embed_random_bytes\\\", (void*)embed_random_bytes);\\n\" out)\n (display \" Sforeign_symbol(\\\"embed_read_passphrase\\\", (void*)embed_read_passphrase);\\n\" out)\n (display \"}\\n\\n\" out)\n (display \"int main(int argc, char *argv[]) {\\n\" out)\n (display \" setenv(\\\"JERBOA_STATIC\\\", \\\"1\\\", 1);\\n\" out)\n (display \" ffi_ensure_std_fds();\\n\\n\" out)\n ;; argv → JSH_ARGn env-var forwarding\n (display \" char countbuf[32];\\n\" out)\n (display \" snprintf(countbuf, sizeof(countbuf), \\\"%d\\\", argc - 1);\\n\" out)\n (display \" setenv(\\\"JSH_ARGC\\\", countbuf, 1);\\n\" out)\n (display \" for (int i = 1; i < argc; i++) {\\n\" out)\n (display \" char name[32];\\n\" out)\n (display \" snprintf(name, sizeof(name), \\\"JSH_ARG%d\\\", i - 1);\\n\" out)\n (display \" setenv(name, argv[i], 1);\\n\" out)\n (display \" }\\n\\n\" out)\n ;; Resolve exe path via sysctl (FreeBSD has no /proc/self/exe by default)\n (display \" {\\n\" out)\n (display \" int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };\\n\" out)\n (display \" char exe_buf[4096];\\n\" out)\n (display \" size_t exe_len = sizeof(exe_buf);\\n\" out)\n (display \" if (sysctl(mib, 4, exe_buf, &exe_len, NULL, 0) == 0) {\\n\" out)\n (display \" setenv(\\\"JSH_EXE\\\", exe_buf, 1);\\n\" out)\n (display \" }\\n\" out)\n (display \" }\\n\\n\" out)\n ;; FreeBSD: always use tmpfile for the WPO program. memfd_create exists on\n ;; FreeBSD 13+ but /dev/fd/N requires fdescfs which isn't mounted by default.\n (display \" char prog_path[256];\\n\" out)\n (display \" const char *tmpdir = getenv(\\\"TMPDIR\\\"); if (!tmpdir) tmpdir = \\\"/tmp\\\";\\n\" out)\n (display \" snprintf(prog_path, sizeof(prog_path), \\\"%s/.jsh-prog-%d.so\\\", tmpdir, getpid());\\n\" out)\n (display \" FILE *fp = fopen(prog_path, \\\"wb\\\");\\n\" out)\n (display \" if (!fp) { perror(\\\"fopen tmpfile\\\"); return 1; }\\n\" out)\n (display \" if (fwrite(jsh_program, 1, jsh_program_size, fp) != jsh_program_size) {\\n\" out)\n (display \" perror(\\\"fwrite\\\"); fclose(fp); unlink(prog_path); return 1;\\n\" out)\n (display \" }\\n\" out)\n (display \" fclose(fp);\\n\\n\" out)\n ;; Boot Chez.\n (display \" Sscheme_init(NULL);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"petite\\\", (void *)petite_boot, petite_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"scheme\\\", (void *)scheme_boot, scheme_boot_size);\\n\" out)\n (display \" Sregister_boot_file_bytes(\\\"jsh-libs\\\", (void *)jsh_libs_boot, jsh_libs_boot_size);\\n\" out)\n (display \" Sbuild_heap(NULL, NULL);\\n\" out)\n (display \" register_ffi_symbols();\\n\\n\" out)\n (display \" const char *prog_argv[] = { argv[0] };\\n\" out)\n (display \" int status = Sscheme_program(prog_path, 1, prog_argv);\\n\\n\" out)\n (display \" unlink(prog_path);\\n\" out)\n (display \" Sscheme_deinit();\\n\" out)\n (display \" return status;\\n\" out)\n (display \"}\\n\" out))\n\n(call-with-port (open-file-output-port main-c-path\n (file-options no-fail) (buffer-mode block) (native-transcoder))\n emit-c)\n(printf \"==> [5/6] generated ~a (~a ffi-shim + ~a native + ~a freebsd-stubs + ~a posix + ~a weak)~n\"\n main-c-path\n (length ffi-shim-symbols)\n (length jerboa-native-symbols)\n (length freebsd-stub-symbols)\n (length posix-symbols)\n (length weak-stub-symbols))\n\n;; ── Stage 6: compile + link with cross-cc ──────────────────────────────────\n(printf \"==> [6/6] compile + link with ~a~n\" cross-cc)\n;; FreeBSD differences from Linux musl:\n;; - Dynamic link (no -static): FreeBSD libc uses symbol versioning\n;; (e.g. wait4@FBSD_1.0) that libc.a + libc_nonshared.a can't satisfy.\n;; The standard FreeBSD distribution model is dynamic linking against\n;; the system libc.so.7 / libm.so / libthr.so / libutil.so.\n;; - No -ldl needed (dlopen lives in libc on FreeBSD)\n;; - -lpthread = libthr (FreeBSD's POSIX thread library)\n;; - -lutil for openpty (Rust pty crate)\n;; - -Wl,--export-dynamic so dlsym(RTLD_DEFAULT, ...) finds main-exe symbols\n(define link-cmd\n (format\n \"~a -O2 -Wl,--export-dynamic -I~a -o ~a ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a ~a -lm -lpthread -lutil\"\n cross-cc cross-csv-dir output main-c-path ffi-shim\n cross-csv-dir cross-csv-dir cross-csv-dir jerboa-native-a))\n(printf \" ~a~n\" link-cmd)\n(let ([rc (system link-cmd)])\n (unless (zero? rc)\n (error 'build-jsh-freebsd-cross \"cross-link failed\" rc)))\n\n(printf \"~n=== Build complete: ~a ===~n\" output)\n(system (format \"ls -lh ~a\" output))\n(system (format \"file ~a\" output))\n"} {"text":";; FILE: jerboa-shell/finding.md\n# Jerboa Shell - What's Missing\n\n## Current State\nThe project is a full POSIX shell with:\n- Job control (fg, bg, kill)\n- File descriptor redirection (2>&1, <, >, >>)\n- Command history with arrow key navigation\n- Tab completion for commands and paths\n- Process management (wait, jobs, disown)\n- Signal handling for SIGINT/SIGQUIT\n- Prompt customization (PS1-style prompt variables)\n\n## What's Missing\n\n### 1. Process Substitution\n`<(command)` and `>(command)` syntax is not implemented.\n\n**Why it matters:** This is a bash feature that allows piping to/from processes without temporary files. It's used in advanced shell scripting.\n\n**Effort:** Medium - requires implementing subshell execution with pipe redirection.\n\n### 2. Here Documents\n`cat << EOF` style input is not implemented.\n\n**Why it matters:** This is a standard shell feature for multi-line string input to commands.\n\n**Effort:** Medium - requires parsing the here document delimiter and handling it as input to a command.\n\n### 3. Advanced Redirection\nSome complex redirection patterns may not work (e.g., `command {fd}<file`).\n\n**Why it matters:** Bash supports fd redirection syntax that may not be fully implemented.\n\n**Effort:** Medium - requires testing and implementing missing patterns.\n\n### 4. Bash-style Aliases\nWhile alias/unalias commands exist, they may need refinement.\n\n**Why it matters:** Bash aliases have more features (e.g., alias expansion in command context).\n\n**Effort:** Small - review current implementation and add missing features.\n\n### 5. Process Substitution with Named Pipes\n`<(command)` creates a named pipe that can be read from.\n\n**Why it matters:** This is a core bash feature used in many scripts.\n\n**Effort:** Medium - requires creating named pipes and managing their lifecycle.\n\n### 6. Here String\n`command <<< \"string\"` syntax is not implemented.\n\n**Why it matters:** Another bash feature for providing strings as command input.\n\n**Effort:** Medium - requires parsing the <<< operator and treating the string as stdin.\n\n### 7. Command Substitution with Process Substitution\n`$(command)` inside `<(command2)` or `>(command2)`.\n\n**Why it matters:** Nested command substitution with process substitution.\n\n**Effort:** Medium - requires proper nesting handling.\n\n### 8. Signal Trapping\n`trap 'command' SIGTERM` syntax is not implemented.\n\n**Why it matters:** Bash allows trapping signals with custom handlers.\n\n**Effort:** Medium - requires implementing signal handler registration and execution.\n\n### 9. Arithmetic Expansion\n`$((expression))` syntax is not implemented.\n\n**Why it matters:** Bash arithmetic expansion with variable substitution.\n\n**Effort:** Medium - requires implementing the arithmetic expression parser.\n\n### 10. Parameter Expansion Extensions\n`${var#pattern}`, `${var##pattern}`, `${var%pattern}`, `${var%%pattern}`, `${var:pos}`, `${var:pos:len}`, `${var/pattern/repl}`, etc.\n\n**Why it matters:** Bash parameter expansion is extensive and used heavily in shell scripts.\n\n**Effort:** Large - requires implementing all the parameter expansion variants.\n\n## Recommendations\n\n1. **Start with process substitution** (`<(command)`) - it's the highest-value missing feature\n2. **Add here documents** (`cat << EOF`) - standard shell feature\n3. **Implement signal trapping** (`trap 'command' SIGTERM`) - needed for robust scripts\n4. **Add arithmetic expansion** (`$((1 + 2))`) - used in many scripts\n5. **Add here strings** (`command <<< \"string\"`) - related to here documents\n\nThe shell is already quite sophisticated for a POSIX shell. The gaps are mainly around advanced bash features that users expect.\n"} @@ -286,7 +286,7 @@ {"text":";; FILE: jerboa-shell/bench-smp.chez.ss\n#!chezscheme\n(import\n (except (chezscheme) void box box? unbox set-box! andmap\n ormap iota last-pair find \\x31;+ \\x31;- fx/ fx1+ fx1- error\n error? raise with-exception-handler identifier? hash-table?\n make-hash-table filter remove partition fold-right\n path-extension)\n (compat gambit-compat)\n (compat format)\n (compat misc))\n\n(define (fmt-secs s)\n (let ([ms (inexact->exact (round (* s 1000)))])\n (string-append (number->string ms) \"ms\")))\n\n(define (log! msg)\n (display msg (current-error-port))\n (flush-output-port (current-error-port)))\n\n(define (fib n)\n (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))\n\n(define (ack m n)\n (cond\n [(= m 0) (+ n 1)]\n [(= n 0) (ack (- m 1) 1)]\n [else (ack (- m 1) (ack m (- n 1)))]))\n\n(define (tak x y z)\n (if (<= x y)\n z\n (tak (tak (- x 1) y z)\n (tak (- y 1) z x)\n (tak (- z 1) x y))))\n\n(define (collatz-len n)\n (let loop ([n n] [len 0])\n (if (<= n 1)\n len\n (loop\n (if (even? n) (quotient n 2) (+ (* 3 n) 1))\n (+ len 1)))))\n\n(define (collatz-sum limit)\n (let loop ([i 1] [sum 0])\n (if (> i limit)\n sum\n (loop (+ i 1) (+ sum (collatz-len i))))))\n\n(define (matmul-bench size)\n (let ([a (make-vector (* size size) 1.0)]\n [b (make-vector (* size size) 2.0)]\n [c (make-vector (* size size) 0.0)])\n (let loopi ([i 0])\n (when (< i size)\n (let loopj ([j 0])\n (when (< j size)\n (let loopk ([k 0] [sum 0.0])\n (if (< k size)\n (loopk\n (+ k 1)\n (+ sum\n (* (vector-ref a (+ (* i size) k))\n (vector-ref b (+ (* k size) j)))))\n (vector-set! c (+ (* i size) j) sum)))\n (loopj (+ j 1))))\n (loopi (+ i 1))))\n (vector-ref c 0)))\n\n(define num-cores 8)\n\n(define (run-benchmarks-smp)\n (\\x23;\\x23;set-parallelism-level! num-cores)\n (\\x23;\\x23;startup-parallelism!)\n (thread-sleep! 0.1)\n (log!\n (format\n \"\\n=== bench-smp (~a cores, ~a processors) ===\\n\"\n num-cores\n (\\x23;\\x23;current-vm-processor-count)))\n (let ([benchmarks (list (cons \"fib(38) \" (lambda () (fib 38)))\n (cons \"ack(3,10) \" (lambda () (ack 3 10)))\n (cons \"tak(30,20,10)\" (lambda () (tak 30 20 10)))\n (cons\n \"collatz 500k \"\n (lambda () (collatz-sum 500000)))\n (cons\n \"matmul 150 \"\n (lambda () (matmul-bench 150))))])\n (log! \"\\n--- sequential (8x each) ---\\n\")\n (let ([seq-times (map (lambda (b)\n (let ([start (\\x23;\\x23;process-statistics)])\n (let loop ([i 0])\n (when (< i num-cores)\n ((cdr b))\n (loop (+ i 1))))\n (let* ([end (\\x23;\\x23;process-statistics)]\n [wall (- (f64vector-ref end 2)\n (f64vector-ref start 2))])\n (log!\n (format\n \" ~a ~a wall\\n\"\n (car b)\n (fmt-secs wall)))\n (cons (car b) wall))))\n benchmarks)])\n (log! \"\\n--- parallel (8 threads) ---\\n\")\n (let ([par-times (map (lambda (b)\n (let* ([start (\\x23;\\x23;process-statistics)]\n [threads (map (lambda (i)\n (thread-start!\n (make-thread\n (cdr b))))\n (iota num-cores))]\n [_ (for-each thread-join! threads)]\n [end (\\x23;\\x23;process-statistics)]\n [wall (- (f64vector-ref end 2)\n (f64vector-ref start 2))])\n (log!\n (format\n \" ~a ~a wall\\n\"\n (car b)\n (fmt-secs wall)))\n (cons (car b) wall)))\n benchmarks)])\n (log! \"\\n--- speedup ---\\n\")\n (for-each\n (lambda (seq par)\n (let* ([s (cdr seq)]\n [p (cdr par)]\n [speedup (if (> p 0) (/ s p) 0.0)])\n (log! (format \" ~a ~1,1fx\\n\" (car seq) speedup))))\n seq-times\n par-times))))\n [log! \"=== done ===\\n\"])\n\n(run-benchmarks-smp)\n"} {"text":";; FILE: jerboa-shell/tmux.md\n# jsh Multiplexer: tmux-like Session Management\n\n## Overview\n\nAdd tmux-like capabilities directly to jsh so that a single `jsh` process can manage\nmultiple terminal sessions, survive disconnection, and allow re-attachment — all without\nrequiring tmux itself. Users get tmux-style keybindings and session/window/pane management\nas a first-class shell feature.\n\n---\n\n## Two Core Features\n\n### Feature 1: Server/Client Architecture (Detach & Reattach)\n\n```\nTerminal A Background\n┌──────────┐ attach ┌──────────────────┐\n│ jsh │───────────────────→│ jsh --server │\n│ (client) │←───────────────────│ (daemon process) │\n└──────────┘ pty relay │ │\n │ session 0: bash │\nTerminal B │ session 1: vim │\n┌──────────┐ attach │ session 2: htop │\n│ jsh │───────────────────→│ │\n│ (client) │←───────────────────│ │\n└──────────┘ └──────────────────────┘\n```\n\n**Usage:**\n\n```bash\n# Start a new server (daemonizes, survives HUP)\njsh --server # start server, print socket path\njsh --server --name work # named server\njsh --server --password # prompt for password (not echoed)\njsh --server --password-file ~/.jsh-pass # read password from file\n\n# Attach to existing server\njsh --attach # attach to default server\njsh --attach --name work # attach to named server\njsh -A # short form\n# (prompted for password if server requires one)\n\n# List running servers\njsh --list-servers # show active servers + session counts\n\n# Change or remove password on a running server\n# ,password # set/change password (prompted)\n# ,password --clear # remove password requirement\n\n# Detach from inside a session\n# Ctrl-b d # tmux-style keybinding\n# ,detach # meta-command\n```\n\n**Output replay on reconnect.** Each pane keeps a ring of its recent output. On\ndetach or a dropped connection the client prints a resume hint with the byte\noffset it reached; reconnect with `--from OFFSET` to replay output missed while\naway before resuming live:\n\n```\njsh: to resume this stream: ,mux attach HOST:PORT --from 18432\n,mux attach HOST:PORT --from 18432 # (or ,attach --from OFFSET for a local server)\n```\n\n### Feature 2: Multi-Session Multiplexing (Windows & Panes)\n\n```\n┌─────────────────────────────────────────────────┐\n│ [0: jsh] [1: vim]* [2: logs] Ctrl-b ? │\n├────────────────────────┬────────────────────────┤\n│ ~/project │ ~/project │\n│ $ make build │ $ tail -f app.log │\n│ Building... │ [2026-03-19] INFO ... │\n│ Done. │ [2026-03-19] WARN ... │\n│ $ │ │\n│ │ │\n│ │ │\n│ │ │\n├────────────────────────┴────────────────────────┤\n│ [session: work] [window: 1/3] [pane: 0] │\n└─────────────────────────────────────────────────┘\n```\n\n**Keybindings (tmux-compatible prefix: Ctrl-b):**\n\n| Key | Action |\n|-----|--------|\n| `Ctrl-b c` | Create new window |\n| `Ctrl-b C` | Create new sudo `jsh` window |\n| `Ctrl-b n` / `Ctrl-b p` | Next / previous window |\n| `Ctrl-b 0-9` | Switch to window N |\n| `Ctrl-b %` | Split pane vertically |\n| `Ctrl-b \"` | Split pane horizontally |\n| `Ctrl-b S` | Split pane with sudo `jsh` |\n| `Ctrl-b o` | Cycle to next pane |\n| `Ctrl-b Arrow` | Move to pane in direction |\n| `Ctrl-b x` | Kill current pane |\n| `Ctrl-b &` | Kill current window |\n| `Ctrl-b d` | Detach from server |\n| `Ctrl-b W` | Save session (layout + scrollback) to snapshot `default` |\n| `Ctrl-b E` | Restore snapshot `default` (fresh shells, scrollback replayed) |\n| `Ctrl-b z` | Toggle pane zoom (fullscreen) |\n| `Ctrl-b [` | Enter scroll/copy mode |\n| `Ctrl-b ]` | Paste from copy buffer |\n| `Ctrl-b ,` | Rename current window |\n| `Ctrl-b w` | List windows (interactive chooser) |\n| `Ctrl-b s` | List sessions (interactive chooser) |\n| `Ctrl-b :` | Command prompt (like tmux command mode) |\n| `Ctrl-b ?` | Show keybinding help |\n\nSudo panes run sudo inside the pane PTY, so sudo prompts there directly. By\ndefault they pass the invoking user's `JSH_EMBED_OVERLAY_DIR` (or\n`$HOME/.jsh/embed/`) through `/usr/bin/env` and chown new overlay files back to\nthe invoking uid/gid. Set `JSH_MUX_SUDO_SHARE_EMBED=0` to use root's overlay\ninstead. Set `JSH_MUX_SUDO_FORCE_PROMPT=1` to add `sudo -k`,\n`JSH_MUX_SUDO_TARGET=/path/to/jsh` to override the target binary, or\n`JSH_MUX_ENV_COMMAND=/path/to/env` if `/usr/bin/env` is not correct.\n\n---\n\n## Architecture\n\n### Component Diagram\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ jsh Server Process │\n│ (setsid, daemonized) │\n│ │\n│ ┌──────────────────────────────────────────────────┐ │\n│ │ Session Manager │ │\n│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │\n│ │ │ Session 0│ │ Session 1│ │ Session 2│ ... │ │\n│ │ │ │ │ │ │ │ │ │\n│ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │\n│ │ │ │Win 0 │ │ │ │Win 0 │ │ │ │Win 0 │ │ │ │\n│ │ │ │┌────┐│ │ │ │┌────┐│ │ │ │┌────┐│ │ │ │\n│ │ │ ││Pane││ │ │ ││Pane││ │ │ ││Pane││ │ │ │\n│ │ │ ││PTY ││ │ │ ││PTY ││ │ │ ││PTY ││ │ │ │\n│ │ │ │└────┘│ │ │ │└────┘│ │ │ │└────┘│ │ │ │\n│ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ │\n│ │ └──────────┘ └──────────┘ └──────────┘ │ │\n│ └──────────────────────────────────────────────────┘ │\n│ │ │\n│ ┌───────────────────────┴──────────────────────────┐ │\n│ │ Client Connection Manager │ │\n│ │ Unix socket: /run/user/$UID/jsh/default.sock │ │\n│ └──────────────────────────────────────────────────┘ │\n│ │ │\n└──────────────────────────┼──────────────────────────────┘\n │ Unix domain socket\n ┌────────────┼────────────────┐\n │ │ │\n ┌────┴────┐ ┌────┴────┐ ┌────┴────┐\n │Client A │ │Client B │ │Client C │\n │(terminal│ │(terminal│ │(terminal│\n │attached)│ │attached)│ │attached)│\n └─────────┘ └─────────┘ └─────────┘\n```\n\n### Data Structures\n\n```scheme\n;; A session groups related windows\n(defstruct session\n id: fixnum ; unique session id\n name: string ; human-readable name\n windows: (list-of window)\n active-window: fixnum ; index into windows\n created: fixnum ; timestamp\n env: hashtable) ; session-level environment overrides\n\n;; A window contains one or more panes in a layout\n(defstruct window\n id: fixnum\n name: string ; user-settable, or auto from running command\n panes: (list-of pane)\n active-pane: fixnum\n layout: layout) ; how panes are arranged\n\n;; A pane is a single shell instance backed by a PTY\n(defstruct pane\n id: fixnum\n pty-master: fixnum ; master fd\n pty-slave: fixnum ; slave fd (held for lifecycle)\n pid: fixnum ; child shell process pid\n cols: fixnum ; pane width in columns\n rows: fixnum ; pane height in rows\n x: fixnum ; pane origin column in window\n y: fixnum ; pane origin row in window\n scrollback: bytevector ; ring buffer of output history\n scroll-pos: fixnum ; current scroll position (for copy mode)\n title: string ; pane title (from OSC escape or command)\n zoomed?: boolean) ; is this pane in zoom mode?\n\n;; Layout tree for splitting\n(defstruct layout\n type: symbol ; 'horizontal | 'vertical | 'leaf\n ratio: flonum ; split ratio (0.0-1.0)\n children: (or pane (list-of layout)))\n\n;; Server authentication state\n(defstruct server-auth\n enabled?: boolean ; is password required?\n password-hash: (or #f bytevector) ; Argon2id hash of password\n salt: (or #f bytevector) ; 16-byte random salt\n max-attempts: fixnum ; max failed attempts before lockout (default 5)\n lockout-sec: fixnum ; lockout duration in seconds (default 60)\n failed-ips: hashtable) ; peer-cred uid → (attempts . last-fail-time)\n\n;; Client connection state\n(defstruct client\n fd: fixnum ; Unix socket fd\n session-id: fixnum ; which session this client is viewing\n cols: fixnum ; client terminal width\n rows: fixnum ; client terminal height\n attached?: boolean ; is client actively attached?\n authenticated?: boolean ; has client passed auth challenge?\n tx-cipher: (or #f cipher-state) ; encrypt outgoing (server→client)\n rx-cipher: (or #f cipher-state)) ; decrypt incoming (client→server)\n\n;; Per-direction encryption state (ChaCha20-Poly1305)\n(defstruct cipher-state\n key: bytevector ; 32-byte symmetric key\n nonce-counter: fixnum) ; monotonic counter, starts at 0\n```\n\n### Wire Protocol (Client <-> Server)\n\nBinary message framing over Unix domain socket:\n\n```\n┌──────────┬──────────┬──────────────────────┐\n│ Type (1B)│ Len (4B) │ Payload (variable) │\n└──────────┴──────────┴──────────────────────┘\n```\n\n**Message Types:**\n\n| Type | Direction | Payload | Purpose |\n|------|-----------|---------|---------|\n| `0x01` ATTACH | C→S | `{session_name, cols, rows}` | Attach to / create session |\n| `0x02` DETACH | C→S | (empty) | Graceful detach |\n| `0x03` INPUT | C→S | raw bytes | Keyboard input from client |\n| `0x04` RESIZE | C→S | `{cols, rows}` | Client terminal resized |\n| `0x05` OUTPUT | S→C | raw bytes | PTY output to display |\n| `0x06` REDRAW | S→C | full screen bytes | Complete screen redraw |\n| `0x07` CMD | C→S | `{command, args}` | Multiplexer command (new-window, etc.) |\n| `0x08` STATUS | S→C | `{windows, active, pane_info}` | Status bar update |\n| `0x09` ERROR | S→C | error string | Error message |\n| `0x0A` PING | C→S | (empty) | Keepalive |\n| `0x0B` PONG | S→C | (empty) | Keepalive response |\n| `0x0C` SESSION_LIST | S→C | `[{id, name, windows, attached}]` | Response to list command |\n| `0x10` AUTH_REQUIRED | S→C | `{salt, challenge}` | Server requires authentication |\n| `0x11` AUTH_RESPONSE | C→S | `{proof}` | Client sends password proof |\n| `0x12` AUTH_OK | S→C | (empty) | Authentication succeeded |\n| `0x13` AUTH_FAIL | S→C | `{reason, retry_after_sec}` | Authentication failed |\n| `0x14` AUTH_CHANGE | C→S | `{old_proof, new_hash, new_salt}` | Change password (from attached client) |\n| `0x15` KEY_EXCHANGE | C→S | `{client_pubkey}` | Ephemeral X25519 public key (no-password encryption) |\n\n---\n\n## Implementation Plan\n\n### Phase 0: PTY Support in FFI (Foundation)\n\n**Goal:** Add pseudoterminal allocation to `ffi-shim.c` so the server can create\nPTY pairs for each pane.\n\n**New FFI functions:**\n\n```c\n// Allocate a new PTY pair, returns master fd (slave path via ptsname)\nint ffi_pty_open(void); // posix_openpt + grantpt + unlockpt\nchar* ffi_pty_slave_name(int master_fd); // ptsname(master_fd)\nint ffi_pty_open_slave(int master_fd); // open(ptsname(master_fd))\nvoid ffi_pty_set_size(int fd, int cols, int rows); // ioctl TIOCSWINSZ\nvoid ffi_pty_get_size(int fd, int* cols, int* rows); // ioctl TIOCGWINSZ\n\n// Daemonization\nint ffi_daemonize(void); // fork, setsid, fork, chdir(\"/\"), close fds\nint ffi_write_pidfile(const char* path); // write getpid() to file\n```\n\n**Files modified:**\n- `ffi-shim.c` — add PTY and daemon functions\n- `src/jsh/ffi.sls` — add Scheme bindings\n\n**Tests:**\n- Allocate PTY, write to master, read from slave\n- Set/get PTY size\n- Fork child into PTY slave, verify I/O\n\n**Estimated complexity:** Small. ~150 lines of C, ~50 lines of Scheme bindings.\n\n---\n\n### Phase 1: Server Daemon Mode\n\n**Goal:** `jsh --server` starts a background daemon listening on a Unix socket.\n\n**Startup sequence:**\n\n```\njsh --server [--name NAME] [--password | --password-file PATH]\n │\n ├─ 1. Check for existing server (socket file exists + responsive)\n │ → if exists and alive: print error, exit\n │ → if exists and dead: remove stale socket, continue\n │\n ├─ 1.5. If --password or --password-file:\n │ Read/prompt for password, hash with Argon2id,\n │ write NAME.auth (mode 0600), zero password from memory\n │\n ├─ 2. Daemonize (double-fork, setsid, close stdio)\n │\n ├─ 3. Write PID file: /run/user/$UID/jsh/NAME.pid\n │\n ├─ 4. Create Unix socket: /run/user/$UID/jsh/NAME.sock\n │ (fallback: ~/.jsh/NAME.sock if /run/user not available)\n │ Socket directory mode 0700 (owner-only access)\n │\n ├─ 5. Install signal handlers:\n │ SIGTERM → graceful shutdown (notify clients, kill children)\n │ SIGCHLD → reap children, update pane status\n │ SIGHUP → ignored (daemon must survive)\n │ SIGPIPE → ignored (client may disconnect)\n │\n ├─ 6. Create default session with one window and one pane\n │ (allocate PTY, fork child jsh into it)\n │\n └─ 7. Enter event loop:\n select/poll on: server socket + all client fds + all PTY master fds\n ├─ New client connection → accept, run auth handshake (if enabled), then ATTACH\n ├─ Client input → route to active pane's PTY master\n ├─ PTY output → broadcast to all clients viewing that pane\n ├─ Client disconnect → mark detached, keep session alive\n └─ PTY EOF → pane died, clean up, maybe close window\n```\n\n**Socket path convention:**\n```\n/run/user/$UID/jsh/ # XDG_RUNTIME_DIR based (mode 0700)\n├── default.sock # default server\n├── default.pid\n├── default.auth # password hash + salt (only if --password)\n├── work.sock # named server \"work\"\n├── work.pid\n├── work.auth # (only if --password)\n└── ...\n```\n\n**Files:**\n- New: `src/jsh/server.sls` — server event loop, client management\n- New: `src/jsh/mux-protocol.sls` — wire protocol encode/decode\n- Modified: `src/jsh/main.sls` — add `--server`, `--attach`, `--list-servers` arg parsing\n\n**Estimated complexity:** Medium-large. Core event loop + socket management.\n\n---\n\n### Phase 1.5: Authentication\n\n**Goal:** Optionally require a password to attach to a server. Prevent unauthorized\naccess when the socket is reachable (shared machines, forwarded sockets, etc.).\n\n#### Design Principles\n\n1. **Opt-in** — no password by default (Unix socket permissions are sufficient for\n single-user machines). Password adds defense-in-depth.\n2. **No plaintext on wire** — challenge-response protocol so the password never\n travels over the socket, even in cleartext Unix domain socket scenarios.\n3. **Encrypted channel** — after authentication, all traffic is encrypted with\n ChaCha20-Poly1305 using a session key derived from the auth handshake. Even\n without a password, clients and server can negotiate an encrypted channel.\n4. **Brute-force resistant** — Argon2id for password hashing, rate limiting on\n failed attempts, exponential backoff with lockout.\n5. **No stored plaintext** — server stores only the Argon2id hash + salt, never\n the password itself. Hash file is mode 0600.\n6. **Changeable at runtime** — password can be set, changed, or cleared while\n the server is running via meta-command from an authenticated session.\n\n#### Authentication Flow\n\n```\nClient Server\n │ │\n ├─── connect() ───────────────────────────────→ │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Server checks: auth enabled? │ │\n │ │ No → send AUTH_OK, skip to ATTACH │ │\n │ │ Yes → generate challenge, send it │ │\n │ └──────────────────────────────────────┘ │\n │ │\n │ ←── AUTH_REQUIRED {salt, challenge} ────────── │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Client prompts user for password │ │\n │ │ (canonical mode, no echo, on stderr) │ │\n │ │ │ │\n │ │ Compute: │ │\n │ │ key = Argon2id(password, salt) │ │\n │ │ proof = HMAC-SHA256(key, challenge) │ │\n │ └──────────────────────────────────────┘ │\n │ │\n ├─── AUTH_RESPONSE {proof} ─────────────────→ │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Server computes expected proof: │ │\n │ │ key = stored_hash (already Argon2id)│ │\n │ │ expected = HMAC-SHA256(key, challenge) │\n │ │ │ │\n │ │ constant-time compare proof vs expected│ │\n │ │ Match → AUTH_OK │ │\n │ │ Mismatch → AUTH_FAIL + rate limit │ │\n │ └──────────────────────────────────────┘ │\n │ │\n │ ←── AUTH_OK {server_nonce} ───────────────── │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Both sides derive session key: │ │\n │ │ session_key = HMAC-SHA256(key, │ │\n │ │ challenge ‖ server_nonce) │ │\n │ │ │ │\n │ │ All subsequent messages encrypted │ │\n │ │ with ChaCha20-Poly1305 using │ │\n │ │ session_key + per-message nonce │ │\n │ └──────────────────────────────────────┘ │\n │ │\n ├─── ATTACH {session, cols, rows} ──────────→ │\n │ (encrypted from here on) │\n```\n\n**No-password mode with encryption:**\n\nWhen the server has no password set, the channel can still be encrypted using\nan ephemeral key exchange:\n\n```\nClient Server\n │ │\n ├─── connect() ───────────────────────────────→ │\n │ │\n │ ←── AUTH_OK {server_nonce, server_pubkey} ─── │\n │ (no password required) │\n │ │\n ├─── KEY_EXCHANGE {client_pubkey} ────────────→ │\n │ │\n │ ┌──────────────────────────────────────┐ │\n │ │ Both sides compute: │ │\n │ │ shared = X25519(my_priv, their_pub) │ │\n │ │ session_key = HMAC-SHA256(shared, │ │\n │ │ server_nonce ‖ client_pubkey │ │\n │ │ ‖ server_pubkey) │ │\n │ │ │ │\n │ │ Encrypted channel established │ │\n │ └──────────────────────────────────────┘ │\n │ │\n ├─── ATTACH {session, cols, rows} ──────────→ │\n │ (encrypted from here on) │\n```\n\nThis gives encryption even without authentication — protecting against passive\neavesdropping on the socket (e.g., root sniffing with `socat`, forwarded sockets).\nIt does **not** protect against active MITM (no identity verification without a\npassword). To disable encryption entirely (micro-optimization for trusted local\nsockets), use `JSH_MUX_ENCRYPT=off`.\n\n#### Challenge-Response Detail\n\nThe server never sends or receives the raw password. Instead:\n\n1. **Server stores:** `Argon2id(password, salt)` → 32-byte `key`, plus the 16-byte `salt`\n2. **On connect:** Server generates a random 32-byte `challenge` (one-time nonce)\n3. **Client computes:** `key = Argon2id(password, salt)` then `proof = HMAC-SHA256(key, challenge)`\n4. **Server computes:** `expected = HMAC-SHA256(stored_key, challenge)` and compares\n5. **Timing-safe comparison:** `ffi_constant_time_compare(proof, expected, 32)`\n\nThis ensures:\n- Password never crosses the wire (only the HMAC proof does)\n- Replay attacks fail (each challenge is a fresh nonce)\n- Offline brute-force requires cracking Argon2id\n\n#### Encrypted Channel (Post-Auth)\n\nOnce authentication succeeds (or key exchange completes in no-password mode),\nall subsequent wire protocol messages are wrapped in ChaCha20-Poly1305 AEAD:\n\n```\nPlaintext message (Type + Len + Payload):\n┌──────────┬──────────┬──────────────────────┐\n│ Type (1B)│ Len (4B) │ Payload (variable) │\n└──────────┴──────────┴──────────────────────┘\n\nEncrypted on wire:\n┌────────────┬──────────────────────────────────┬──────────┐\n│ Nonce (12B)│ Ciphertext (5B + payload) │ Tag (16B)│\n└────────────┴──────────────────────────────────┴──────────┘\n ↑ ChaCha20-Poly1305 encrypts ↑ authenticates\n Type+Len+Payload nonce+ciphertext\n```\n\n**Nonce management:**\n\nEach direction (client→server, server→client) maintains an independent 64-bit\nmessage counter starting at 0, encoded as a 12-byte little-endian nonce\n(4 zero bytes prefix + 8-byte counter). The counter increments after every\nmessage. This avoids nonce reuse without coordination.\n\n```scheme\n;; Per-direction encryption state\n(defstruct cipher-state\n key: bytevector ; 32-byte ChaCha20-Poly1305 key\n nonce-counter: fixnum) ; monotonically increasing, starts at 0\n\n;; Encrypt a message\n(define (encrypt-message state type payload)\n (let* ((nonce (counter->nonce (cipher-state-nonce-counter state)))\n (plaintext (encode-message type payload))\n (ciphertext (chacha20-poly1305-encrypt\n (cipher-state-key state) nonce plaintext)))\n (set! (cipher-state-nonce-counter state)\n (+ 1 (cipher-state-nonce-counter state)))\n (bytevector-append nonce ciphertext)))\n\n;; Decrypt a message\n(define (decrypt-message state encrypted)\n (let* ((nonce (bytevector-slice encrypted 0 12))\n (expected-nonce (counter->nonce (cipher-state-nonce-counter state)))\n (_ (unless (bytevector=? nonce expected-nonce)\n (error \"nonce mismatch — replay or reorder detected\")))\n (ciphertext (bytevector-slice encrypted 12))\n (plaintext (chacha20-poly1305-decrypt\n (cipher-state-key state) nonce ciphertext)))\n (set! (cipher-state-nonce-counter state)\n (+ 1 (cipher-state-nonce-counter state)))\n (decode-message plaintext)))\n```\n\n**Session key derivation (password mode):**\n\nBoth sides already share `key` (the Argon2id hash) and `challenge`. The server\ngenerates a fresh `server_nonce` and includes it in `AUTH_OK`. The session key\nis derived deterministically:\n\n```\nsession_key = HMAC-SHA256(key, challenge ‖ server_nonce ‖ \"jsh-mux-v1\")\n```\n\nTwo directional keys are derived from this:\n\n```\nclient_to_server_key = HMAC-SHA256(session_key, \"client-to-server\")\nserver_to_client_key = HMAC-SHA256(session_key, \"server-to-client\")\n```\n\nUsing separate keys per direction prevents reflection attacks.\n\n**Session key derivation (no-password mode):**\n\nX25519 key agreement produces a 32-byte shared secret. The session key is:\n\n```\nsession_key = HMAC-SHA256(shared_secret,\n server_nonce ‖ client_pubkey ‖ server_pubkey ‖ \"jsh-mux-v1\")\n```\n\nSame directional key split as above.\n\n**Performance:**\n\nChaCha20-Poly1305 is extremely fast (~3 GB/s on modern CPUs without hardware\nAES). For terminal I/O (typically <1 MB/s even under heavy `cat /dev/urandom`),\nthe overhead is negligible — under 1% CPU.\n\n#### Password Storage\n\n```\n/run/user/$UID/jsh/NAME.auth (mode 0600)\n```\n\nFormat (binary):\n```\n┌──────────┬────────────┬───────────────────────┐\n│ Salt(16B)│ Argon2id │ Hash (32B) │\n│ │ params(12B)│ │\n└──────────┴────────────┴───────────────────────┘\n```\n\nArgon2id parameters: `t=3, m=65536 (64MB), p=1` — tuned for interactive\nlatency (~0.5s on modern hardware). Stored alongside the hash so the client\ncan use matching parameters.\n\n#### Rate Limiting & Lockout\n\n```\nPer connecting UID (via SO_PEERCRED on Unix sockets):\n\n Attempt 1-3: immediate response\n Attempt 4: 1 second delay\n Attempt 5: AUTH_FAIL + lockout for 60 seconds\n After lockout: counter resets\n\nServer logs failed attempts to stderr (if --verbose) or syslog.\n```\n\nThe lockout state is tracked in `server-auth.failed-ips` (keyed by peer UID\nfrom `SO_PEERCRED`). This prevents a local user from brute-forcing the password\nbut doesn't penalize legitimate users on failed typos excessively.\n\n#### Server-Side Setup\n\n```\njsh --server --password\n │\n ├─ 1. Prompt: \"Set server password: \" (no echo, canonical mode)\n ├─ 2. Prompt: \"Confirm password: \" (no echo)\n ├─ 3. If mismatch → error, exit\n ├─ 4. Generate 16-byte random salt (getrandom/urandom)\n ├─ 5. Compute hash = Argon2id(password, salt, t=3, m=64MB, p=1)\n ├─ 6. Zero password from memory (explicit_bzero)\n ├─ 7. Write {salt, params, hash} to NAME.auth (mode 0600)\n └─ 8. Continue normal server startup\n\njsh --server --password-file PATH\n │\n ├─ 1. Read first line of PATH as password (trim newline)\n ├─ 2. Same steps 4-8 as above\n └─ 3. Zero file contents from memory\n```\n\n#### Client-Side Auth\n\n```\njsh --attach [--name NAME]\n │\n ├─ 1. Connect to socket\n ├─ 2. Receive first message:\n │ ├─ AUTH_OK → no password needed, proceed to attach\n │ └─ AUTH_REQUIRED {salt, challenge} → need password\n │\n ├─ 3. If AUTH_REQUIRED:\n │ ├─ If stdin is a TTY:\n │ │ Prompt: \"Password: \" (no echo, on stderr so pipes work)\n │ ├─ If stdin is not a TTY:\n │ │ Read from JSH_MUX_PASSWORD env var\n │ │ Or read from --password-file if given\n │ │ Or error: \"password required but no TTY\"\n │ │\n │ ├─ Compute key = Argon2id(password, salt)\n │ ├─ Compute proof = HMAC-SHA256(key, challenge)\n │ ├─ Zero password + key from memory\n │ ├─ Send AUTH_RESPONSE {proof}\n │ │\n │ ├─ Receive response:\n │ │ ├─ AUTH_OK → proceed to attach\n │ │ └─ AUTH_FAIL {reason, retry_after} →\n │ │ print error, wait retry_after, re-prompt (up to 3 tries)\n │ │\n │ └─ After 3 client-side failures → exit 1\n │\n └─ 4. Send ATTACH message (normal flow)\n```\n\n#### Runtime Password Management\n\nFrom an already-authenticated session:\n\n```\n,password # set or change password\n ├─ Prompt: \"New password: \" (no echo)\n ├─ Prompt: \"Confirm: \"\n ├─ Server re-hashes with new salt\n ├─ Existing authenticated clients remain connected\n └─ New connections must use new password\n\n,password --clear # remove password requirement\n ├─ Server disables auth\n ├─ Deletes NAME.auth file\n └─ New connections no longer prompted\n```\n\n#### Security Considerations\n\n| Threat | Mitigation |\n|--------|------------|\n| Eavesdropping on Unix socket | All post-auth traffic encrypted with ChaCha20-Poly1305. Even without password, X25519 key exchange encrypts the channel. |\n| Passive network sniffing (forwarded sockets) | Encrypted channel protects all INPUT/OUTPUT/CMD traffic. Attacker sees only ciphertext + message lengths. |\n| Active MITM (no password) | X25519 without authentication cannot prevent MITM. Use `--password` for full protection on untrusted paths. |\n| Brute force | Argon2id (slow hash) + rate limiting + lockout after 5 failures |\n| Replay attack on auth | Fresh 32-byte random challenge per connection attempt |\n| Replay/reorder attack on channel | Monotonic nonce counter per direction; out-of-order nonces rejected. Poly1305 tag rejects tampered ciphertext. |\n| Timing side-channel | `ffi_constant_time_compare` for proof verification |\n| Password in memory | `explicit_bzero` on password, key material after use (already in ffi-shim.c) |\n| Session key in memory | Keys zeroed on detach/disconnect. Keys are per-connection (not reused). |\n| Stolen .auth file | Attacker gets Argon2id hash, must still crack it. No plaintext. Does not reveal session keys. |\n| Privilege escalation via socket | `SO_PEERCRED` verifies connecting UID matches server UID (unless explicitly opened) |\n| Password file on disk | `--password-file` read once at startup, contents zeroed. File can be on tmpfs or removed after. |\n| Nonce exhaustion | 64-bit counter supports 2^64 messages per direction (~500 exabytes at max throughput). Practically inexhaustible. |\n\n#### New FFI Functions\n\n```c\n// Argon2id password hashing (using system libargon2, or embedded impl)\nint ffi_argon2id_hash(const char* password, int pwlen,\n const uint8_t* salt, int saltlen,\n int t_cost, int m_cost, int parallelism,\n uint8_t* out, int outlen);\n\n// HMAC-SHA256 for challenge-response proof\nint ffi_hmac_sha256(const uint8_t* key, int keylen,\n const uint8_t* msg, int msglen,\n uint8_t* out); // always 32 bytes\n\n// Constant-time comparison (prevents timing attacks)\nint ffi_constant_time_compare(const uint8_t* a, const uint8_t* b, int len);\n\n// Cryptographic random bytes\nint ffi_getrandom(uint8_t* buf, int len); // getrandom(2) or /dev/urandom\n\n// Get peer credentials from Unix socket\nint ffi_peercred_uid(int sockfd); // getsockopt SO_PEERCRED → uid\n\n// X25519 key exchange (for no-password encrypted channels)\nint ffi_x25519_keypair(uint8_t* pubkey, uint8_t* privkey); // generate ephemeral pair\nint ffi_x25519_shared(const uint8_t* my_priv, const uint8_t* their_pub,\n uint8_t* shared_out); // compute shared secret\n```\n\nNote: `explicit_bzero`, `ChaCha20-Poly1305`, and PBKDF2 primitives already exist in\n`ffi-shim.c` / `embed-crypto.c` — the encrypted channel reuses the existing\nChaCha20-Poly1305 implementation directly. HMAC-SHA256 can be built from the\nexisting SHA256, or pulled from libargon2's dependency on libcrypto. X25519 can\nuse TweetNaCl (~800 lines of C, public domain) or system libsodium.\n\n**Files:**\n- New: `src/jsh/mux-auth.sls` — auth protocol, hashing, challenge generation, encrypted channel\n- Modified: `ffi-shim.c` — add Argon2id, HMAC-SHA256, getrandom, peercred, X25519 FFI\n- Modified: `src/jsh/server.sls` — integrate auth check + encryption into accept flow\n- Modified: `src/jsh/client.sls` — integrate auth handshake + encryption into connect flow\n- Modified: `src/jsh/mux-protocol.sls` — encrypt/decrypt wrapper around message encode/decode\n\n**Estimated complexity:** Medium. ~400 lines Scheme + ~250 lines C (or ~800 lines C if embedding TweetNaCl for X25519).\n\n---\n\n### Phase 2: Client Attach Mode\n\n**Goal:** `jsh --attach` connects to a running server and relays terminal I/O.\n\n**Client lifecycle:**\n\n```\njsh --attach [--name NAME] [--session SESSION]\n │\n ├─ 1. Locate socket: /run/user/$UID/jsh/NAME.sock\n │ → not found: error \"no server named NAME\"\n │\n ├─ 2. Connect to Unix socket\n │\n ├─ 3. Authentication handshake (Phase 1.5)\n │ → AUTH_OK received (either no-auth or password accepted)\n │\n ├─ 4. Send ATTACH message with {session_name, cols, rows}\n │\n ├─ 5. Save terminal state (termios)\n │\n ├─ 6. Set terminal to raw mode\n │\n ├─ 7. Enter relay loop:\n │ select/poll on: stdin (fd 0) + server socket\n │ ├─ stdin data → wrap as INPUT message, send to server\n │ │ (intercept Ctrl-b prefix for local mux commands)\n │ ├─ server OUTPUT → write directly to stdout\n │ ├─ server STATUS → render status bar\n │ ├─ server ERROR → display error\n │ ├─ SIGWINCH → send RESIZE message to server\n │ └─ server disconnect → restore terminal, exit\n │\n └─ 7. On detach/disconnect:\n Restore terminal state\n Print \"detached from session NAME\"\n Exit 0\n```\n\n**Prefix key handling (client-side):**\n\n```\nInput byte → is prefix active?\n │\n ├─ No: is this Ctrl-b (0x02)?\n │ ├─ Yes: set prefix_active = true, start 1s timeout\n │ └─ No: forward byte to server as INPUT\n │\n └─ Yes (prefix active):\n ├─ 'd' → send DETACH, disconnect\n ├─ 'c' → send CMD{new-window}\n ├─ 'n' → send CMD{next-window}\n ├─ 'p' → send CMD{prev-window}\n ├─ '0'-'9' → send CMD{select-window, N}\n ├─ '%' → send CMD{split-vertical}\n ├─ '\"' → send CMD{split-horizontal}\n ├─ 'o' → send CMD{next-pane}\n ├─ 'x' → send CMD{kill-pane}\n ├─ 'z' → send CMD{zoom-pane}\n ├─ 'w' → send CMD{list-windows}\n ├─ 's' → send CMD{list-sessions}\n ├─ '?' → send CMD{show-help}\n ├─ ':' → enter local command-line mode\n ├─ Ctrl-b → forward literal Ctrl-b to server\n ├─ timeout → forward original Ctrl-b + this byte\n └─ unknown → beep, clear prefix\n```\n\n**Files:**\n- New: `src/jsh/client.sls` — client relay loop, prefix key handling\n- Modified: `src/jsh/main.sls` — dispatch to client mode\n\n**Estimated complexity:** Medium. Mostly I/O relay + keybinding dispatch.\n\n---\n\n### Phase 3: Session, Window, and Pane Management\n\n**Goal:** Full tmux-like session/window/pane lifecycle within the server.\n\n#### 3a: Sessions\n\n```\nServer maintains: *sessions* hashtable (id → session)\n *next-session-id* counter\n\nCommands:\n new-session [name] → create session + default window + pane\n kill-session [id|name] → kill all windows/panes, notify clients\n rename-session [name] → update session name\n list-sessions → return session list with metadata\n switch-session [id] → move client to different session\n```\n\n#### 3b: Windows\n\n```\nEach session has an ordered list of windows.\n\nCommands:\n new-window [name] → create window with one pane, switch to it\n kill-window [id] → kill all panes in window, remove from list\n next-window → cycle to next window\n prev-window → cycle to previous window\n select-window [N] → jump to window N\n rename-window [name] → set window title\n list-windows → interactive chooser overlay\n move-window [target] → reorder window in list\n last-window → toggle to previously active window\n```\n\n#### 3c: Panes\n\n```\nEach window has a layout tree of panes.\n\nCommands:\n split-vertical → split active pane left/right (new PTY)\n split-horizontal → split active pane top/bottom (new PTY)\n kill-pane → close pane, rebalance layout\n next-pane → cycle focus to next pane\n select-pane [dir] → move focus up/down/left/right\n resize-pane [dir] [N] → grow/shrink pane by N rows/cols\n zoom-pane → toggle fullscreen for active pane\n swap-pane [dir] → swap pane position with neighbor\n```\n\n#### 3d: Layout Engine\n\nThe layout engine recursively subdivides the window area:\n\n```\nWindow (80x24, minus 1 row for status bar = 80x23 usable)\n\nHorizontal split (50/50):\n┌─────────────────────┬─────────────────────┐\n│ Pane 0 (40x23) │ Pane 1 (39x23) │\n│ │ │\n└─────────────────────┴─────────────────────┘\n\nThen vertical split pane 1 (50/50):\n┌─────────────────────┬─────────────────────┐\n│ Pane 0 (40x23) │ Pane 1 (39x11) │\n│ ├─────────────────────┤\n│ │ Pane 2 (39x11) │\n└─────────────────────┴─────────────────────┘\n```\n\n**Layout recalculation triggers:**\n- Client RESIZE (terminal size changed)\n- Pane created or destroyed\n- Manual resize-pane command\n- Zoom/unzoom\n\n**Files:**\n- New: `src/jsh/session.sls` — session/window/pane CRUD\n- New: `src/jsh/layout.sls` — layout tree, split/merge, resize\n- Modified: `src/jsh/server.sls` — integrate session manager\n\n**Estimated complexity:** Large. Layout engine is the trickiest part.\n\n---\n\n### Phase 4: Screen Rendering\n\n**Goal:** Server composites all visible panes into a single screen buffer and sends\nit to attached clients.\n\n#### Virtual Terminal Emulator\n\nEach pane needs a virtual terminal (VT) that interprets the PTY output and maintains\na character grid — the same role that xterm/kitty/alacritty play, but in-process.\n\n```\nPTY master output bytes\n │\n ▼\n┌────────────────────────────────────────┐\n│ VT Parser (ANSI/xterm escape decoder) │\n│ - CSI sequences (cursor, color, etc.) │\n│ - OSC sequences (title, clipboard) │\n│ - SGR (text attributes) │\n│ - DEC private modes │\n└────────────────────────────────────────┘\n │\n ▼\n┌────────────────────────────────────────┐\n│ Cell Grid (cols × rows) │\n│ Each cell: {char, fg, bg, attrs} │\n│ + cursor position │\n│ + scrollback ring buffer │\n└────────────────────────────────────────┘\n```\n\n**VT state per pane:**\n\n```scheme\n(defstruct vt\n grid: vector ; vector of rows, each row = vector of cells\n cols: fixnum\n rows: fixnum\n cursor-x: fixnum\n cursor-y: fixnum\n saved-cursor: (cons fixnum fixnum) ; for DECSC/DECRC\n fg: fixnum ; current foreground color\n bg: fixnum ; current background color\n attrs: fixnum ; bold, underline, reverse, etc. bitmask\n scrollback: vector ; ring buffer of past rows\n scroll-top: fixnum ; scroll region top\n scroll-bot: fixnum ; scroll region bottom\n charset: symbol ; G0/G1 charset\n modes: fixnum ; DEC private mode bits\n title: string ; window title (from OSC 0/2)\n alt-grid: (or #f vector)) ; alternate screen buffer\n\n(defstruct cell\n char: char\n fg: fixnum ; 256-color or true-color index\n bg: fixnum\n attrs: fixnum) ; bold(1) underline(2) reverse(4) dim(8) italic(16) strikethrough(32)\n```\n\n#### Screen Compositor\n\n```\nFor each attached client:\n 1. Determine visible session → window → pane layout\n 2. For each visible pane:\n a. Read pane's VT grid\n b. Map pane cells to screen coordinates (pane.x, pane.y offset)\n 3. Draw pane borders (│, ─, ┼, etc.)\n 4. Draw status bar (bottom row)\n 5. Diff against last-sent screen buffer\n 6. Generate minimal ANSI escape sequence to update only changed cells\n 7. Send OUTPUT message to client\n```\n\n**Differential rendering:**\n\n```\nlast_screen[row][col] vs current_screen[row][col]\n │\n ├─ Same cell → skip\n └─ Different → emit: CSI row;col H (move cursor)\n SGR attrs (set colors/attrs)\n char (print character)\n```\n\nThis minimizes bandwidth and keeps the client responsive.\n\n**Files:**\n- New: `src/jsh/vt.sls` — VT100/xterm terminal emulator (escape parser + cell grid)\n- New: `src/jsh/screen.sls` — screen compositor, diff renderer, status bar\n- Modified: `src/jsh/server.sls` — integrate rendering into event loop\n\n**Estimated complexity:** Large. VT emulator is significant but well-specified.\n\n---\n\n### Phase 5: Copy Mode & Scrollback\n\n**Goal:** `Ctrl-b [` enters a mode where the user can scroll through pane history\nand copy text, like tmux copy mode.\n\n**Copy mode keybindings (vi-style, matching tmux):**\n\n| Key | Action |\n|-----|--------|\n| `q` / `Escape` | Exit copy mode |\n| `h/j/k/l` | Cursor movement |\n| `Ctrl-u/d` | Page up/down |\n| `g/G` | Top/bottom of scrollback |\n| `Space` | Start selection |\n| `Enter` | Copy selection, exit copy mode |\n| `/` | Search forward |\n| `?` | Search backward |\n| `n/N` | Next/prev search match |\n| `w/b` | Word forward/backward |\n| `0/$` | Line start/end |\n\n**Scrollback buffer:**\n\n```\nPer pane: ring buffer of N rows (default 2000, configurable)\n\n┌─────────────────────────────────┐\n│ scrollback[0] (oldest) │ ← scroll-start\n│ scrollback[1] │\n│ ... │\n│ scrollback[N-1] (most recent) │ ← scroll-end\n├─────────────────────────────────┤\n│ grid[0] (visible top) │ ← viewport top\n│ ... │\n│ grid[rows-1] (visible bot) │ ← viewport bottom\n└─────────────────────────────────┘\n```\n\nIn copy mode, the viewport shifts up into scrollback. The VT grid freezes\n(new output buffered, not displayed until copy mode exits).\n\n**Files:**\n- New: `src/jsh/copymode.sls` — copy mode input handling, selection, search\n- Modified: `src/jsh/vt.sls` — scrollback ring buffer integration\n- Modified: `src/jsh/screen.sls` — render scrollback viewport + selection highlight\n\n**Estimated complexity:** Medium.\n\n---\n\n### Phase 6: Status Bar & Chrome\n\n**Goal:** Render a tmux-style status bar and pane borders.\n\n**Status bar layout:**\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ [0:jsh] [1:vim]* [2:make] \"session-name\" 2026-03-19 14:30 │\n└─────────────────────────────────────────────────────────────────────┘\n ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^\n window list (* = active) session name date/time\n```\n\n**Pane borders:**\n\n```\nActive pane border: bright green (configurable)\nInactive pane border: dim gray\n\n│ ← vertical border\n─ ← horizontal border\n┌┐└┘┬┴├┤┼ ← corner/junction characters\n```\n\n**Configurable via shell variables:**\n\n```bash\nJSH_MUX_STATUS_LEFT='[#S] #W' # session name, window list\nJSH_MUX_STATUS_RIGHT='%Y-%m-%d %H:%M'\nJSH_MUX_STATUS_BG=colour235\nJSH_MUX_STATUS_FG=colour136\nJSH_MUX_PREFIX=C-b # prefix key (default Ctrl-b)\nJSH_MUX_SCROLLBACK=2000 # scrollback lines\nJSH_MUX_PANE_BORDER_ACTIVE=green\nJSH_MUX_PANE_BORDER_INACTIVE=colour240\n```\n\n**Files:**\n- New: `src/jsh/statusbar.sls` — status bar formatting and rendering\n- Modified: `src/jsh/screen.sls` — integrate borders and status bar\n\n**Estimated complexity:** Small-medium.\n\n---\n\n### Phase 7: Persistence & Resurrection\n\n**Goal:** Server state survives crashes. On restart, sessions can be partially recovered.\n\n**Server state file:** `/run/user/$UID/jsh/NAME.state` (or `~/.jsh/NAME.state`)\n\n```scheme\n;; Serialized on clean shutdown and periodically (every 30s)\n(server-state\n (sessions\n ((id 0) (name \"work\") (cwd \"/home/user/project\")\n (windows\n ((id 0) (name \"editor\") (panes ((cmd \"vim\" \"main.c\"))))\n ((id 1) (name \"build\") (panes ((cmd \"make\" \"watch\")))))))\n (env ((EDITOR . \"vim\") (PAGER . \"less\"))))\n```\n\n**On server restart after crash:**\n1. Read state file\n2. Recreate sessions/windows with same names\n3. Re-execute saved commands in new PTYs (best-effort)\n4. Notify re-attaching clients that session was resurrected\n\n**Files:**\n- New: `src/jsh/persist.sls` — state serialization/deserialization\n- Modified: `src/jsh/server.sls` — periodic state save, crash recovery\n\n**Estimated complexity:** Small-medium.\n\n---\n\n### Phase 8: Integration with Existing jsh Features\n\n**Goal:** Wire the multiplexer into jsh's existing infrastructure.\n\n#### 8a: Recording Integration\n\nEach pane automatically records if `*recording?*` is active in the server.\nThe recorder captures per-pane events with pane ID tags:\n\n```json\n[1.5, \"o\", \"output text\", {\"pane\": 2, \"window\": 1, \"session\": 0}]\n```\n\nPlayback can filter by pane/window/session.\n\n#### 8b: Meta-Commands\n\nAdd multiplexer meta-commands alongside existing `,record` / `,play`:\n\n| Command | Action |\n|---------|--------|\n| `,mux` | Show multiplexer status |\n| `,detach` | Detach from server |\n| `,new-window [name]` | Create window |\n| `,split [-v\\|-h]` | Split pane |\n| `,sessions` | List sessions |\n| `,attach [name]` | Attach to session |\n| `,rename [name]` | Rename current window/session |\n\n#### 8c: History Sharing\n\nAll panes in a session share command history (append-mode, like `HISTFILE`\nwith `shopt -s histappend`). Cross-pane history is merged on read.\n\n#### 8d: Environment Propagation\n\nNew panes inherit the environment of the pane they were split from.\nSession-level env vars (set via `,setenv`) propagate to new windows.\n\n**Files modified:**\n- `src/jsh/recorder.sls` — add pane-aware event emission\n- `src/jsh/main.sls` — register meta-commands\n- `src/jsh/history.sls` — shared history file locking\n\n**Estimated complexity:** Medium.\n\n---\n\n## Implementation Order & Dependencies\n\n```\nPhase 0: PTY FFI ──────────────────────────────┐\n │\nPhase 1: Server Daemon ────────────────────────┤\n │\nPhase 1.5: Authentication ─────────────────────┤\n │\nPhase 2: Client Attach ────────────────────────┤\n (minimal working system at this point) │\n │\nPhase 4: VT Emulator ──┐ │\n │ │\nPhase 3: Sessions ──────┤ (can develop │\n │ in parallel) │\nPhase 6: Status Bar ────┘ │\n │\nPhase 5: Copy Mode ────────────────────────────┤\n │\nPhase 7: Persistence ──────────────────────────┤\n │\nPhase 8: Integration ──────────────────────────┘\n```\n\n**Milestone 1 (MVP):** Phases 0-2 (incl. 1.5) — single session, single pane, attach/detach with optional auth.\n**Milestone 2 (Usable):** + Phases 3-4 — multiple windows/panes with proper rendering.\n**Milestone 3 (Complete):** + Phases 5-8 — copy mode, persistence, full integration.\n\n---\n\n## FFI Additions Summary\n\nAll new C functions needed in `ffi-shim.c`:\n\n```c\n// PTY management\nint ffi_pty_open(void);\nchar* ffi_pty_slave_name(int master_fd);\nint ffi_pty_open_slave(int master_fd);\nvoid ffi_pty_set_size(int master_fd, int cols, int rows);\n\n// Daemon support\nint ffi_daemonize(void);\nint ffi_write_pidfile(const char* path);\nint ffi_read_pidfile(const char* path);\nint ffi_process_alive(int pid); // kill(pid, 0)\nint ffi_unlink(const char* path); // already exists\n\n// Poll/select for event loop\nint ffi_poll(int* fds, int nfds, int timeout_ms, int* revents);\n// or: use existing ffi-byte-ready? in a loop with short timeout\n\n// Authentication & Encryption (Phase 1.5)\nint ffi_argon2id_hash(const char* pw, int pwlen, const uint8_t* salt,\n int saltlen, int t, int m, int p,\n uint8_t* out, int outlen);\nint ffi_hmac_sha256(const uint8_t* key, int keylen,\n const uint8_t* msg, int msglen, uint8_t* out);\nint ffi_constant_time_compare(const uint8_t* a, const uint8_t* b, int len);\nint ffi_getrandom(uint8_t* buf, int len);\nint ffi_peercred_uid(int sockfd);\nint ffi_x25519_keypair(uint8_t* pub, uint8_t* priv);\nint ffi_x25519_shared(const uint8_t* priv, const uint8_t* pub, uint8_t* out);\n// Note: explicit_bzero + ChaCha20-Poly1305 already exist in ffi-shim.c\n```\n\n---\n\n## New Module Summary\n\n| Module | Purpose | Lines (est.) |\n|--------|---------|-------------|\n| `src/jsh/server.sls` | Server daemon, event loop, client manager | ~800 |\n| `src/jsh/client.sls` | Client attach mode, prefix key handling | ~400 |\n| `src/jsh/mux-protocol.sls` | Wire protocol encode/decode | ~200 |\n| `src/jsh/mux-auth.sls` | Auth protocol, Argon2id hashing, challenge/proof, encrypted channel | ~400 |\n| `src/jsh/session.sls` | Session/window/pane lifecycle | ~500 |\n| `src/jsh/layout.sls` | Pane layout tree, split/merge/resize | ~400 |\n| `src/jsh/vt.sls` | VT100/xterm terminal emulator | ~1200 |\n| `src/jsh/screen.sls` | Screen compositor, diff renderer | ~600 |\n| `src/jsh/copymode.sls` | Copy mode, scrollback nav, selection | ~400 |\n| `src/jsh/statusbar.sls` | Status bar formatting | ~200 |\n| `src/jsh/persist.sls` | Server state serialization | ~200 |\n| **Total** | | **~5300** |\n\n---\n\n## Risks & Mitigations\n\n| Risk | Impact | Mitigation |\n|------|--------|------------|\n| VT emulator complexity | High — incomplete escape handling causes rendering glitches | Start with a minimal subset (CSI cursor/SGR/erase), add escapes incrementally. Use vttest for validation. |\n| PTY lifecycle bugs | Medium — leaked PTY fds, zombie children | Careful fd tracking in pane struct. SIGCHLD handler reaps all. Periodic audit of /proc/self/fd. |\n| Race conditions in event loop | Medium — concurrent client input + PTY output | Single-threaded event loop (no threading). All I/O via poll/select. |\n| Performance of screen diffing | Low-medium — large terminals with rapid output | Dirty-rectangle tracking. Rate-limit OUTPUT messages (max 60 fps). Batch PTY reads. |\n| Client crash leaves stale state | Low — orphaned socket connection | Server detects client EOF on socket read. Periodic PING/PONG with timeout. |\n| Chez Scheme threading model | Medium — foreign thread callbacks | Keep FFI calls non-blocking. Use poll with timeouts, never blocking reads on multiple fds from Scheme. |\n| Argon2id library dependency | Low — need libargon2 or embedded impl | Use system libargon2-dev if available, else embed reference C impl (~500 lines). Already have crypto primitives in embed-crypto.c. |\n| Auth bypass via socket steal | Low — attacker with same UID could connect | SO_PEERCRED validates UID. Socket dir is mode 0700. Password adds second factor beyond Unix permissions. |\n| DoS via auth flooding | Low — attacker spams connections | Rate limit per UID via SO_PEERCRED. Lockout after 5 failures. Server stays responsive for PTY I/O during auth delays. |\n\n---\n\n## Testing Strategy\n\n1. **Unit tests:** VT emulator escape parsing, layout tree operations, protocol encode/decode\n2. **Integration tests:** Start server, attach client, type commands, verify output\n3. **Stress tests:** Rapid window create/destroy, large scrollback, many concurrent clients\n4. **Compatibility tests:** Run `vttest` inside a pane, verify correct rendering\n5. **Crash recovery:** Kill server with SIGKILL, restart, verify state file resurrection\n6. **Binary tests:** Add mux-specific tests to `test-binary.sh` (start server, attach, detach, verify)\n7. **Auth tests:** Password set/verify round-trip, wrong password rejection, lockout after 5 failures, rate limit timing, password change while clients connected, `--password-file` mode, no-auth server skips handshake\n\n---\n\n## Configuration\n\nAll configuration via shell variables (no separate config file):\n\n```bash\n# In ~/.jshrc or ~/.profile\nexport JSH_MUX_PREFIX='C-b' # prefix key (default: Ctrl-b)\nexport JSH_MUX_SCROLLBACK=10000 # scrollback lines per pane\nexport JSH_MUX_MOUSE=on # mouse support (clicks select pane)\nexport JSH_MUX_STATUS=on # show status bar\nexport JSH_MUX_STATUS_POSITION=bottom\nexport JSH_MUX_BASE_INDEX=0 # window numbering starts at 0\nexport JSH_MUX_ESCAPE_TIME=500 # ms to wait after prefix key\nexport JSH_MUX_HISTORY_LIMIT=50000\nexport JSH_MUX_DEFAULT_SHELL=jsh # shell to spawn in new panes\nexport JSH_MUX_SOCKET_DIR=/run/user/$UID/jsh\nexport JSH_MUX_ENCRYPT=on # encrypt client↔server traffic (default: on)\n # \"off\" disables for trusted local sockets\nexport JSH_MUX_AUTH_MAX_ATTEMPTS=5 # failed attempts before lockout\nexport JSH_MUX_AUTH_LOCKOUT_SEC=60 # lockout duration\nexport JSH_MUX_PASSWORD= # for non-interactive attach (scripts/CI)\n```\n"} {"text":";; FILE: jerboa-shell/gen-embed.ss\n#!chezscheme\n;;; gen-embed.ss — Generate src/jsh/embed-data.sls from embed/ directory\n;;;\n;;; Scans the embed/ directory (or JSH_EMBED_DIR) recursively and generates\n;;; a Scheme library containing a hashtable mapping relative paths to bytevectors.\n;;;\n;;; When JSH_EMBED_ENCRYPT=1, prompts for a passphrase and encrypts each file with ChaCha20-Poly1305\n;;; using a key derived from the passphrase via PBKDF2-HMAC-SHA256.\n;;; The salt is generated per-build and stored in the library.\n;;;\n;;; Usage: LD_LIBRARY_PATH=. scheme -q < gen-embed.ss\n\n(import (chezscheme))\n\n;; Pick the right shared-library extension at runtime: macOS uses .dylib,\n;; everything else (Linux, FreeBSD, etc.) uses .so. Try .dylib first when it\n;; actually exists on disk so this works regardless of machine-type encoding.\n(define (resolve-shared-lib base-path)\n (cond\n [(file-exists? (string-append base-path \".dylib\"))\n (string-append base-path \".dylib\")]\n [(file-exists? (string-append base-path \".so\"))\n (string-append base-path \".so\")]\n [else (string-append base-path \".so\")])) ; let load-shared-object fail with a useful error\n\n(define embed-dir\n (or (getenv \"JSH_EMBED_DIR\") \"embed\"))\n\n(define output-file \"src/jsh/embed-data.sls\")\n\n;; Encryption: auto-enable when embedding from ~/.embed/ (override with JSH_EMBED_ENCRYPT=0 or =1)\n(define embed-encrypt?\n (let ([flag (getenv \"JSH_EMBED_ENCRYPT\")])\n (cond\n [(equal? flag \"1\") #t]\n [(equal? flag \"0\") #f]\n [else\n ;; Default: encrypt if embed-dir is under $HOME/.embed\n (let ([home (getenv \"HOME\")])\n (and home\n (let ([dot-embed (string-append home \"/.embed\")])\n (and (>= (string-length embed-dir) (string-length dot-embed))\n (string=? (substring embed-dir 0 (string-length dot-embed))\n dot-embed)))))])))\n\n(when embed-encrypt?\n (display (string-append \"*** Encryption enabled for \" embed-dir \"\\n\")\n (current-error-port)))\n\n;; Resolve the Rust native library (libjerboa_native.{so,dylib}). The\n;; same library now provides the embed_* crypto symbols (ring-backed)\n;; AND the X25519 symbols used below. Order: $JERBOA_NATIVE_LIB env,\n;; then the vendored sparse crate.\n(define (resolve-jerboa-native-lib)\n (let ([env (getenv \"JERBOA_NATIVE_LIB\")]\n [vendor \"vendor/jerboa-native-rs/target/release/libjerboa_native\"])\n (cond\n [(and env (file-exists? env)) env]\n [(file-exists? (string-append vendor \".dylib\"))\n (string-append vendor \".dylib\")]\n [(file-exists? (string-append vendor \".so\"))\n (string-append vendor \".so\")]\n [else #f])))\n\n;; Load libjerboa_native for embed encryption. Replaces the historical\n;; hand-rolled embed-crypto.c (L-1, W-1): every crypto primitive\n;; (PBKDF2, ChaCha20-Poly1305 AEAD, random_bytes) plus the tty\n;; passphrase reader is now served by ring + libc inside Rust.\n(define crypto-lib\n (and embed-encrypt?\n (guard (e [#t\n (display \"Error: Cannot load libjerboa_native.{so,dylib} for encryption\\n\"\n (current-error-port))\n (display \"Build it: (cd vendor/jerboa-native-rs && cargo build --release --no-default-features --features 'tls crypto')\\n\"\n (current-error-port))\n (exit 1)])\n (let ([p (resolve-jerboa-native-lib)])\n (unless p\n (display \"Error: libjerboa_native not found in vendor/jerboa-native-rs\\n\"\n (current-error-port))\n (exit 1))\n (load-shared-object p)))))\n\n;; Prompt for passphrase on /dev/tty (no echo), verify with second prompt\n(define embed-key\n (and embed-encrypt?\n (let ([buf (make-bytevector 256)]\n [buf2 (make-bytevector 256)]\n [read-pass (foreign-procedure \"embed_read_passphrase\"\n (string u8* int) int)])\n (let ([len (read-pass \"Embed passphrase: \" buf 256)])\n (if (<= len 0)\n (begin\n (display \"Error: No passphrase provided\\n\" (current-error-port))\n (exit 1))\n (let ([len2 (read-pass \"Confirm passphrase: \" buf2 256)])\n (let ([match? (and (= len len2)\n (let loop ([i 0])\n (or (= i len)\n (and (= (bytevector-u8-ref buf i)\n (bytevector-u8-ref buf2 i))\n (loop (+ i 1))))))])\n (unless match?\n (display \"Error: Passphrases do not match\\n\" (current-error-port))\n (exit 1))\n (let ([out (make-bytevector len)])\n (bytevector-copy! buf 0 out 0 len)\n (utf8->string out)))))))))\n\n;; FFI bindings for crypto (only used when encrypting)\n(define (crypto-random-bytes bv)\n (when embed-key\n (let ([rc ((foreign-procedure \"embed_random_bytes\" (u8* int) int)\n bv (bytevector-length bv))])\n (unless (= rc 0)\n (error 'gen-embed \"Failed to generate random bytes\")))))\n\n(define (crypto-pbkdf2 password salt iterations out)\n ((foreign-procedure \"embed_pbkdf2_sha256\"\n (u8* int u8* int unsigned-32 u8* int) void)\n password (bytevector-length password)\n salt (bytevector-length salt)\n iterations\n out (bytevector-length out)))\n\n(define (crypto-encrypt key nonce plaintext out)\n ((foreign-procedure \"embed_encrypt\"\n (u8* u8* u8* int u8*) int)\n key nonce plaintext (bytevector-length plaintext) out))\n\n;; Generate salt and derive key at build time\n(define build-salt #f)\n(define build-key #f)\n\n(when embed-key\n (set! build-salt (make-bytevector 32))\n (crypto-random-bytes build-salt)\n (set! build-key (make-bytevector 32))\n (let ([pass-bv (string->utf8 embed-key)])\n (crypto-pbkdf2 pass-bv build-salt 100000 build-key)\n (when (getenv \"JSH_EMBED_DEBUG\")\n (display \"[gen-embed-debug] pass len=\" (current-error-port))\n (display (bytevector-length pass-bv) (current-error-port))\n (display \" salt[0..15]=\" (current-error-port))\n (do ((i 0 (+ i 1))) ((= i 16))\n (fprintf (current-error-port) \"~2,'0x\" (bytevector-u8-ref build-salt i)))\n (display \" key[0..7]=\" (current-error-port))\n (do ((i 0 (+ i 1))) ((= i 8))\n (fprintf (current-error-port) \"~2,'0x\" (bytevector-u8-ref build-key i)))\n (newline (current-error-port))))\n (printf \" Encryption enabled (PBKDF2 100k iterations)~n\"))\n\n;; Encrypt a bytevector, returning nonce||tag||ciphertext\n(define (encrypt-bytevector bv)\n (let* ([nonce (make-bytevector 12)]\n [out (make-bytevector (+ 28 (bytevector-length bv)))])\n (crypto-random-bytes nonce)\n (crypto-encrypt build-key nonce bv out)\n out))\n\n;; Recursively collect all regular files under dir, returning relative paths\n(define (collect-files dir prefix)\n (if (and (file-exists? dir) (file-directory? dir))\n (let loop ([entries (directory-list dir)] [acc '()])\n (if (null? entries)\n acc\n (let* ([name (car entries)]\n [full (string-append dir \"/\" name)]\n [rel (if (string=? prefix \"\")\n name\n (string-append prefix \"/\" name))])\n (cond\n [(file-directory? full)\n (loop (cdr entries)\n (append (collect-files full rel) acc))]\n [(file-regular? full)\n (loop (cdr entries) (cons (cons rel full) acc))]\n [else (loop (cdr entries) acc)]))))\n '()))\n\n;; Reject embedding private key material in the clear. When encryption is off,\n;; any embedded file is baked into the binary verbatim and recoverable from the\n;; artifact, so fail closed on sensitive path patterns. (When encrypting, these\n;; are protected and intentionally allowed — e.g. record.key.)\n(define sensitive-embed-patterns\n '(\".ssh/\" \"id_rsa\" \"id_ed25519\" \"id_ecdsa\" \"id_dsa\"\n \"key.pem\" \"privkey\" \".key\" \"mullvad\" \"wireguard\" \"_account.txt\"))\n\n(define (substring-contains? hay needle)\n (let ([hn (string-length hay)] [nn (string-length needle)])\n (and (<= nn hn)\n (let loop ([i 0])\n (cond\n [(> (+ i nn) hn) #f]\n [(string=? (substring hay i (+ i nn)) needle) #t]\n [else (loop (+ i 1))])))))\n\n(define (path-sensitive? rel)\n (let ([low (string-downcase rel)])\n (let loop ([ps sensitive-embed-patterns])\n (cond\n [(null? ps) #f]\n [(substring-contains? low (car ps)) #t]\n [else (loop (cdr ps))]))))\n\n;; Read a file as a bytevector\n(define (read-file-bytes path)\n (let* ([port (open-file-input-port path)]\n [data (get-bytevector-all port)])\n (close-port port)\n (if (eof-object? data)\n (make-bytevector 0)\n data)))\n\n;; Format a bytevector as a Scheme literal\n(define (bytevector->scheme-literal bv)\n (let ([port (open-output-string)])\n (display \"#vu8(\" port)\n (let loop ([i 0])\n (when (< i (bytevector-length bv))\n (when (> i 0) (display \" \" port))\n (display (bytevector-u8-ref bv i) port)\n (loop (+ i 1))))\n (display \")\" port)\n (get-output-string port)))\n\n;; X25519 keypair for sealed-box session log encryption.\n;; The private key lives at <embed-dir>/record.key — generated once, persisted forever.\n;; Public key is derived from it and baked into the binary (always accessible).\n;; Private key is encrypted into the embed file table (requires ,unlock to read).\n(define record-pubkey #f)\n\n(when embed-encrypt?\n (let ([key-path (string-append embed-dir \"/record.key\")])\n ;; Load jerboa-native for X25519\n (guard (e [#t\n (display \"Warning: Cannot load libjerboa_native.{so,dylib} for X25519\\n\"\n (current-error-port))\n (display \" Session log encryption will not be available\\n\"\n (current-error-port))])\n (let ([native-path (resolve-jerboa-native-lib)])\n (when native-path\n (load-shared-object native-path)\n (if (file-exists? key-path)\n ;; Load existing private key, derive public key\n (let ([priv (read-file-bytes key-path)]\n [pub (make-bytevector 32)])\n (when (= (bytevector-length priv) 32)\n (let ([rc ((foreign-procedure \"jerboa_x25519_public_from_private\"\n (u8* int u8*) int)\n priv 32 pub)])\n (when (= rc 0)\n (set! record-pubkey pub)\n (printf \" Loaded record.key — session log encryption enabled~n\")))))\n ;; First time: generate keypair, save private key\n (let ([priv (make-bytevector 32)]\n [pub (make-bytevector 32)])\n (let ([rc ((foreign-procedure \"jerboa_x25519_generate_keypair\"\n (u8* u8*) int)\n priv pub)])\n (when (= rc 0)\n ;; Save private key to embed dir (will be encrypted with other files)\n (let ([port (open-file-output-port key-path\n (file-options no-fail) (buffer-mode block))])\n (put-bytevector port priv)\n (close-port port))\n (chmod key-path #o600)\n (set! record-pubkey pub)\n (printf \" Generated record.key — session log encryption enabled~n\")\n (printf \" Private key saved to ~a~n\" key-path))))))))))\n\n;; Main\n(let ([files (collect-files embed-dir \"\")])\n (printf \"=== Generating embed-data.sls ===~n\")\n (unless embed-key\n (let ([bad (let loop ([fs files] [acc '()])\n (cond\n [(null? fs) (reverse acc)]\n [(path-sensitive? (caar fs)) (loop (cdr fs) (cons (caar fs) acc))]\n [else (loop (cdr fs) acc)]))])\n (when (pair? bad)\n (display \"Error: refusing to embed private material WITHOUT encryption:\\n\"\n (current-error-port))\n (for-each (lambda (r) (fprintf (current-error-port) \" ~a~n\" r)) bad)\n (display \"Enable encryption (JSH_EMBED_ENCRYPT=1, or embed from ~/.embed) or remove these files.\\n\"\n (current-error-port))\n (exit 1))))\n (when embed-key\n (printf \" Encryption enabled — passphrase accepted~n\"))\n (call-with-output-file output-file\n (lambda (out)\n (display \"#!chezscheme\\n\" out)\n (display \";;; embed-data.sls — Auto-generated by gen-embed.ss. Do not edit.\\n\" out)\n (display \";;; Contains embedded file data compiled into the binary.\\n\\n\" out)\n (display \"(library (jsh embed-data)\\n\" out)\n (display \" (export %embed-file-table %embed-encrypted? %embed-salt %record-pubkey)\\n\" out)\n (display \" (import (chezscheme))\\n\\n\" out)\n\n ;; Emit encryption flag\n (fprintf out \" (define %embed-encrypted? ~a)~n~n\"\n (if embed-key \"#t\" \"#f\"))\n\n ;; Emit salt (empty bytevector if not encrypted)\n (fprintf out \" (define %embed-salt ~a)~n~n\"\n (if build-salt\n (bytevector->scheme-literal build-salt)\n \"#vu8()\"))\n\n ;; Emit record public key (always accessible, no unlock needed)\n (fprintf out \" (define %record-pubkey ~a)~n~n\"\n (if record-pubkey\n (bytevector->scheme-literal record-pubkey)\n \"#f\"))\n\n ;; Emit file table\n (display \" (define %embed-file-table\\n\" out)\n (display \" (let ((ht (make-hashtable string-hash string=?)))\\n\" out)\n (for-each\n (lambda (entry)\n (let* ([rel (car entry)]\n [full (cdr entry)]\n [data (read-file-bytes full)]\n [raw-size (bytevector-length data)]\n [stored (if embed-key (encrypt-bytevector data) data)]\n [stored-size (bytevector-length stored)])\n (if embed-key\n (printf \" ~a (~a bytes -> ~a bytes encrypted)~n\"\n rel raw-size stored-size)\n (printf \" ~a (~a bytes)~n\" rel raw-size))\n (fprintf out \" (hashtable-set! ht ~s ~a)~n\"\n rel (bytevector->scheme-literal stored))))\n (sort (lambda (a b) (string<? (car a) (car b))) files))\n ;; record.key is picked up from the embed dir by collect-files above\n (display \" ht))\\n\\n\" out)\n (display \" ) ;; end library\\n\" out))\n 'replace)\n (printf \" Generated ~a with ~a file~a~a~n\"\n output-file (length files)\n (if (= (length files) 1) \"\" \"s\")\n (if embed-key \" (encrypted)\" \"\")))\n"}