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