Skip to content

Shield and Swap screens, a terminal-aware std, and a TLS check the wallet was missing - #583

Open
eKisNonos wants to merge 29 commits into
linux/store-installfrom
ui/shield-swap-terminal
Open

eKisNonos wants to merge 29 commits into
linux/store-installfrom
ui/shield-swap-terminal

Conversation

@eKisNonos

@eKisNonos eKisNonos commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #567. This PR targets linux/store-install so it shows only its own 29 commits: 9 terminal commits that were already on ui/terminal, and 20 new ones. When #567 merges into main, retarget this PR to main.

Each commit builds on its own. The wallet and terminal were checked live on a 1920x1080 QEMU boot of the image built from this branch.

Kernel and std

  • kernel: tell a program whether its streams reach a terminal adds a tty table, sys_tty_set (parent only, needs the IPC capability) and sys_tty_query, which answers rows and columns or ENOTTY.
  • std: ask the kernel whether a standard stream is a terminal makes is_terminal real in the std layer. Tools such as pastel now colour their output on a screen and stay plain in a pipe, without being patched.
  • build: drop std build caches when the std layer changes fixes a stale-std hazard: -Zbuild-std did not rebuild std when rust-src changed underneath it.

Terminal and toolkit

  • Readline keys and chords that type nothing.
  • Help that fits 80 columns and names every key, with tests.
  • rg dropped, since no build registers it, and refusals named.
  • Selection can start in the margin.
  • The terminal tells the programs it runs that they reach a screen, and updates them on resize.
  • Commands longer than the screen is wide (LINE_MAX 1024).
  • Sixteen colours per theme, each held to WCAG contrast by a test.
  • Box-drawing characters stroked across the whole cell, so tables join. This was checked live with csview.
  • The toolkit draws characters the UI face lacks from the mono face.
  • The editor draws folder chevrons in pixels and colours only code.

Wallet

  • talk to the RPC only after the server proves it is the host: TLS 1.3 CertificateVerify is now checked (RFC 8446 4.4.3) before any request is written. Before this, the chain was checked but the server never had to prove it held the key.

  • show the recovery phrase, and make Esc go back: the phrase is shown once, in a numbered grid. Esc goes back one step instead of closing the window.

  • refuse to sign a transfer above a 1000 gwei gas price.

  • draw the Shield screens and say when no shield service answers:

    • home, with shielded balances first and the nox1 receive address;
    • deposit and withdraw in the pool's standard sizes only (0.01 to 10 ETH, 1,000 to 5,000,000 NOX);
    • private send, with the sentence the pool asks every wallet to show;
    • review, proof progress, history, and the Sepolia contracts written out in full.

    Spending a note before the 20-note, 6-hour wait needs the typed words "spend early". A nox1 address is pasted with Ctrl+V.

  • make a withdrawal wait like a payment: found on the live boot, where Review withdrawal was enabled with the wait not even counted. The same commit resets the size between screens and makes the banner's Dismiss work.

  • draw Swap on the Etna frame: rate, price impact, minimum received, slippage, fee and route in one open tile. Picking the token already on the other side swaps the pair.

What is not wired, and says so

  • Shield: Confirm goes through a single request seam in screen/shield/request.rs. It refuses with a sentence while no shield service answers, and while this build cannot yet speak the service's request format.
  • Swap: the quote source is still the stub, so no trade is priced or sent. The screen shows a banner instead of a zero price.
  • History and proofs show only what the shield service reports. Nothing is invented.

Checked

  • cargo check for x86_64-nonos-user, with no new errors; the only new warnings are dead code for history states the shield service will fill.
  • Every new file is at most 75 lines.
  • The size ladder was run on the host against the expected wei values.
  • A live boot walk-through of Shield home, Deposit, Review, Send with the override, Withdraw, History, Sepolia and Swap. Screenshots are available on request.

With NONOS_LINUX_GUESTS=1 the kernel was still built with the setup
profile. Under it the apps, the Linux personality among them, spawn only
once first-boot setup exits, and setup waits for keys. An unattended guest
boot therefore never started its guest: in 180 s after `[INIT] Capsules
spawned` the log had `[SETUP] keyboard held` and no `[APP-LINUX]` or
`[LINUX]` line.

