The practice sandbox at the bridge; the guided tour covers the online beta - #1097
Merged
Merged
Conversation
…names
Practice mode swapped dsmClient's properties and refused the ones whose names
matched a list of verbs. A screen that imported a write directly never saw the
swap: TokenCreationDialog's createToken (a real token, the real ERA fee
burned), AccountsScreen's burnToken / forgetToken / addTokenByAnchor, every
SoFi action (trade, route, createVault, close, resolve, relay), the lock's
configure_lock and the NFC and recovery services all reached the real wallet
while the tour ran. The verb list also missed trade, route, resolve, relay,
enable, activate and complete.
Owner ruling 2026-10-01: "make the bridge-level practice gate the authoritative
safety boundary, and treat the current dsmClient interception as convenience
behavior only… While practice mode is active, no state-changing Rust call
reaches the real wallet unless it is explicitly designated as a practice-safe
simulated action."
- bridge/practiceGate.ts: while the sandbox is on, every request is read
before it leaves. Only named reads cross: bridge methods, router routes
(queries and the two SoFi reads the router takes as invokes), the camera,
and the display preferences ui_theme / sfx_enabled. Everything else,
including a request that cannot be read and a raw sendMessageBin frame
carrying an ingress or host request, is answered "blocked in practice mode"
in its channel's wire shape and never reaches Rust or the host.
- bridge/BridgeRegistry.ts: getBridgeInstance() is the practice view while the
sandbox is on. Every path to native code (callBin, the ingress, host
requests) takes its bridge from here.
- practiceMode: enter/leave switch the sandbox. The dsmClient overrides keep
only the simulated answers; the verb regex and its refusals are deleted.
- The practice addContact answered { ok: true }, which contactsStore read as a
refusal, and stored a fixed practice id for every added device. It now
answers AddContactResult for the card Rust read, with the card's ids, named as
Rust names a contact added with no alias.
- tests/helpers/rustIngressRecord.ts: the record-answering bridge, now shared,
logs what reached it.
Tests (bridge/__tests__/practiceGate.test.ts):
- practice on: direct imports burnToken, sofi.trade and sofi.resolve are
blocked, and a raw ingress frame too; the bridge receives nothing;
- a read (wallet.amount) crosses and Rust answers it;
- ui_theme crosses, lock and diagnostics preferences do not;
- the QR camera crosses, an NFC write does not;
- practice off: the same writes reach the bridge;
- practiceMode: dsmClient.burnToken / forgetToken are blocked, a practice send
crosses only wallet.amount reads, and leave() ends the sandbox.
…or Bitcoin Owner ruling 2026-10-01: the tour "needs to be expanded… cover everything… Except for Bitcoin for now, and the offline for now", with walkthroughs that highlight the real UI and keep the action blocked where Rust offers no practice route, and hands-on add-contact-by-code. Removed: the five offline-send steps (mode, go-offline, offline-funding, appliance, go-online) and the Bitcoin tab in the wallet-tabs text. Fixed: the lock step named a fingerprint unlock the app does not have; the storage step said two tabs where there are three; token-detail described Burn and Forget on a card that has neither. Added, 79 steps from 57: - Wallet: recent activity, wallet identity, the History tab, the inbox (the real one, only read) and closing it. - Tokens: the creation wizard opened, walked through, and closed; Add Token by anchor; Burn and Forget; sharing a token by its anchor and QR. - Trade: what you pay, what you get, the quote and trade; a liquidity vault's reserves and fee, closing a vault, relaying a fulfillment. - Contacts, hands-on: copy bob's code, paste it, Add; open a contact and its facts and stitched receipts. - Storage: the DLVs tab's liquidity vaults. - Settings: the lock setup screen, the ring backup and the recover screen. Bob's code is Rust's. The dsm_sdk test the_tour_practice_contact_code_is_one_rust_reads builds his card on the beta network, requires read_contact_code to read it back, and holds components/tour/practiceContact.ts equal to its encoding (DSM_WRITE_FRONTEND_FIXTURES=1 rewrites it). In the tour, pasting it runs the real contacts.readContactCode, a read the sandbox lets through, and Add is practice mode's. - GuidedTour: a step can carry text the user needs, shown selectable with a Copy button. - Anchors where no stable selector existed: recent-activity, add-token, swap-pay, swap-get, liquidity-create, liquidity-close, contact-code, contact-list, liquidity-vaults; and the classes wallet-identity, sofi-relay. The DLVs anchor wraps the vault section in every state, so the step does not wait forever on a wallet with no vaults. Tests (tour.test.tsx): - the steps' data-tour anchors are exactly the fourteen, each in the app; - every target, wait and back-out selector names only attribute values, classes and ids the app's screens or the shell render; - no step teaches offline sending or Bitcoin, or targets their anchors. practiceMode.test.ts: - Add Contact answers AddContactResult under the card's ids; - with no alias, the contact is named as Rust names it.
clippy -D warnings refused the `..Default::default()` in the tour practice contact test: the literal already names every ContactQrV3 field.
…ce id After #1096 an online send names its recipient by device id, never by alias, and practice mode finds the contact the same way. The sandbox test's practice send now goes to PRACTICE_CONTACT_DEVICE_ID.
…olds The real-code guard flagged `accepted: true`, a literal. The answer is now whether the practice wallet holds the contact after the add, as the real answer reports the contact stored. The tour suites pass (146). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
One conflict, practiceMode's imports: this branch's encodeBase32Crockford and main's routerQueryBin and decodeFramedEnvelopeV3 (practice ERA's protocolDefined from Rust's balance.list, #1098). Both kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
…1098 Since #1098, practice ERA's protocolDefined is Rust's balance.list row, so a simulated practice send reads balance.list as well as wallet.amount. Both are reads the sandbox lets through; nothing else crosses the bridge. Full frontend suite: 907 passed. tsc, eslint and the real-code guard clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
cryptskii
pushed a commit
that referenced
this pull request
Oct 2, 2026
No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
10 of 11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
During the guided tour, practice mode kept only the
dsmClientobject in practice. Every screen that imported a write directly reached the real wallet:This branch moves the sandbox to the bridge, where every call to native code passes, and expands the tour to cover the online beta. Offline sending and Bitcoin are left out.
Owner rulings (2026-10-01)
dsmClientinterception as convenience behavior only. … While practice mode is active, no state-changing Rust call reaches the real wallet unless it is explicitly designated as a practice-safe simulated action." Deny by default, with an explicit read allowlist; everything else gets "a deterministic 'blocked in practice mode' result without touching Rust state."Changes
The sandbox at the bridge (
bridge/practiceGate.ts,bridge/BridgeRegistry.ts). While practice mode is on,getBridgeInstance()returns the practice view. Every path to native code takes its bridge from there:callBin, the router ingress and host requests. Each request is read before it leaves, and only named reads cross:sofi.findRouteandsofi.vaults, which the router takes as invokes;ui_themeandsfx_enabled.Everything else gets "blocked in practice mode" in that channel's own wire shape and never reaches Rust or the host. That includes an unreadable request, and a raw
sendMessageBinframe carrying an ingress or host request. Practice mode's verb regex and its refusals are deleted; itsdsmClientoverrides now only simulate.Practice Add Contact answered
{ ok: true }, which the contacts store read as a refusal, and it stored one fixed practice id for every device. It now answersAddContactResultwith the card's real ids. A contact added with no alias gets the name Rust gives it.The tour, 79 steps (up from 57):
Bob's code comes from Rust. The dsm_sdk test
the_tour_practice_contact_code_is_one_rust_readsbuilds his card on the beta network and requiresread_contact_codeto read it back. It also keepscomponents/tour/practiceContact.tsequal to Rust's encoding (DSM_WRITE_FRONTEND_FIXTURES=1rewrites it). In the tour, pasting the code runs the realcontacts.readContactCode, a read the sandbox lets through; adding bob is practice mode's job. A tour step can now carry text, shown selectable with a Copy button.Rebased onto #1096. Practice sends name their recipient by device id, as the real
sendOnlineTransferSmartnow does: the simulated send takesPRACTICE_CONTACT_DEVICE_ID, andrecordSendmatches ondeviceIdonly. This was agreed with the SoFi/#1096 session.Tests
bridge/__tests__/practiceGate.test.tscovers the acceptance list:burnToken,sofi.tradeandsofi.resolveare blocked, as is a raw ingress frame; the bridge receives nothing;dsmClient.burnTokenandforgetTokenare blocked; a practice send crosses onlywallet.amountreads;ui_themecrosses, while the lock and diagnostics preferences don't; the camera crosses, an NFC write doesn't;leave()ends the sandbox.tour.test.tsx:data-touranchors are exactly fourteen, and each exists in the app;practiceMode.test.ts: the Add Contact shape and ids, and the no-alias name.token.burnon the read list turns 2 red;step wallet-identity: …);no step teaches offline sending or Bitcoinred.Verification (local, combined head f92a91a on e736dbc)
wallet_amountand ingress record tests pass, 7/0. Both frontend records still match live Rust after security: beta pre-audit fixes (combined CORE, STORAGE, SOFI) #1096's proto change.-p dsm_sdk --lib --tests -D warnings: 0. rustfmt check: 0.For the owner: one more baseline entry
This branch adds one guard line: the real
AddContactResultshape in the practice Add Contact.components/tour/practiceMode.ts:26355012bf03379d80freturn { accepted: true, contactId, alias };Path prefix:
dsm_client/frontend/src/. Main's frontend still reports #1095's 21 lines until main records them. Three of those are in this file (now lines 93, 215 and 227), with unchanged hashes.Notes
identity_routes.rsand screen files, so pins over those may read stale. I'll refresh them from this PR's CI map.