Fix debug REPL and client hardening
ober
765b64a7ddbc66a6cc71566050552be9261408d7
--- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ jcode.json /android/android.jar /android/build/ /android/dist/ +/android/debug.keystore /android/app/src/main/assets/native/ # Runtime data (checkpoints, security cache, logs) --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ android:icon="@drawable/ic_launcher" android:theme="@android:style/Theme.Material.Light.DarkActionBar" android:networkSecurityConfig="@xml/network_security_config" - android:debuggable="true" + android:debuggable="false" android:allowBackup="false"> <activity --- a/android/app/src/main/java/dev/jerboa/jcode/JcodeClient.kt +++ b/android/app/src/main/java/dev/jerboa/jcode/JcodeClient.kt @@ -89,7 +89,7 @@ class JcodeClient(private val context: Context) { return } - Thread { + readerThread = Thread { try { Log.i(TAG, "connecting to $host:$port") val s = Socket(host, port) @@ -169,6 +169,10 @@ class JcodeClient(private val context: Context) { outbound.clear() reconnectThread?.interrupt() reconnectThread = null + writerThread?.interrupt() + writerThread = null + readerThread?.interrupt() + readerThread = null socket?.let { try { it.close() } catch (_: Exception) {} } deleted file mode 100644 Binary files a/android/debug.keystore and /dev/null differ --- a/src/jcode/core/debug-repl.ss +++ b/src/jcode/core/debug-repl.ss @@ -18,7 +18,8 @@ stop-jcode-repl! jcode-repl-port) -(import :jcode/core/log +(import :jerboa/reader + :jcode/core/log :std/misc/thread) (def logger (make-logger "debug-repl")) @@ -169,7 +170,7 @@ op) (flush-output-port op) 'continue]) - (read ip)))) + (jerboa-read ip)))) (cond ((eof-object? expr) (void)) ;; client disconnected ((eq? expr 'continue) (loop)) --- a/src/jcode/core/session.ss +++ b/src/jcode/core/session.ss @@ -162,30 +162,46 @@ ;; Atomically replace ALL stored messages for SESSION-ID with MSGS. ;; Used by /compact to swap older turns for an LLM-generated summary. (let* ((db (open-db)) - (now (timestamp-now))) - (sqlite-exec db "BEGIN") - (sqlite-eval db "DELETE FROM messages WHERE session_id = ?" session-id) - (for-each - (lambda (msg) - (let ((tool-calls-json - (and (message-tool-calls msg) - (json-object->string - (map tool-call->stored-json (message-tool-calls msg)))))) - (sqlite-eval db - "INSERT INTO messages (session_id, role, content, tool_calls, tool_call_id, created_at) - VALUES (?, ?, ?, ?, ?, ?)" - session-id - (message-role msg) - (let ((c (message-content msg))) (if (eq? c (void)) #f c)) - tool-calls-json - (let ((id (message-tool-call-id msg))) (if (eq? id (void)) #f id)) - now))) - msgs) - (sqlite-eval db - "UPDATE sessions SET updated_at = ? WHERE id = ?" - now session-id) - (sqlite-exec db "COMMIT") - (sqlite-close db))) + (now (timestamp-now)) + (begun? #f) + (committed? #f)) + (dynamic-wind + (lambda () (void)) + (lambda () + (try + (begin + (sqlite-exec db "BEGIN") + (set! begun? #t) + (sqlite-eval db "DELETE FROM messages WHERE session_id = ?" session-id) + (for-each + (lambda (msg) + (let ((tool-calls-json + (and (message-tool-calls msg) + (json-object->string + (map tool-call->stored-json (message-tool-calls msg)))))) + (sqlite-eval db + "INSERT INTO messages (session_id, role, content, tool_calls, tool_call_id, created_at) + VALUES (?, ?, ?, ?, ?, ?)" + session-id + (message-role msg) + (let ((c (message-content msg))) (if (eq? c (void)) #f c)) + tool-calls-json + (let ((id (message-tool-call-id msg))) (if (eq? id (void)) #f id)) + now))) + msgs) + (sqlite-eval db + "UPDATE sessions SET updated_at = ? WHERE id = ?" + now session-id) + (sqlite-exec db "COMMIT") + (set! committed? #t) + (set! begun? #f)) + (catch (e) + (when (and begun? (not committed?)) + (try (sqlite-exec db "ROLLBACK") + (catch (_) (void)))) + (error 'session-replace-messages "transaction failed" e)))) + (lambda () + (sqlite-close db))))) (def (session-update-title session-id title) (with-db --- a/src/jcode/ui/cli.ss +++ b/src/jcode/ui/cli.ss @@ -71,7 +71,7 @@ (load-config) (session-init-db) (init-tools) - ;; Start debug REPL if requested (--repl for auto-port, --repl-port N) + ;; Start debug REPL only when an explicit port is requested. (let ((repl-opt (assoc '--repl-port opts))) (when repl-opt (let ((port (start-jcode-repl! (cdr repl-opt)))) @@ -112,8 +112,17 @@ ((equal? (car args) "--no-mcp") (loop (cdr args) (cons '(--no-mcp . #t) opts))) ((equal? (car args) "--verbose") (loop (cdr args) (cons '(--verbose . #t) opts))) ((and (equal? (car args) "--repl-port") (pair? (cdr args))) - (loop (cddr args) (cons (cons '--repl-port (string->number (cadr args))) opts))) - ((equal? (car args) "--repl") (loop (cdr args) (cons '(--repl-port . 0) opts))) + (let ((p (string->number (cadr args)))) + (if (and p (> p 0) (< p 65536)) + (loop (cddr args) (cons (cons '--repl-port p) opts)) + (begin + (fprintf (current-error-port) "[ERROR] invalid --repl-port: ~a~n" (cadr args)) + (exit 1))))) + ((equal? (car args) "--repl") + (begin + (fprintf (current-error-port) + "[ERROR] --repl was removed; use --repl-port N to enable the debug REPL.~n") + (exit 1))) ((and (equal? (car args) "--trace") (pair? (cdr args))) (loop (cddr args) (cons (cons '--trace (cadr args)) opts))) ((and (equal? (car args) "--model") (pair? (cdr args))) @@ -167,8 +176,7 @@ OPTIONS: --tui Launch terminal UI mode --no-tui Force line-mode REPL (default) --no-mcp Skip MCP server initialization - --repl Start debug REPL on auto-assigned port - --repl-port N Start debug REPL on specific port + --repl-port N Start debug REPL on specific localhost port --verbose Log TUI events to ~/jcode.log --trace FILE Trace EVERYTHING to FILE: full HTTP requests/responses (API keys redacted), full tool args/results, all log --- a/src/jcode/ui/connect.ss +++ b/src/jcode/ui/connect.ss @@ -25,9 +25,14 @@ (def (token-path) (path-join (or (getenv "HOME") ".") ".jcode" "relay-token")) +(def (secure-token-file! path) + (try (chmod path #o600) + (catch (_) (void)))) + (def (load-token) (let ((p (token-path))) (and (file-exists? p) + (begin (secure-token-file! p) #t) (call-with-input-file p (lambda (port) (let ((s (get-string-all port))) --- a/src/jcode/ui/relay.ss +++ b/src/jcode/ui/relay.ss @@ -73,9 +73,14 @@ (def (token-path) (path-join (or (getenv "HOME") ".") ".jcode" "relay-token")) +(def (secure-path! path mode) + (try (chmod path mode) + (catch (_) (void)))) + (def (load-token) (let ((p (token-path))) (and (file-exists? p) + (begin (secure-path! p #o600) #t) (call-with-input-file p (lambda (port) (let ((s (get-string-all port))) @@ -107,10 +112,12 @@ (let ((dir (path-join (or (getenv "HOME") ".") ".jcode"))) (unless (file-exists? dir) (mkdir dir)) + (secure-path! dir #o700) (let* ((tok (random-hex-32)) (path (token-path))) (call-with-output-file path (lambda (out) (display tok out))) + (secure-path! path #o600) (fprintf (current-error-port) "[INFO] generated new relay token~n") (fprintf (current-error-port) --- a/src/jcode/ui/serve.ss +++ b/src/jcode/ui/serve.ss @@ -53,6 +53,8 @@ (def logger (make-logger "serve")) +(def *auth-timeout-secs* 10) + ;; Tools hidden when mode = "plan". (def *mutating-tools* '("write" "edit" "multi-edit" "patch" "bash" "git_commit")) @@ -465,10 +467,15 @@ (def (token-path) (path-join (or (getenv "HOME") ".") ".jcode" "server-token")) +(def (secure-path! path mode) + (try (chmod path mode) + (catch (_) (void)))) + (def (ensure-jcode-dir!) (let ((dir (path-join (or (getenv "HOME") ".") ".jcode"))) (unless (file-exists? dir) - (mkdir dir)))) + (mkdir dir)) + (secure-path! dir #o700))) (def (bytes->hex bv) (let ((len (bytevector-length bv))) @@ -484,13 +491,16 @@ (ensure-jcode-dir!) (let ((token (bytes->hex (random-bytes 32)))) (write-file-string (token-path) token) + (secure-path! (token-path) #o600) token)) (def (load-token) "Load the server token from disk. Returns string or #f." (let ((path (token-path))) (if (file-exists? path) - (string-trim (read-file-string path)) + (begin + (secure-path! path #o600) + (string-trim (read-file-string path))) #f))) (def (ensure-token!) @@ -534,6 +544,21 @@ (emit-error (format "auth error: ~a" (err->string e))) #f)))))) +(def (start-auth-timeout! in out label) + "Close unauthenticated connections that do not send the auth line promptly." + (let ((done (cons #f #f))) + (spawn + (lambda () + (thread-sleep! *auth-timeout-secs*) + (unless (car done) + (log-warn logger "auth-timeout" `((label . ,label))) + (try (close-port in) (catch (_) (void))) + (try (close-port out) (catch (_) (void)))))) + done)) + +(def (finish-auth-timeout! done) + (when done (set-car! done #t))) + ;; ── reverse-tunnel client (host side) ──────────────────────────────── ;; ;; Dial a relay, register as a host under NAME, wait for a controller to be @@ -615,8 +640,11 @@ (set! *session-tokens-in* 0) (set! *session-tokens-out* 0) (set! *session-cost* 0.0) - (when (validate-auth! server-token) - (serve-loop))))) + (let ((auth-timeout (start-auth-timeout! sin sout "relay"))) + (try + (when (validate-auth! server-token) + (serve-loop)) + (finally (finish-auth-timeout! auth-timeout))))))) (finally (try (close-port sin) (catch (_) (void))) (try (close-port sout) (catch (_) (void)))))) @@ -659,9 +687,12 @@ (set! *session-tokens-in* 0) (set! *session-tokens-out* 0) (set! *session-cost* 0.0) - (if (validate-auth! token) - (serve-loop) - (log-warn logger "tcp-reject" '((msg . "auth failed, closing"))))) + (let ((auth-timeout (start-auth-timeout! in out "tcp"))) + (try + (if (validate-auth! token) + (serve-loop) + (log-warn logger "tcp-reject" '((msg . "auth failed, closing")))) + (finally (finish-auth-timeout! auth-timeout))))) (catch (e) (log-error logger "tcp-error" `((msg . ,(err->string e))))) (finally --- a/src/jcode/ui/tui.ss +++ b/src/jcode/ui/tui.ss @@ -218,10 +218,6 @@ ;; NOTE: cli-main already called load-config, session-init-db, init-tools ;; before dispatching here — do NOT re-init. (apply-tui-overrides! args) - ;; Skip if --repl-port already started a REPL — otherwise we stomp the user's port. - (unless (jcode-repl-port) - (try (start-jcode-repl!) - (catch (e) (void)))) (let ((verbose? (and (member "--verbose" args) #t))) (when verbose? (open-tui-log!)) (tui-log "tui-main: starting, args=~a" args)