That image now builds `microkernel-desktop-gui` with `nonos-stark-attest`,
the same capsule set without setup, so the apps spawn at boot. Every other
build of nonos-mk-desktop-gui-prod is unchanged.
Threads run on their leader's tables without owning an address space
entry, so release freed the tables when the leader was finalized, whatever
its threads were doing. A thread still in the process table can be on the
CPU under those tables, taking its own kill. When a Go guest exited, the
personality killed its threads and then its leader, and the timer drain
finalized the leader while a Go thread was running. The kernel then took a
page fault fetching the double fault gate, and the machine reset:
CR3 was the guest's, FS held the Go thread's TLS, and CR2 was the IDT's
entry for vector 8.

Release now hands the address space to another process in the table that
runs on the same tables, and rebinds that process's capability token to
the ASID it now owns. The last holder's release frees the tables.
A Linux clone child resumes after its parent's `syscall` holding the
parent's registers, with rax zero and rsp the new stack. MkForeignThread
started the child on fresh registers instead, and both runtimes that start
threads here call through a register in the child: Go's `runtime.clone`
calls r12 and reads m and g from r13 and r9, and musl 1.2.4's `__clone`
calls r9. The first gohello boot started two threads, and they faulted at
`rip=0` and at a heap address on an NX page.

MkForeignThread takes a fifth argument, the guest thread parked in the
call that asked. Given, the new thread starts on a copy of that thread's
parked registers, with rax zero, the entry and stack given, and the
parent's FS base unless a TLS is named. The source thread must be
supervised by the caller and in the same thread group as the target, so no
guest receives another's register contents. Zero keeps the fresh start.

The personality passes the calling thread, and passes a TLS only when
CLONE_SETTLS is set; before, a clone without it installed the fifth
argument, whatever it held, as the child's FS base.
MkKill admitted a parent or a holder of ProcessControl. A guest thread's
parent is its group leader, not the supervisor, so when a Linux guest
exited, the personality's kill of each thread was refused with EPERM, and
the refusal was dropped. The threads outlived their process. Once the
personality had exited, their parked calls were abandoned and they ran Go
code with no one to answer them: a second goconc boot showed a guest
thread faulting at 0x1006 and then at address zero after `[LINUX] guest
exited`.

MkKill now also admits the caller the foreign registry names as the
target's supervisor. The personality prints a refused kill as
`[LINUX] kill refused: pid <n> outlives its process, errno <e>`, so a
thread that survives its process is named in the log.
Pressing the close button now asks the app first, through close_requested.
An app holding unsaved work returns false and says so in its own window; the next press closes. The default closes as before, so no other app changes.
A failed open no longer renames the tab, so the next save cannot write the old text over the file that failed, and a failed open from the tree opens no tab. Opening puts the file in its own tab and never replaces text; a load resets undo, redo and selection. A new document is untitled instead of carrying /notes.txt unread, and saving it asks for a path. Save As asks before replacing another file, and a document takes a new name only once the write lands.

Undo and redo track a save point, so undoing back to the saved text is clean and undoing past it is not, even after old steps are dropped. Every close control and the window close button ask once before discarding unsaved text, and Esc no longer closes the window. editor_proofs compiles again, with save-point proofs: 34 pass.
A no_std crate the terminal will draw from. The parser is the DEC state machine: CSI with private markers, colon sub-parameters and intermediates, OSC, DCS, SOS/PM/APC swallowed, CAN/SUB/ESC aborting any sequence, UTF-8 split across reads. The screen defers the wrap at the last column, so a full-screen program can fill the bottom-right cell, and implements IL, DL, RI, SU, SD, ICH, DCH, ECH, REP, tab stops, origin and insert modes, DECSC/DECRC, the 47/1047/1048/1049 alternate screens, DEC line drawing, wide characters kept whole, and combining marks and ZWJ sequences in one cell. Resize re-wraps the normal screen and its history.

It answers DA1, DA2, DSR, DECRQM, XTVERSION, XTGETTCAP, DECRQSS, size reports and OSC colour queries, since editors wait for them. A program may not move or resize its window, and a clipboard read (OSC 52 ?) is refused and counted. Keys, mouse (X10 and SGR), focus and paste encode per mode; paste strips escape bytes so pasted text cannot end the bracket and run as commands. Every buffer output can grow has a ceiling in limits.

