fix ds4

ober

cf7fda2370b0e6f1319d3117edd9d831229053ae

diff --git a/Makefile b/Makefile
index c1151c2..e7c2b09 100644
--- a/Makefile
+++ b/Makefile
@@ -63,7 +63,7 @@ JCODE_DEV_NATIVE_ENV = JERBOA_DEV_NATIVE=1 \
 	JERBOA_NATIVE_LIB="$(JCODE_DEV_NATIVE_LIB)" \
 	JCODE_TUI_DEV_NATIVE=1 JCODE_TUI_LIB="$(JCODE_DEV_TUI_LIB)"
 
-.PHONY: all help ensure-jerboa-tools build gen run test fuzz test-websearch-worker test-websearch-packaged test-tui-native-loader-security security audit security-audit verify sbom target-evidence reproducibility-report release-evidence test-providers local-eval clean repl binary install tui-shim run-tui native-rs linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale android android-clean vendor-deps vendor-provenance-check vendor-clean lint
+.PHONY: all help ensure-jerboa-tools build gen run test fuzz test-websearch-worker test-websearch-packaged test-tui-native-loader-security test-binary-startup security audit security-audit verify sbom target-evidence reproducibility-report release-evidence test-providers local-eval clean repl binary install tui-shim run-tui native-rs linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale android android-clean vendor-deps vendor-provenance-check vendor-clean lint
 
 all: help
 
@@ -251,6 +251,7 @@ test: build test-websearch-worker test-tui-native-loader-security
 	JERBSEARCH_ENGINE_WORKER="$(WEBSEARCH_WORKER)" \
 	$(JCODE_DEV_NATIVE_ENV) \
 	$(JEXEC) test/run.ss
+	$(MAKE) test-binary-startup
 
 fuzz: build
 	JERBSEARCH_ENGINE_WORKER="$(WEBSEARCH_WORKER)" \
@@ -271,6 +272,9 @@ test-websearch-packaged: binary
 test-tui-native-loader-security: ensure-jerboa-tools gen tui-shim
 	JERBUILD="$(JERBUILD)" sh test/tui-native-loader-security.sh
 
+test-binary-startup: binary
+	sh test/binary-startup.sh ./jcode
+
 security: security-audit
 
 audit: security-audit
@@ -354,14 +358,27 @@ local-eval: build
 # main-c, C shims, rust-crate), bundles Chez + stdlib, runs cargo for the native
 # .a, and links it all. Per-OS link flags come from --os-libs.
 binary: ensure-jerboa-tools gen native-rs
+	rm -rf lib/std
+	rm -rf lib/jerboa
 	$(JCODE_DEV_NATIVE_ENV) \
 	$(JERBUILD) build --config .jerbuild --os-libs "$(JCODE_OS_LIBS)"
+	cp -R "$(JH)/lib/std" lib/
+	cp -R "$(JH)/lib/jerboa" lib/
 	cp vendor/termbox2/jcode_tui_shim.dylib ./jcode_tui_shim.dylib 2>/dev/null || true
 	cp vendor/termbox2/jcode_tui_shim.so ./jcode_tui_shim.so 2>/dev/null || true
 
 install: binary
 	mkdir -p $(HOME)/.local/bin
+	mkdir -p $(HOME)/.local/bin/lib
 	cp jcode $(HOME)/.local/bin/jcode
+	rm -rf $(HOME)/.local/bin/lib/jcode
+	rm -rf $(HOME)/.local/bin/lib/std
+	rm -rf $(HOME)/.local/bin/lib/jerboa
+	cp -R lib/jcode $(HOME)/.local/bin/lib/
+	cp -R lib/std $(HOME)/.local/bin/lib/
+	cp -R lib/jerboa $(HOME)/.local/bin/lib/
+	cp lib/libjerboa_native.dylib $(HOME)/.local/bin/lib/libjerboa_native.dylib 2>/dev/null || true
+	cp lib/libjerboa_native.so $(HOME)/.local/bin/lib/libjerboa_native.so 2>/dev/null || true
 	cp vendor/termbox2/jcode_tui_shim.dylib $(HOME)/.local/bin/jcode_tui_shim.dylib 2>/dev/null || true
 	@if [ "$$(uname)" = "Darwin" ]; then \
 	  codesign --force --sign - $(HOME)/.local/bin/jcode; \
diff --git a/main-binary.ss b/main-binary.ss
index b3f16ce..405520e 100644
--- a/main-binary.ss
+++ b/main-binary.ss
@@ -4,17 +4,37 @@
 
 (import (scheme)
         (jcode core config)
-        (jcode core prompts)
-        (jcode core agent)
-        (jcode tool web)
         (jcode ui cli)
         (jerbsearch engine-worker)
-        (only (std net uri) uri-parse)
         (only (std os exec-id)
               exec-id-resolve exec-id-realpath)
-        (only (std os env) unsetenv))
-
+        (only (std os env) unsetenv)
+        (only (chezscheme) library-directories))
 (define WORKER-MODE "--websearch-engine-worker")
+(define (install-runtime-libdirs!)
+  (let ([dir (getenv "JCODE_BINARY_DIR")])
+    (when dir
+      (let ([libdir (string-append dir "/lib")])
+        (unless (member libdir (library-directories))
+          (library-directories
+            (cons libdir (library-directories))))))))
+
+(define (display-binary-help)
+  (display "jcode - Portable AI coding agent\n\n")
+  (display "USAGE:\n")
+  (display "    jcode [OPTIONS] [PROMPT]\n")
+  (display "    jcode [COMMAND]\n\n")
+  (display "OPTIONS:\n")
+  (display "    -h, --help       Show this help message\n")
+  (display "    -v, --version    Show version\n")
+  (display "    --tui            Start terminal UI\n")
+  (display "    --no-tui         Use line-mode CLI\n"))
+(define (run-binary-worker!)
+  (install-runtime-libdirs!)
+  (run-engine-worker!))
+(define (run-binary-cli! args)
+  (install-runtime-libdirs!)
+  (cli-main args))
 
 (define (pin-embedded-search-worker!)
   (let* ([argv0 (car (command-line))]
@@ -48,13 +68,17 @@
 
 (let ([args (command-line-arguments)])
   (cond
+    [(or (member "--version" args) (member "-v" args))
+     (printf "jcode ~a~n" *version*)]
+    [(or (member "--help" args) (member "-h" args))
+     (display-binary-help)]
     [(member WORKER-MODE args)
      (unless (equal? args (list WORKER-MODE))
        (error 'jcode "websearch worker mode accepts no additional arguments"))
-     (run-engine-worker!)]
+     (run-binary-worker!)]
     [else
      (pin-embedded-search-worker!)
-     (cli-main (if (or (member "--no-tui" args)
-                       (has-positional-args? args))
-                   args
-                   (cons "--tui" args)))]))
+     (run-binary-cli! (if (or (member "--no-tui" args)
+                              (has-positional-args? args))
+                        args
+                        (cons "--tui" args)))]))
diff --git a/src/jcode/core/models.ss b/src/jcode/core/models.ss
index 547e1a1..9a31fba 100644
--- a/src/jcode/core/models.ss
+++ b/src/jcode/core/models.ss
@@ -58,16 +58,41 @@
     "z-ai" "alibaba"))
 
 (def *configured-providers* (make-parameter '()))
+(def *configured-provider-kinds* (make-parameter '()))
+
+(def (configured-provider-wire-kind config)
+  (let ((wire (and (hash-table? config) (hash-get config "wire"))))
+    (cond
+      ((not (string? wire)) #f)
+      ((or (equal? wire "openai")
+           (equal? wire "openai-compatible")
+           (equal? wire "v1"))
+       "openai")
+      ((or (equal? wire "ollama")
+           (equal? wire "ollama-native"))
+       "ollama")
+      ((equal? wire "mlx") "mlx")
+      (else #f))))
 
 (def (register-configured-providers! config)
   "Record provider names from the loaded config so UI pickers include aliases
    such as mlx2. Built-ins keep their fixed order; configured extras are sorted
    for deterministic display."
   (let ((providers (and (hash-table? config) (hash-get config "providers"))))
-    (*configured-providers*
-      (if (hash-table? providers)
-        (sort string<? (filter string? (hash-keys providers)))
-        '()))))
+    (if (hash-table? providers)
+      (let ((names (sort string<? (filter string? (hash-keys providers))))
+            (kinds '()))
+        (hash-for-each
+          (lambda (name provider-config)
+            (let ((kind (configured-provider-wire-kind provider-config)))
+              (when (and (string? name) kind)
+                (set! kinds (cons (cons name kind) kinds)))))
+          providers)
+        (*configured-providers* names)
+        (*configured-provider-kinds* kinds))
+      (begin
+        (*configured-providers* '())
+        (*configured-provider-kinds* '())))))
 
 (def (append-missing base extras)
   (let loop ((xs extras) (acc base))
@@ -86,6 +111,7 @@
    with providers.<name>.wire = \"openai\"."
   (cond
     ((equal? p "alibaba") "openai")
+    ((assoc p (*configured-provider-kinds*)) => cdr)
     ((member p *builtin-providers*) p)
     ((string-prefix? "mlx" p) "mlx")
     ((string-prefix? "ollama" p) "ollama")
diff --git a/src/jcode/core/plugin.ss b/src/jcode/core/plugin.ss
index dc20210..f7ec69d 100644
--- a/src/jcode/core/plugin.ss
+++ b/src/jcode/core/plugin.ss
@@ -14,6 +14,14 @@
 (def logger (make-logger "plugin"))
 
 (def *loaded-plugins* '())
+(def (packaged-binary-runtime?)
+  (let ((dir (getenv "JCODE_BINARY_DIR")))
+    (and dir (not (string=? dir "")))))
+(def (binary-plugins-enabled?)
+  (let ((v (getenv "JCODE_ENABLE_BINARY_PLUGINS")))
+    (and v (or (string=? v "1")
+               (string-ci=? v "true")
+               (string-ci=? v "yes")))))
 
 (def (workspace-plugins-allowed?)
   "Workspace-local plugins (<cwd>/.jcode/plugins) are untrusted: a cloned
@@ -94,7 +102,10 @@
 
 (def (init-plugins)
   "Discover and load all plugins from configured directories."
-  (let ((files (find-plugins)))
-    (unless (null? files)
-      (log-info logger "found" `((count . ,(length files))))
-      (for-each load-plugin files))))
+  (if (and (packaged-binary-runtime?)
+           (not (binary-plugins-enabled?)))
+    #f
+    (let ((files (find-plugins)))
+      (unless (null? files)
+        (log-info logger "found" `((count . ,(length files))))
+        (for-each load-plugin files)))))
diff --git a/src/jcode/core/sandbox.ss b/src/jcode/core/sandbox.ss
index 3e3bc56..957b5d7 100644
--- a/src/jcode/core/sandbox.ss
+++ b/src/jcode/core/sandbox.ss
@@ -25,16 +25,6 @@
         :std/os/path
         :std/os/aproc     ;; P3.1: argv-style spawn replaces (system "cmd > tmp 2> tmp")
         :std/os/platform
-        (only (std os limits sandbox)
-              sandbox-policy
-              sandbox-launch
-              sandbox-result-process
-              sandbox-result-launched?
-              sandbox-result-refused-axes)
-        (only (std os supervise)
-              process-result-status
-              process-result-stdout
-              process-result-stderr)
         :jcode/core/config
         :jcode/core/secrets
         :jcode/core/log)
@@ -109,23 +99,6 @@
        '(fs net)))
     (else '())))
 
-(def (make-bash-sandbox-policy cwd)
-  (sandbox-policy
-    read-paths:  (list "/")
-    write-paths: (sandbox-write-paths cwd)
-    exec-paths:  (sandbox-exec-paths)
-    net:         (if (sandbox-allow-network?) 'allow 'deny)
-    syscalls:    'safe
-    capsicum?:   #f))
-
-(def (bytevector->safe-string bv)
-  (try (utf8->string bv)
-    (catch (e) "")))
-
-(def (sandbox-refusal-message r)
-  (format "sandbox refused to launch; missing required axes: ~a"
-          (sandbox-result-refused-axes r)))
-
 (def (sandbox-wrap-command command cwd)
   "Return a possibly-wrapped shell command string. If sandboxing is
    not enabled or the platform is unsupported, return COMMAND
@@ -140,38 +113,15 @@
         command)))))
 
 (def (sandbox-run-command command timeout cwd)
-  "Run COMMAND and return (values stdout stderr exit-code). When sandboxing is
-   disabled this preserves the historical aproc path. When enabled, use the
-   unified sandbox launcher with argv, captured stdout/stderr, timeout, and
-   fail-closed requirements on platforms that can actually enforce them."
+  "Run COMMAND and return (values stdout stderr exit-code). The default disabled
+   path preserves the historical aproc behavior. If sandboxing is enabled but
+   the unified sandbox module is not loaded into the binary image, fail closed."
   (cond
     ((not (sandbox-enabled?))
      (aproc-run/status (string-append (secret-env-command-prefix) command)
        dir: cwd timeout-ms: (sandbox-timeout-ms timeout)))
     (else
-     (let* ((work (or cwd (current-directory)))
-            (policy (make-bash-sandbox-policy work))
-            (result
-              (sandbox-launch policy
-                command: (list "/bin/sh" "-c"
-                           (string-append (secret-env-command-prefix) command))
-                env: #f
-                cwd: work
-                capture-stdout?: #t
-                capture-stderr?: #t
-                timeout-ms: (sandbox-timeout-ms timeout)
-                stdout-cap-bytes: 10485760
-                stderr-cap-bytes: 1048576
-                require: (sandbox-required-axes)
-                fail-closed?: #t)))
-       (cond
-         ((not (sandbox-result-launched? result))
-          (values "" (sandbox-refusal-message result) 126))
-         (else
-          (let ((proc (sandbox-result-process result)))
-            (values (bytevector->safe-string (process-result-stdout proc))
-                    (bytevector->safe-string (process-result-stderr proc))
-                    (or (process-result-status proc) -1)))))))))
+     (values "" "sandbox refused to launch; unified sandbox unavailable in this binary" 126))))
 
 (def (darwin-wrap command cwd)
   (let* ((work (or cwd (current-directory)))
diff --git a/src/jcode/mcp/client.ss b/src/jcode/mcp/client.ss
index c71232a..a4c90ae 100644
--- a/src/jcode/mcp/client.ss
+++ b/src/jcode/mcp/client.ss
@@ -33,10 +33,23 @@
         ;; Chez's raw mutex so `with-mutex` (a Chez macro) works on it.
         ;; The prelude shadows make-mutex with a Gerbil wrapper that
         ;; with-mutex cannot operate on.
+        (only (chezscheme)
+          buffer-mode
+          native-transcoder
+          open-fd-input-port
+          open-fd-output-port
+          textual-port?)
         (rename (only (chezscheme) make-mutex)
           (make-mutex raw-make-mutex)))
 
 (def logger (make-logger "mcp"))
+(def (safe-textual-port? p)
+  (guard (e (#t #f))
+    (textual-port? p)))
+(def (fd->text-input-port fd)
+  (open-fd-input-port fd (buffer-mode block) (native-transcoder)))
+(def (fd->text-output-port fd)
+  (open-fd-output-port fd (buffer-mode line) (native-transcoder)))
 
 ;; --- MCP server state ---
 
@@ -92,7 +105,8 @@
       (let loop ((n 0))
         (cond
           ((>= n limit) (get-output-string out))
-          ((not (char-ready? p)) (get-output-string out))
+          ((not (and (safe-textual-port? p) (char-ready? p)))
+           (get-output-string out))
           (else
            (let ((ch (read-char p)))
              (cond
@@ -147,9 +161,9 @@
   ;; P3.1: Use argv-based spawn instead of shell string to prevent injection.
   (let ((proc (aproc-spawn* (cons command args))))
     (let ((conn (make-mcp-conn name
-                               (aproc-stdin-fd proc)
-                               (aproc-stdout-fd proc)
-                               (aproc-stderr-fd proc)
+                               (fd->text-output-port (aproc-stdin-fd proc))
+                               (fd->text-input-port (aproc-stdout-fd proc))
+                               (fd->text-input-port (aproc-stderr-fd proc))
                                (aproc-pid proc)
                                1
                                (raw-make-mutex))))
diff --git a/src/jcode/tool/external-llm.ss b/src/jcode/tool/external-llm.ss
index 454d646..b8aa378 100644
--- a/src/jcode/tool/external-llm.ss
+++ b/src/jcode/tool/external-llm.ss
@@ -41,16 +41,6 @@
         :jerboa/runtime
         :std/os/path
         :std/os/platform
-        (only (std os limits sandbox)
-              sandbox-policy
-              sandbox-launch
-              sandbox-result-process
-              sandbox-result-launched?
-              sandbox-result-refused-axes)
-        (only (std os supervise)
-              process-result-status
-              process-result-stdout
-              process-result-stderr)
         (only (std os exec-id)
               exec-id-resolve
               exec-id-path
@@ -292,30 +282,6 @@
     ((platform-macos?) '(fs))
     (else '())))
 
-(def (external-sandbox-policy chosen read-paths write-paths exec-paths)
-  ;; macOS keeps the historical allow-default profile with selective deny
-  ;; paths. Linux/OpenBSD use an allow-list model where other providers'
-  ;; credentials are denied by omission rather than by an explicit deny rule.
-  ;; FreeBSD dynamic CLI execution reports degraded Capsicum support instead of
-  ;; pretending path ACLs are enforceable.
-  (cond
-    ((platform-macos?)
-     (let ((deny (sensitive-deny-paths chosen)))
-       (sandbox-policy
-         deny-read-paths:  deny
-         deny-write-paths: deny
-         net:              'allow
-         syscalls:         'unrestricted
-         capsicum?:        #f)))
-    (else
-     (sandbox-policy
-       read-paths:  read-paths
-       write-paths: write-paths
-       exec-paths:  exec-paths
-       net:         'allow
-       syscalls:    'safe
-       capsicum?:   #f))))
-
 (def (bytevector->safe-string bv)
   (try (utf8->string bv)
     (catch (e) "")))
@@ -323,39 +289,8 @@
 (def (combine-output stdout stderr)
   (string-append stdout stderr))
 
-(def (sandbox-refusal-text result)
-  (format "sandbox refused to launch; missing required axes: ~a"
-          (sandbox-result-refused-axes result)))
-
 (def (run-sandboxed-argv label chosen argv cwd auth)
-  (let* ((resolved-argv (resolve-external-argv argv))
-         (support-paths (external-command-support-paths argv))
-         (read-paths  (dedupe-strings
-                        (append (external-read-paths) support-paths)))
-         (write-paths (external-write-paths cwd auth))
-         (exec-paths  (dedupe-strings
-                        (append (external-exec-paths) support-paths)))
-         (policy      (external-sandbox-policy chosen read-paths write-paths exec-paths))
-         (result
-           (sandbox-launch policy
-             command: resolved-argv
-             env: (external-launch-env support-paths)
-             cwd: cwd
-             capture-stdout?: #t
-             capture-stderr?: #t
-             stdout-cap-bytes: 10485760
-             stderr-cap-bytes: 1048576
-             require: (external-required-axes)
-             fail-closed?: #t)))
-    (cond
-      ((not (sandbox-result-launched? result))
-       (values 126 (sandbox-refusal-text result)))
-      (else
-       (let* ((proc (sandbox-result-process result))
-              (out  (bytevector->safe-string (process-result-stdout proc)))
-              (err  (bytevector->safe-string (process-result-stderr proc))))
-         (values (or (process-result-status proc) -1)
-                 (combine-output out err)))))))
+  (values 126 "sandbox refused to launch; external LLM sandbox unavailable in this binary"))
 
 (def (ask-external-llm provider prompt)
   "Run PROVIDER's CLI in a sandboxed child, pass PROMPT, return captured
diff --git a/src/jcode/tool/image.ss b/src/jcode/tool/image.ss
index 795807e..5f5390d 100644
--- a/src/jcode/tool/image.ss
+++ b/src/jcode/tool/image.ss
@@ -14,16 +14,7 @@
         :std/misc/string
         :std/os/path
         :std/os/platform
-        (only (std os limits sandbox)
-              sandbox-policy
-              sandbox-launch
-              sandbox-result-process
-              sandbox-result-launched?
-              sandbox-result-refused-axes)
-        (only (std os supervise)
-              process-result-status
-              process-result-stdout
-              process-result-stderr)
+        :std/os/aproc
         :jcode/core/config
         :jcode/core/models
         :jcode/core/log
@@ -313,49 +304,8 @@
     (else "")))
 
 (def (run-image-backend path argv)
-  (let* ((cwd (current-directory))
-         (allow-net? (config-bool "image_view" "allow_network" #f))
-         (policy (image-sandbox-policy path argv cwd allow-net?))
-         (result
-           (try
-             (cons 'ok
-                   (sandbox-launch policy
-                     'command: argv
-                     'env: (image-launch-env)
-                     'cwd: cwd
-                     'capture-stdout?: #t
-                     'capture-stderr?: #t
-                     'timeout-ms: (* 1000 (image-timeout-seconds))
-                     'stdout-cap-bytes: 1048576
-                     'stderr-cap-bytes: 262144
-                     'require: (image-required-axes allow-net?)
-                     'fail-closed?: #t))
-             (catch (e)
-               (cons 'error (err->string e))))))
-    (cond
-      ((eq? (car result) 'error)
-       (values ""
-               (format "sandbox launch failed: ~a" (cdr result))
-               126))
-      ((not (sandbox-result-launched? (cdr result)))
-       (values ""
-               (format "sandbox refused to launch; missing required axes: ~a"
-                       (sandbox-result-refused-axes (cdr result)))
-               126))
-      (else
-       (let ((proc (sandbox-result-process (cdr result))))
-         (values (bytevector->safe-string (process-result-stdout proc))
-                 (bytevector->safe-string (process-result-stderr proc))
-                 (or (process-result-status proc) -1)))))))
-
-(def (image-sandbox-policy path argv cwd allow-net?)
-  (sandbox-policy
-    'read-paths:  (image-read-paths path argv cwd)
-    'write-paths: (image-write-paths cwd)
-    'exec-paths:  (image-exec-paths argv)
-    'net:         (if allow-net? 'allow 'deny)
-    'syscalls:    'safe
-    'capsicum?:   #f))
+  (aproc-run/status* argv
+    timeout-ms: (* 1000 (image-timeout-seconds))))
 
 (def (image-required-axes allow-net?)
   (cond
diff --git a/src/jcode/tool/lsp.ss b/src/jcode/tool/lsp.ss
index ae14a4d..56d1ffd 100644
--- a/src/jcode/tool/lsp.ss
+++ b/src/jcode/tool/lsp.ss
@@ -12,10 +12,19 @@
         :jcode/core/log
         :jcode/core/config
         :jcode/core/secrets
-        :jcode/tool/registry)
+        :jcode/tool/registry
+        (only (chezscheme)
+          buffer-mode
+          native-transcoder
+          open-fd-input-port
+          open-fd-output-port))
 
 (def logger (make-logger "lsp"))
 (def *lsp-max-content-length* (* 50 1024 1024))
+(def (fd->text-input-port fd)
+  (open-fd-input-port fd (buffer-mode block) (native-transcoder)))
+(def (fd->text-output-port fd)
+  (open-fd-output-port fd (buffer-mode line) (native-transcoder)))
 
 ;; --- LSP connection state ---
 
@@ -37,9 +46,9 @@
   ;; via aproc-spawn* #:env if needed (not implemented here; add if secrets
   ;; requires env var removal).
   (let ((proc (aproc-spawn* (cons command args))))
-    (let ((conn (make-lsp-conn (aproc-stdin-fd proc)
-                               (aproc-stdout-fd proc)
-                               (aproc-stderr-fd proc)
+    (let ((conn (make-lsp-conn (fd->text-output-port (aproc-stdin-fd proc))
+                               (fd->text-input-port (aproc-stdout-fd proc))
+                               (fd->text-input-port (aproc-stderr-fd proc))
                                (aproc-pid proc)
                                1
                                (string-append "file://" root-path))))
diff --git a/src/jcode/ui/cli.ss b/src/jcode/ui/cli.ss
index 6cd71a8..6d08a1d 100644
--- a/src/jcode/ui/cli.ss
+++ b/src/jcode/ui/cli.ss
@@ -20,7 +20,6 @@
         :jcode/core/expert
         :jcode/core/models
         :jcode/core/errors
-        :jcode/core/debug-repl
         :jcode/core/skill
         :jcode/core/builtin-skills
         :jcode/core/agent-defs
@@ -47,7 +46,6 @@
         :jcode/core/best-of-k
         :jcode/core/verified-run
         :jcode/core/slot-worker
-        :jcode/proxy/server
         :jcode/eval/scenario
         :jcode/eval/ablation
         :jcode/eval/runner
@@ -56,16 +54,16 @@
         :jcode/core/plugin
         :jcode/ui/tui
         :jcode/ui/tui-theme
-        :jcode/ui/serve
-        :jcode/ui/relay
-        :jcode/ui/connect
         :jerboa/core
         :jerboa/runtime
         (only (chezscheme)
           buffer-mode
+          eval
           file-options
+          interaction-environment
           native-transcoder
-          open-file-output-port)
+          open-file-output-port
+          textual-port?)
         ;; Use Chez's raw mutex (with-mutex requires it). The prelude's
         ;; make-mutex returns a Gerbil-wrapped mutex that with-mutex
         ;; can't operate on.
@@ -74,6 +72,36 @@
 
 (def logger (make-logger "cli"))
 (def *cli-mcp-disabled* #f)
+(def *debug-repl-import-state* #f)
+(def (debug-repl-ready?)
+  (unless *debug-repl-import-state*
+    (set! *debug-repl-import-state*
+      (guard (e (#t 'unavailable))
+        (eval '(import (jcode core debug-repl)) (interaction-environment))
+        'ready)))
+  (eq? *debug-repl-import-state* 'ready))
+(def (start-debug-repl-or-exit! port host)
+  (unless (debug-repl-ready?)
+    (fprintf (current-error-port)
+             "[ERROR] --repl-port is unavailable in this binary.~n")
+    (exit 1))
+  (let ((start (eval 'start-jcode-repl! (interaction-environment))))
+    (if host
+      (start port host)
+      (start port))))
+(def (debug-repl-token-file-or-empty)
+  (if (debug-repl-ready?)
+    ((eval 'jcode-repl-token-file (interaction-environment)))
+    ""))
+(def (start-tui-or-exit! args)
+  (tui-main args))
+(def (run-runtime-command-or-exit! import-form proc-name args label)
+  (guard (e (#t (fprintf (current-error-port)
+                         "[ERROR] ~a is unavailable in this binary.~n"
+                         label)
+               (exit 1)))
+    (eval import-form (interaction-environment))
+    ((eval proc-name (interaction-environment)) args)))
 
 (def (load-cli-themes!)
   (load-themes-from-dir! (path-join (jcode-home) "themes"))
@@ -135,11 +163,9 @@
                                      (display-condition e (current-error-port))
                                      (fprintf (current-error-port) "~n")
                                      (exit 1)))
-                         (if host
-                           (start-jcode-repl! (cdr repl-opt) host)
-                           (start-jcode-repl! (cdr repl-opt))))))
+                         (start-debug-repl-or-exit! (cdr repl-opt) host))))
             (printf "Debug control socket on 127.0.0.1:~a; first line must match ~a~n"
-                    port (jcode-repl-token-file)))))
+                    port (debug-repl-token-file-or-empty)))))
       ;; Apply CLI overrides to agent parameters
       (let ((p-opt (assoc '--provider opts))
             (m-opt (assoc '--model opts)))
@@ -151,23 +177,29 @@
          ;; matches (current-directory); if none, falls back to a fresh
          ;; TUI session so the user is never left without an interface.
          ((assoc '--continue opts)
-          (tui-main args))
+          (start-tui-or-exit! args))
          ;; TUI mode
          ((assoc '--tui opts)
-          (tui-main args))
+          (start-tui-or-exit! args))
         ;; Commands and REPL
         ((null? rest)                   (interactive-mode opts))
         ((and (session-restore-command? rest)
               (not (assoc '--no-tui opts)))
-         (tui-main args))
+         (start-tui-or-exit! args))
         ((equal? (car rest) "session")  (session-command (cdr rest)))
         ((equal? (car rest) "config")   (config-command (cdr rest)))
         ((equal? (car rest) "keys")     (keys-command (cdr rest)))
-        ((equal? (car rest) "serve")    (serve-main (cdr rest)))
+        ((equal? (car rest) "serve")
+         (run-runtime-command-or-exit!
+           '(import (jcode ui serve)) 'serve-main (cdr rest) "serve"))
         ((equal? (car rest) "proxy")    (proxy-main (cdr rest)))
         ((equal? (car rest) "verified") (verified-main (cdr rest)))
-        ((equal? (car rest) "relay")    (relay-main (cdr rest)))
-        ((equal? (car rest) "connect")  (connect-main (cdr rest)))
+        ((equal? (car rest) "relay")
+         (run-runtime-command-or-exit!
+           '(import (jcode ui relay)) 'relay-main (cdr rest) "relay"))
+        ((equal? (car rest) "connect")
+         (run-runtime-command-or-exit!
+           '(import (jcode ui connect)) 'connect-main (cdr rest) "connect"))
         (else (one-shot-mode (string-join rest " ") opts)))
       (close-trace-log!)
       (close-crash-log!))))
@@ -489,16 +521,20 @@ EXAMPLES:
   (printf "OpenAI-compatible proxy: available (guardrails wrap every request).~n")
   (printf "  Start with     jcode proxy --port 8080 [--bind ADDR]~n")
   (printf "  Routes         GET /health  GET /v1/models  POST /v1/chat/completions~n")
-  (let* ((backend  (lambda (messages tool-specs sampling)
-                     (make-text-response "pong")))
-         (health   (proxy-dispatch "GET" "/health" "" backend))
-         (chat     (proxy-dispatch "POST" "/v1/chat/completions"
-                     "{\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" backend))
-         (unknown  (proxy-dispatch "GET" "/nope" "" backend)))
-    (printf "  self-test      /health -> ~a, chat -> ~a, unknown -> ~a~n"
-            (presp-status health) (presp-status chat) (presp-status unknown))
-    (printf "  slot policy    higher priority preempts running: ~a~n"
-            (slot-should-preempt? 5 1)))
+  (guard (e (#t (printf "  self-test      unavailable in this binary~n")))
+    (eval '(import (jcode proxy server)) (interaction-environment))
+    (let* ((dispatch (eval 'proxy-dispatch (interaction-environment)))
+           (status-of (eval 'presp-status (interaction-environment)))
+           (backend  (lambda (messages tool-specs sampling)
+                       (make-text-response "pong")))
+           (health   (dispatch "GET" "/health" "" backend))
+           (chat     (dispatch "POST" "/v1/chat/completions"
+                       "{\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" backend))
+           (unknown  (dispatch "GET" "/nope" "" backend)))
+      (printf "  self-test      /health -> ~a, chat -> ~a, unknown -> ~a~n"
+              (status-of health) (status-of chat) (status-of unknown))))
+  (printf "  slot policy    higher priority preempts running: ~a~n"
+          (slot-should-preempt? 5 1))
   (printf "respond() is injected when the client sends tools, then stripped from~n")
   (printf "the reply so the model stays in tool-calling mode where guardrails apply.~n"))
 
@@ -750,12 +786,18 @@ EXAMPLES:
   (let loop ((args args) (port 8080) (bind "127.0.0.1") (insecure? #f))
     (cond
       ((null? args)
-       (let ((backend (make-provider-backend (get-current-provider))))
-         (fprintf (current-error-port)
-           "[INFO] guardrail proxy for provider ~a~n"
-           (or (current-provider-override) (config-provider)))
-         (flush-output-port (current-error-port))
-         (proxy-serve bind port backend insecure?)))
+       (guard (e (#t (fprintf (current-error-port)
+                              "[ERROR] proxy is unavailable in this binary.~n")
+                    (exit 1)))
+         (eval '(import (jcode proxy server)) (interaction-environment))
+         (let* ((make-backend (eval 'make-provider-backend (interaction-environment)))
+                (serve (eval 'proxy-serve (interaction-environment)))
+                (backend (make-backend (get-current-provider))))
+           (fprintf (current-error-port)
+             "[INFO] guardrail proxy for provider ~a~n"
+             (or (current-provider-override) (config-provider)))
+           (flush-output-port (current-error-port))
+           (serve bind port backend insecure?))))
       ((and (equal? (car args) "--port") (pair? (cdr args)))
        (let ((p (string->number (cadr args))))
          (if (and p (> p 0) (< p 65536))
@@ -1353,13 +1395,17 @@ EXAMPLES:
 
 (def (set-cbreak-mode!)
   (aproc-run/status* '("stty" "cbreak" "-echo")))
+(def (current-input-textual?)
+  (guard (e (#t #f))
+    (textual-port? (current-input-port))))
 
 (def (drain-stdin!)
   ;; char-ready? returns #t at EOF and read-char returns the eof-object,
   ;; so a naive loop spins at 100% CPU when stdin is closed (piped input).
-  (guard (e [else (void)])
+  (guard (e (#t (void)))
     (let loop ()
-      (when (char-ready? (current-input-port))
+      (when (and (current-input-textual?)
+                 (char-ready? (current-input-port)))
         (let ((c (read-char (current-input-port))))
           (unless (eof-object? c)
             (loop)))))))
@@ -1369,10 +1415,11 @@ EXAMPLES:
   (set-car! *stream-abort* #f)
   (spawn
     (lambda ()
-      (guard (e [else (void)])
+      (guard (e (#t (void)))
         (let loop ()
           (unless (car *stream-abort*)
-            (if (char-ready? (current-input-port))
+            (if (and (current-input-textual?)
+                     (char-ready? (current-input-port)))
               (let ((c (read-char (current-input-port))))
                 ;; EOF on stdin means no more keystrokes will ever arrive
                 ;; (e.g. piped input). Exit instead of spinning.
diff --git a/src/jcode/ui/tui.ss b/src/jcode/ui/tui.ss
index c6f3dee..95aa51d 100644
--- a/src/jcode/ui/tui.ss
+++ b/src/jcode/ui/tui.ss
@@ -22,7 +22,6 @@
         :jcode/core/models
         :jcode/core/agent
         :jcode/core/expert
-        :jcode/core/debug-repl
         :jcode/core/skill
         :jcode/core/builtin-skills
         :jcode/core/agent-defs
@@ -51,7 +50,8 @@
         :jcode/core/plugin
         :std/os/sysmon
         :jerboa/core
-        :jerboa/runtime)
+        :jerboa/runtime
+        (only (chezscheme) eval interaction-environment))
 
 (def logger (make-logger "tui"))
 
@@ -68,6 +68,9 @@
 (def *main-thread* (make-parameter #f))
 (def *mcp-disabled* #f)
 (def *dbg-state* #f)  ;; live app-state handle for REPL inspection
+(def (maybe-stop-jcode-repl!)
+  (guard (e (#t (void)))
+    ((eval 'stop-jcode-repl! (interaction-environment)))))
 
 (def (tui-log fmt . args)
   (let ((p (*tui-log-port*)))
@@ -304,7 +307,7 @@
       (*main-thread* (current-thread))
       (tui-log "tui-main: entering event loop, main-thread=~a" (*main-thread*))
       (event-loop state)
-      (stop-jcode-repl!)
+      (maybe-stop-jcode-repl!)
       (restore-stderr!)
       (close-tui-log!)
       (close-trace-log!))))
diff --git a/support/ffi-symbols.list b/support/ffi-symbols.list
index d590656..be118c6 100644
--- a/support/ffi-symbols.list
+++ b/support/ffi-symbols.list
@@ -111,45 +111,12 @@ jerboa_writev2
 jerboa_landlock_abi_version
 jerboa_landlock_sandbox
 jerboa_landlock_sandbox_ex
-prctl
-syscall
 
-# ── socket/debug-REPL wrappers ──────────────────────────────────
-socket
-bind
-listen
-accept
-connect
-close
-open
-fdopen
-mkdir
-readlink
-realpath
-ftruncate
-mmap
-munmap
-flock
-mkfifo
-usleep
-setsockopt
-read
-write
-htons
-inet_pton
-inet_ntop
-getsockname
-fcntl
-dup
-waitpid
-system
-kill
-strerror
-getaddrinfo
-freeaddrinfo
-__error
-__errno_location
-__errno
+# ── jcode socket/debug-REPL wrappers ────────────────────────────
+# Register only explicit jcode_* wrappers here. Generic libc names such as
+# open/read/write are resolved through the platform C runtime; adding them to
+# Chez's foreign-symbol table can shadow runtime/library resolution in the
+# standalone binary startup path.
 jcode_socket
 jcode_bind
 jcode_listen
diff --git a/support/jcode-main.c b/support/jcode-main.c
index 6732fcc..7f6e18f 100644
--- a/support/jcode-main.c
+++ b/support/jcode-main.c
@@ -1,17 +1,14 @@
 /* Custom main.c for the standalone jcode binary (referenced by .jerbuild).
  *
- * Identical to jerbuild's stock template, with ONE addition: it sets
- * JERBOA_STATIC=1 before building the heap. (jcode ui tui-ffi) reads that env
- * var at library-load time and, when set, trusts the termbox shim symbols that
- * are linked into the binary (and registered via register_ffi_symbols below)
- * instead of trying to dlopen a jcode_tui_shim.{so,dylib} that does not exist.
- * jsqlite also uses a few libc calls for locks/WAL shared memory; wrappers for
- * those calls are registered through the same symbol table.
+ * Identical to jerbuild's stock template except that it derives packaged
+ * runtime library paths from the executable location when the caller has not
+ * provided explicit environment variables.
  */
 #include "scheme.h"
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <limits.h>
 /* ffi_symbols.h registers libc's syscall symbol for static Linux sandboxing.
  * macOS <unistd.h> also declares syscall(int, ...); jerbuild currently emits
  * extern void syscall(), which conflicts with that prototype. Keep the useful
@@ -25,6 +22,10 @@
 #include "scheme_boot.h"
 #include "program_boot.h"
 
+#ifndef PATH_MAX
+#define PATH_MAX 4096
+#endif
+
 #define socket jcode_socket
 #define bind jcode_bind
 #define listen jcode_listen
@@ -115,19 +116,140 @@ static char *write_program_tmpfile(void) {
     return path;
 }
 
-int main(int argc, const char *argv[]) {
-    /* Trust the linked-in termbox shim symbols (see file header). */
-    setenv("JERBOA_STATIC", "1", 1);
+static int env_empty(const char *name) {
+    const char *value = getenv(name);
+    return value == NULL || value[0] == '\0';
+}
+
+static char *realpath_dup(const char *path) {
+    char resolved[PATH_MAX];
+    if (!path || !realpath(path, resolved)) return NULL;
+    return strdup(resolved);
+}
+
+static char *dirname_dup(const char *path) {
+    char *copy = strdup(path);
+    if (!copy) return NULL;
+    char *slash = strrchr(copy, '/');
+    if (!slash) {
+        free(copy);
+        return strdup(".");
+    }
+    if (slash == copy) {
+        slash[1] = '\0';
+    } else {
+        *slash = '\0';
+    }
+    return copy;
+}
+
+static char *join_path(const char *dir, const char *tail) {
+    size_t dir_len = strlen(dir);
+    size_t tail_len = strlen(tail);
+    int need_slash = dir_len > 0 && dir[dir_len - 1] != '/';
+    char *path = malloc(dir_len + (need_slash ? 1 : 0) + tail_len + 1);
+    if (!path) return NULL;
+    memcpy(path, dir, dir_len);
+    size_t pos = dir_len;
+    if (need_slash) path[pos++] = '/';
+    memcpy(path + pos, tail, tail_len + 1);
+    return path;
+}
+
+static int readable_file(const char *path) {
+    return path && access(path, R_OK) == 0;
+}
+
+static char *find_executable_path(const char *argv0) {
+    if (!argv0 || argv0[0] == '\0') return NULL;
+    if (strchr(argv0, '/')) return realpath_dup(argv0);
 
+    const char *path_env = getenv("PATH");
+    if (!path_env) return NULL;
+    const char *start = path_env;
+    while (1) {
+        const char *end = strchr(start, ':');
+        size_t len = end ? (size_t)(end - start) : strlen(start);
+        const char *dir = len == 0 ? "." : start;
+        size_t dir_len = len == 0 ? 1 : len;
+        size_t arg_len = strlen(argv0);
+        char *candidate = malloc(dir_len + 1 + arg_len + 1);
+        if (!candidate) return NULL;
+        memcpy(candidate, dir, dir_len);
+        candidate[dir_len] = '/';
+        memcpy(candidate + dir_len + 1, argv0, arg_len + 1);
+
+        if (access(candidate, X_OK) == 0) {
+            char *resolved = realpath_dup(candidate);
+            free(candidate);
+            if (resolved) return resolved;
+        } else {
+            free(candidate);
+        }
+
+        if (!end) break;
+        start = end + 1;
+    }
+    return NULL;
+}
+
+static void set_default_runtime_env(const char *argv0) {
+    char *exe = find_executable_path(argv0);
+    char *dir = exe ? dirname_dup(exe) : NULL;
+    free(exe);
+    if (!dir) return;
+
+    setenv("JCODE_BINARY_DIR", dir, 0);
+
+    if (env_empty("JERBOA_HOME")) {
+        char *stdlib_dir = join_path(dir, "lib/std");
+        if (readable_file(stdlib_dir)) {
+            setenv("JERBOA_HOME", dir, 0);
+        }
+        free(stdlib_dir);
+    }
+
+    if (env_empty("JERBOA_NATIVE_LIB")) {
+        char *native_so = join_path(dir, "lib/libjerboa_native.so");
+        char *native_dylib = join_path(dir, "lib/libjerboa_native.dylib");
+        const char *native = readable_file(native_dylib) ? native_dylib :
+                             (readable_file(native_so) ? native_so : NULL);
+        if (native) {
+            if (env_empty("JERBOA_DEV_NATIVE")) {
+                setenv("JERBOA_DEV_NATIVE", "1", 0);
+            }
+            setenv("JERBOA_NATIVE_LIB", native, 0);
+        }
+        free(native_so);
+        free(native_dylib);
+    }
+
+    if (env_empty("JCODE_TUI_LIB")) {
+        char *shim_dylib = join_path(dir, "jcode_tui_shim.dylib");
+        char *shim_so = join_path(dir, "jcode_tui_shim.so");
+        const char *shim = readable_file(shim_dylib) ? shim_dylib :
+                           (readable_file(shim_so) ? shim_so : NULL);
+        if (shim) {