• src/hash/sha256.c sha256.h src/syncterm/scripts/auto/connected/sftp_pu

    From Deuc¨@VERT to Git commit to main/sbbs/master on Sat May 2 15:47:32 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/961d4d463139cb3c0f8632b7
    Added Files:
    src/hash/sha256.c sha256.h src/syncterm/scripts/auto/connected/sftp_pubkey.wren sftp_queue_init.wren src/syncterm/scripts/sftp_app.wren sftp_queue.wren src/syncterm/wren_token.c wren_token.h
    Modified Files:
    src/hash/CMakeLists.txt objects.mk src/syncterm/CMakeLists.txt GNUmakefile Wren.adoc bbslist.c src/syncterm/scripts/syncterm.wren ui_app.wren wrentest.wren src/syncterm/ssh.c ssh.h term.c term.h uifcinit.c uifcinit.h wren_bind.c wren_bind_conn.c wren_bind_conn.h wren_bind_fs.c wren_bind_fs.h wren_bind_hook.c wren_bind_hook.h wren_bind_internal.h wren_bind_screen.c wren_bind_screen.h wren_bind_sftp.c wren_bind_sftp.h wren_embed_gen.c wren_host.c wren_host.h wren_host_internal.h
    Removed Files:
    src/syncterm/scripts/auto/connected/sftp_browser.wren src/syncterm/sftp_browser.c sftp_browser.h sftp_degraded.c sftp_degraded.h sftp_queue.c sftp_queue.h sftp_queue_screen.c sftp_queue_screen.h sftp_session.h sftp_wait.c sftp_wait.h
    Log Message:
    SyncTERM: replace C SFTP client with Wren implementation

    Eight-step rewrite (squashed from 8 WIP/feature commits). The
    C-side SFTP browser, queue, queue-screen, and degraded-modal are
    replaced by a single Wren App with three modes; the in-process
    authorized_keys upload path moves to a Wren script too; and the
    Wren input model is rebuilt from a single-subscriber slot to a
    per-fiber claim stack so stacked Apps actually work. Net diff:
    +4577 / -3413, but most of the +4577 is the new C-extensions
    infrastructure (claim stack, consent tokens, SHA-256 fallback,
    UIFC wrappers) that the Wren scripts couldn't have been built
    without.

    == Motivation

    The C SFTP UI was a bug-prone fork of Synchronet's UIFC list /
    modal idioms with three independent state machines (browser,
    queue, degraded) duplicating screen save/restore, focus tracking,
    and event routing. Most of the same code already exists in the
    Wren UI primitives we built earlier this branch (Pane, ListView,
    Popup, App, modal stack), and Wren's fiber model is a much
    better fit for the SFTP transfer queue's "block on async
    completion + check cancel between chunks" shape than C threads
    + condition variables.

    == Wren input model Ä claim stack

    Replaced the single `parked_fiber` WrenHandle slot in
    wren_host_internal.h with a linked-list claim stack (newest
    fiber wins). Each Wren App.run pushes a claim that consumes
    events synchronously and posts a wake to its own fiber for
    repaint; the topmost living claim wins each event. Auto-prune
    of claims whose owning fiber is done. Pass-through (false)
    returns cascade to lower claims and then to the existing
    Hook.onKey/onMouse chain.

    `Input.nextEvent` is deleted outright. `Input.wake` becomes
    `Wake.post`. `App.runningCount` (and the gate that previously
    suppressed Alt-Q while another App was up) is gone Ä the claim
    stack is the ordering signal. Browser + queue + degraded modal
    can stack freely now.

    == SFTP browser, queue, degraded Ä single Wren App

    `scripts/sftp_app.wren` is one App with `setMode(\"browser\" |
    \"queue\" | \"degraded\")`. The browser shows directory
    contents with a 4-char status chip ([==] / [<>] / [] / [Q] /
    [er] / etc.) per row that refreshes in place as transfer state
    changes. F4 opens uifc's filepick_multi to tag local files for
    upload. Alt-Q from the browser switches to queue mode without
    spinning up a second App; queue mode shows direction arrow,
    status, bytes done/total, percent, basename, and remote path,
    sorted ACTIVE  QUEUED  DONE  FAILED  CANCELLED. Selection
    is preserved across rebuilds by (dir, remote) lookup. Del
    cancels the selected job. Esc / [X] dismisses; the dismiss
    semantic flips to \"suspend workers + persist\" when the shell
    has closed.

    `scripts/sftp_queue.wren` runs the transfer queue: 1-up + 1-dn
    worker fibers, 4 KB chunks, 250 ms idle poll cadence, .won
    persistence in the per-BBS Cache directory (versioned schema).
    Workers check cancel between chunks and bail on suspend. Both
    directions resume from `job.done` across reconnects (the C
    queue restarted from byte 0). Successful download and upload
    both stamp the destination's mtime to match the source so the
    browser's [==] chip works without depending on hash extensions.

    The previously-separate degraded-modal state is now just queue
    mode with `_shellClosed = true`. In that state, onTick_
    auto-quits the App when the queue drains naturally OR the
    workers have suspended, so the user isn't stranded on an
    inactive screen.

    `scripts/auto/connected/sftp_queue_init.wren` is the connect-
    time autoload: spawns workers, binds Alt-S / Alt-Q, binds
    onShellClose / onDisconnect.

    == authorized_keys upload Ä moved to Wren

    `scripts/auto/connected/sftp_pubkey.wren` runs at connect time:
    if `BBS.sftpPublicKey` is set and SFTP is available, it reads
    the local SSH public key via `Host.sshPublicKey`, opens
    `.ssh/authorized_keys` with `READ|WRITE|APPEND|CREAT`, scans
    existing lines for the blob (matches the old C
    `key_not_present` semantics), and appends if absent. Holds
    `CTerm.sftpActive = true` for the duration so disconnect waits.

    `Host.sshPublicKey` returns a structured `Map { algo, blob }`
    rather than the raw OpenSSH line the old C used Ä fixes the
    latent bug where the C code hardcoded `\"ssh-ed25519 %s ...\"`
    regardless of which algorithm the local key actually was, so
    RSA / sntrup keys would have been written with the wrong line
    prefix (had the C path supported them, which it didn't).

    == Picker consent tokens (sandbox-preserving capability)

    The old C `sftp_browser` picker UI was inside the same TU as
    the queue, so \"upload from arbitrary path\" was natural. The
    Wren sandbox doesn't allow scripts to construct File foreigns
    from raw paths Ä that's the whole point of the Cache / Upload /
    Download Directory roots and the relaxed-name predicate. But
    queued picker uploads need to survive disconnect / reconnect,
    which means writing absolute paths to disk and re-opening them
    on resume.

    Solved with a capability-token mechanism (`wren_token.c` /
    `wren_token.h`). At pick time the C side mints an opaque token
    binding (path, file-content SHA-256) under HMAC-SHA256 with a
    per-installation key; Wren stores it freely (it's bytes); only
    the C side can mint or verify. `Host.openLocalFile(token)` is
    the only path by which Wren can construct a non-sandbox File
    foreign, and its verify checks both the HMAC and that the
    file's content hash still matches. File edited or replaced
    since consent  token rejected, user must re-pick.

    The signing key (32 random bytes) lives in the user's encrypted
    syncterm.lst under `WrenPickerHmacKey`. Generated on first
    USER_BBSLIST open if absent. Loaded only from the user's
    personal list Ä never from SYSTEM_BBSLIST or web-fetched lists
    (those would let a malicious shared list inject a known key
    and forge tokens).

    The file-content hash routes through DeuceSSH's
    `dssh_hash_oneshot` when available (Botan or OpenSSL backend,
    which on hardware with crypto extensions hits SHA-NI / ARM
    crypto). WITHOUT_DEUCESSH builds fall back to a new pure-C
    SHA-256 in `src/hash/sha256.c`. Random source falls back from
    `dssh_random` to `xp_random` for key generation in the
    no-crypto build. HMAC always uses the C SHA-256 (inputs are
    ~100 bytes; no benefit from acceleration).

    == UIFC wrappers for connected-session use

    The pre-existing `Host.pickFile` binding called `filepick`
    directly on the global uifc state. That state isn't valid
    during a connected session Ä calling api->list under those
    conditions crashes inside the redraw path. Added `uifcfilepick`
    / `uifcfilepick_multi` wrappers in `uifcinit.c` that mirror
    the existing `uifcmsg` / `uifcinput` / `confirm` save / init /
    bail / restore dance. The Wren bindings now route through
    those wrappers. (The `pickFile` binding had been latently
    broken in connected context but never triggered because nothing
    ever called it from a script.)

    == Wren API additions / changes

    Foreign-method bindings:

    Input
    + pushClaim(fn)  ClaimHandle (replaces nextEvent)
    ClaimHandle (new foreign class)
    + pop()
    Wake (new namespace)
    + post(fiber, value) (replaces Input.wake)
    Hook
    + onShellClose(fn), onDisconnect(fn)
    Host
    + downloadDir, uploadDir (relaxed-name Directory roots)
    + pickFile(initialDir, mask, opts) Ä accepts String OR
    Directory foreign for initialDir
    + pickFiles(initialDir, mask, opts)  List<File> | null
    (multi-select counterpart)
    + openLocalFile(token)  File | null (consent-token reopen)
    + sshPublicKey  Map { algo, blob } | null
    + uploadArrow=(b), downloadArrow=(b)
    CTerm
    + sftpActive getter / setter (Wrenssh.c bridge for
    teardown gating)
    + refreshStatus()
    File
    + mtime getter, mtime=(t) setter
    + token getter (consent-token bytes; null for sandboxed Files)
    SFTP
    + setMtime(fiber, path, t)
    + lname getter (extension-negotiated flag)
    SFTPEntry
    + hasLongDesc, hash
    App
    + runChild(fn) Ä spawn child fiber, pump until completion

    Plus the Directory bindings gained the relaxed-name predicate
    (spaces, leading dots, parens, lengths up to 255 bytes Ä but
    still blocks separators, NUL, control bytes, dot/dotdot,
    Windows reserved names) inherited from Host.downloadDir /
    Host.uploadDir.

    == C-side infrastructure (needed pieces)

    src/hash/sha256.c, sha256.h
    Public-domain reference SHA-256 (FIPS 180-4) matching the
    existing sha1.h API shape. Added to objects.mk +
    CMakeLists.txt. Used by the consent-token verify path
    when DeuceSSH isn't built in.

    src/syncterm/wren_token.c, wren_token.h
    HMAC-SHA256 sign / verify shim plus per-installation key
    set / clear / generate / hex helpers. HMAC implemented
    inline (RFC 2104) on top of the SHA-256 above. File hash
    prefers DeuceSSH's hardware-accelerated path when
    available.

    src/syncterm/uifcinit.c, uifcinit.h
    + uifcfilepick / uifcfilepick_multi wrappers + shared
    uifcfilepick_common helper.

    src/syncterm/bbslist.c
    iniReadBBSList(USER_BBSLIST) loads or generates
    WrenPickerHmacKey from the encrypted blob. SYSTEM_BBSLIST
    and web lists never trigger this branch.

    src/syncterm/wren_host_internal.h
    parked_fiber slot replaced by claim stack
    (struct wren_input_claim *claim_top + claim_next_id).

    src/syncterm/wren_bind_screen.c
    Claim push / dispatch / auto-prune; nextEvent / parked
    teardown removed.

    src/syncterm/wren_bind_fs.c
    Token field on wren_file; Host.pickFile signature change
    (Directory acceptance + token signing); Host.pickFiles;
    Host.openLocalFile; File.token getter; Host.downloadDir /
    uploadDir + relaxed-name predicate.

    src/syncterm/wren_bind_conn.c
    CTerm.sftpActive + wren_sftp_active() bridge for ssh.c.

    src/syncterm/term.c
    is_connected ORs in wren_sftp_active(); shell-close
    transition fires wren_host_dispatch_shell_close exactly
    once; status-bar arrows read wren_upload_arrow_lit /
    wren_download_arrow_lit.

    src/syncterm/wren_embed_gen.c
    Minifier preserves blank lines so script error messages
    report source line numbers correctly.

    src/syncterm/scripts/wrentest.wren
    New T-cases for claim auto-prune, same-fiber replacement,
    cascade ordering.

    == Files removed (replaced by Wren equivalents)

    sftp_browser.c / .h Ä Alt-S file browser
    sftp_queue.c / .h Ä transfer queue + worker threads
    sftp_queue_screen.c / .h Ä Alt-Q queue UI
    sftp_degraded.c / .h Ä shell-close modal
    sftp_wait.c / .h Ä sync-wait shim around sftpc_*;
    only remaining caller (ssh.c
    authorized_keys upload, now in
    Wren) is gone, and ssh.c's
    SSH_FXP_INIT wait is now an
    inline xpevent_t
    sftp_session.h Ä folded into ssh.h (where the
    two surviving externs sftp_state
    / sftp_available actually belong)

    ssh.c also drops `add_public_key`, `key_not_present`,
    `get_pubkey_str`, `pubkey_thread_running`, all eleven
    `free(pubkey)` call sites, the `_beginthread(add_public_key,
    ...)` launch, and the pubkey-thread wait in `ssh_close`.

    == Verification

    Built clean under `gmake` (DeuceSSH/Botan backend on FreeBSD).
    End-to-end manually verified on bbsdev.net: F4 picks tagged
    files; uploads enqueue and complete with consent tokens;
    shell-close mid-upload preserves the in-flight job ACTIVE; on
    reconnect, token verifies (HMAC + content SHA-256 match) and
    the worker resumes from the saved offset. Browser status chips
    update in place. Alt-Q stacks the queue on top of the browser;
    Esc returns; queue auto-quits when shell has closed and work
    has drained.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net