69 tests, including a seeded mutation fuzz that interleaves hostile output with resizes, copies and searches and checks the cursor and row widths after every step; it found a cursor left below the screen after a resize, fixed here.
The PS/2 keymap already emits Insert as 0x1209 and F1 to F12 as 0x1101 to 0x110C.
Apps had no names for them, so none handled them.
The fixed 96x40 grid and its partial VT layer give way to nonos_vt. The screen follows the window size and zoom, re-wrapping on resize, with 3000 lines of history a tab on a 64 MiB heap, and draws bold, dim, every underline style, strikethrough, overline, inverse, hidden, wide and combining characters, and the cursor shape a program sets. A program in the foreground or on the alternate screen owns the body; the prompt returns when it ends, with any modes it left on reset, and output is read up to 64 KiB a tick instead of 256 bytes.

A program reading raw gets keys as xterm sends them (taken as raw once it asks for the alternate screen, application cursor keys, bracketed paste or mouse reports, as there is no path for its tty settings yet); any other gets lines edited as a tty's canonical mode edits them. Ctrl+C interrupts, and a program's queries are answered on its stdin.

The pointer selects text (double click a word or path, triple click a line, Alt for a block), or goes to a program that asked for mouse reports unless Shift is held. Ctrl+Shift+C copies, Ctrl+Shift+V pastes, Ctrl+Shift+F searches history across wrapped lines, and the wheel scrolls history or sends cursor keys on the alternate screen. Proofs for the line editing and selection added to terminal_line_proofs.
A launcher that renders a child's output knows which of the child's
standard streams reach its screen; the child had no way to ask. MTTY lets
the parent record that, with the size in cells, under the same parent
rule that lets it drain the child's output, and it needs IPC as MTRN
does. MTTQ answers the caller about its own stream 0, 1 or 2 with the
size, or ENOTTY. The record is dropped as the process is finalized, so a
reused pid starts with no terminal. Both calls are published in the ABI
with libc wrappers, and the syscall checks pass with the unreachable
count unchanged.
Three answers disagreed on NONOS: std's IsTerminal fell to the
unsupported arm and said no, the vendored is-terminal said no, and the
atty shim said yes for every stream, so one tool could colour a pipe
while another printed plain text to a person. std now asks MTTQ for
streams 0 to 2, and both is-terminal copies and atty go through std, so
there is one answer and it is the launcher's.
Noto Sans has no arrows, no triangles and no not-equal sign, so every app
that printed one showed an empty box. A character the chosen built-in
face lacks is drawn from the other one when that has it, cached under the
face it came from, and measured the same way so layout and paint agree.
Kerning only pairs glyphs of one face.
The explorer drew its chevrons as characters the UI font does not have,
so every folder showed a box. They are now drawn on the pixel grid. The
code lexer ran over every buffer, so a note coloured `from` as a keyword
and a Markdown heading as a comment; plain text and Markdown are now
left as typed.
A Ctrl or Alt chord the line editor did not bind fell through to the
printable arm, so Ctrl-F typed an f. Unbound chords are now swallowed,
AltGr excepted since some layouts type characters with it. Ctrl-F and
Ctrl-H step and rub out a character, Ctrl-P and Ctrl-N walk history, and
Alt-B, Alt-F and Alt-D move and cut by word, with Alt-D's cut going to
the ring Ctrl-Y reads. The skeleton exports MOD_ALTGR, which the PS/2
driver already sets. The arrow and page keys move to their own file.
The help tests parsed writeln calls that the table-driven help no longer
makes, so they read three rows and failed. The tables and row layout now
live in a file the proofs include, and the tests render every row the
terminal prints. That found the files and system rows at 82 and 85
columns; they are regrouped into files, disk, text, system and session.
The tools row names each program once. help keys now covers selection,
the clipboard, search and the readline keys.
rg and ripgrep mapped to tool.ripgrep, which the kernel's tool registry
has never held: the ripgrep capsule is a boot-time service MkToolRun
cannot reach. Typing either printed "tool: launch failed" in every
build. A proof now checks each tool the terminal offers against
apps.list, and a refused launch names the tool and says whether it is
missing from the build or was refused.
A press a few pixels left of the first column landed outside the cell
grid and started nothing. The hit area now takes in the margin painted
around the text, and the column clamps to the edge cell. The pixel to
cell maths moves to its own file.
A tool, store tool or exec'd program whose output this terminal renders
is told, through MTTY, that its three standard streams reach a terminal
of the current size, so it chooses colour and columns for a person. A
pipeline stage or redirected command is never told and writes plain
bytes. When the window resizes, the program in front is told the new
size.
-Zbuild-std fingerprints the sysroot crates by version rather than by
their sources, so a tool whose target dir already held a std kept
linking it after the NONOS std layer changed. Six of the tools shipped
that way without the new terminal query, and pastel still printed its
pipe output to a person. Applying the layer now clears the per-tool std
caches it just made stale.
The line buffer, the history and the kill ring were sized by COLS, the
screen's starting width of 96, so a longer command was cut at column 96.
An echo with a redirect lost its file name there and wrote to a
different file. Typed input now holds LINE_MAX, 1024 bytes, and the
prompt already scrolls sideways across a line wider than the window.
Programs name colours 0 to 15 and got xterm's, whose blue is #0000EE:
2.2:1 against the dark themes, so tokei's table headers could not be
read. Each theme's ground now picks a dark or a light set, and a proof
holds every text colour at 4.5:1 or better on every shipped ground.
A font draws a line character inside its glyph box, so the line gap left
a break between rows and csview's tables did not join. The light, heavy,
double and rounded line characters are now stroked from edge to edge of
the cell, arms meeting at the centre, and anything else still comes from
the font.
The chain, anchor, hostname and validity checks ran only on the probe
connection, where they set status text. The connection that reads
balances, the nonce and the fee, and broadcasts, checked nothing but the
server's Finished, and CertificateVerify was never checked anywhere. A
server that replays the real chain and finishes the handshake on keys of
its own passed, and could name any gas price the wallet then signed at.

