fix ds4
ober
cf7fda2370b0e6f1319d3117edd9d831229053ae
--- 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; \ --- 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)))])) --- 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") --- 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))))) --- 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))) --- 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)))) --- 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 --- 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 --- 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)))) --- 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. --- 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!)))) --- 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 --- 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) {