CertificateVerify is now verified with the leaf's own key over the
transcript to the Certificate, and every RPC call requires it together
with the chain, the pinned root, the hostname, validity against the
clock, and Finished. ClientHello offers only the ECDSA schemes there is
a verifier for.
Creating a wallet set the backup flag, but the Etna Receive screen was
drawn over it, so the phrase never appeared, and the next Enter wiped
it. On a machine that cannot seal keys the account then lived only in
RAM with no way back. The phrase now has its own screen, straight after
creation, with no back button: confirming is the only way on, and it
wipes the words.

Esc used to close the window before any handler saw it, so every "Esc to
cancel" on screen was dead. It now closes an open panel, returns other
screens home, and does nothing at home.
The fee cap is twice the node's gas price with no upper bound, so a
broken node's reading would be signed as given. A transfer is now
refused above 1000 gwei, and the reason the refresh failed is the one
shown, instead of a single message for every cause.
Shield opened the old dashboard. It now has its own Etna screens: home
with shielded balances first and the nox1 receive address, deposit and
withdraw in the pool's standard sizes only (0.01 to 10 ETH, 1,000 to
5,000,000 NOX), private send with the sentence the pool asks every
wallet to show, review, proof progress, history and the Sepolia
contracts in full.

The spend wait is a meter of 20 new notes and 6 hours; spending early
needs the typed words "spend early". A nox1 address is pasted with
Ctrl+V, since it is too long to type.

Nothing here pretends. Balances read from the note store show a dash
until notes exist, history and proofs show only what the shield
service reports, and Confirm goes through one request seam that
refuses with a sentence while this build cannot yet speak the
service's format.
A withdrawal spends a note, so it now waits for the same 20 new notes
and 6 hours as a private payment, or the typed "spend early", before
it can be reviewed. The walk-through on the live image let one through
with the wait not even counted.

Each choosing screen starts with no size picked; the 10,000 NOX picked
on Deposit had been carried into Withdraw. The no-service banner's
Dismiss now hides it until Shield is next opened, where it did nothing.
The Sepolia page's labels are capitals like every other label.
Swap opened the old dashboard. It now has its own screen: the token
paid and the amount typed, the token bought and what the pool says
comes back, then rate, price impact, minimum received, slippage,
network fee and route in one tile, with a warning in words when the
impact is large. Picking the token already on the other side swaps
the pair, so a token is never traded for itself.

The quote source is still the stub, so an amount gets no price. The
screen says that in a banner instead of showing zero, and the button
stays "Enter an amount". Nothing signs a swap in this build.
The Swap screen's no-price banner drew a Dismiss that did nothing; it now hides the banner until the amount is edited. The early-spend warning said "this payment" on the Withdraw screen too, and now reads for either.
@eKisNonos
eKisNonos changed the base branch from main to linux/store-install September 28, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant