Skip to content

Commit 25ec0d8

Browse files
authored
Fill the read-only UI gaps: web/mobile/CLI writes, exec, TUI (#33)
* feat(ffi): bridge write verbs (upload, create_dir, delete, rename) to mobile Add CascadeNode async methods for the engine's Backend write surface, resolving the backend via the VFS tree the same way list_dir/read_file do. Upload resolves the parent FileId via backend metadata (matching the WebDAV presenter) and overwrites via update when the file exists; rename handles cross-backend moves by download/upload/delete. Map all errors onto a new CascadeError::Write variant. Wire the iOS FileProviderExtension's createItem/modifyItem/deleteItem through the new verbs (replacing the noSuchItem stubs) and add write/delete capabilities to FileProviderItem so the Files app surfaces the UI. On Android, implement openDocument write-back: a "w"/"rw" open returns a pipe whose contents are uploaded on close, and set FLAG_SUPPORTS_WRITE on file rows. Create/delete/rename stay unadvertised because the SAF provider surface does not expose override hooks for them without a custom call protocol. Regenerate the committed UniFFI Swift and Kotlin bindings to include the four new methods and the Write error variant. Add a cascade-ffi round-trip test (mkdir -> list -> upload -> read -> rename -> delete) plus overwrite, missing-delete, and leading-slash create_dir tests, and an Android CursorBuilder test verifying the write flag is advertised on file rows. * feat(cli): add remote exec/shell verbs and engine-backed file verbs Feature A — Remote exec (cascade remote <device> exec/shell): - Add RemoteCommand::Exec and Shell variants wired to ManageCommand::PtySpawn - Exec: one-shot command, drain stdout/stderr to terminal - Shell: interactive, pump stdin via tokio::select with output draining - Surface the exec data plane to the manager side: add exec_stream_consumers registry to SyncEngine, route inbound ExecStream frames to registered consumers in handle_message, send ExecStreamAck back - Add subscribe/unsubscribe_exec_stream, send_pty_write/resize/signal on SyncEngine and P2pBackend - Add WireStreamKind and ExecStreamFrame types in p2p::exec_stream - Unit tests for argv->PtySpawn construction and scope authorisation Feature B — CLI file verbs (cascade ls/cat/mkdir/cp/mv/rm): - Construct a NativeEngine from on-disk config (same as daemon startup) - Forward to Backend trait via VfsTree resolve: list_children, metadata, download, create_dir, upload, delete, move_entry - Cross-backend cp/mv: download from source, upload to destination - Integration test: mkdir -> ls -> cat -> rm round-trip with local backend * feat(web): add file management write ops and terminal websocket Feature A — Web file management: - Add POST /v1/folders/{folder}/dirs/{path} for creating directories - Add POST /v1/folders/{folder}/move for rename/move operations - Both gated by Capability::DataWrite and the F3 data-plane readiness bit - Wire TS client methods (createDir, moveEntry) and FilesPage UI: upload (drag-drop + file picker), new folder, rename, delete - Add route tests for capability gating and data-plane readiness Feature B — Web terminal: - Add GET /v1/exec/ws websocket route driving the engine ExecProvider - Gated by Capability::ExecPty, token passed via query params (browser WebSocket API cannot set custom headers) - Add xterm.js + addon-fit as deps, TerminalPage component with keystroke forwarding, output rendering, and resize - Register Terminal route in App router and navigation - Add route tests for websocket registration Engine: add public exec() accessor for the ExecProvider * fix(docs): resolve broken intra-doc link in exec_stream.rs The doc comment on ExecStreamFrame linked to crate::SyncEngine::subscribe_exec_stream, but SyncEngine is not in the cascade_p2p crate -- it lives in cascade-backend-p2p (which depends on cascade_p2p), so the link could never resolve and adding the dependency edge would be circular. Replace the unresolvable link with plain prose that names the type and its crate. * fix(docs): resolve unresolved VfsTree intra-doc link in cli/files.rs The module-level doc comment used a bare [`VfsTree`] intra-doc link, but VfsTree is not use-imported in this file (only resolve_listing_native_id is imported from cascade_engine::vfs). rustdoc therefore reported an unresolved link. Fully-qualify the link to cascade_engine::vfs::VfsTree, matching the existing pattern already used on line 142 for VfsTree::rename. * fix(exec): make the web terminal and remote shell actually authorise The exec surfaces landed in a state where two of them never worked, found by the post-merge review. The web terminal's websocket handler authorised exec:pty over Scope::Node. exec:pty is a dangerous capability, and the authoriser never satisfies a dangerous capability from a node-wide scope, so no grant could ever pass the check — every upgrade was rejected before a PTY was spawned. The route tests only asserted "not 404", so it shipped green. The terminal now takes the folder it opens in as a query parameter, authorises exec:pty over that folder scope (refusing the root, which normalises to node-wide), and seeds the PTY's working directory from it. The folder/scope gate is extracted into a pure helper with unit tests covering missing, blank, root, and real folders. Session-id-only verbs (PtyWrite/Resize/Kill, ProcSignal/Kill) were unreachable for a second, independent reason. The manager sends them with a node-wide wire scope as a placeholder, and the dispatcher was authorising over both the session's stored scope and that wire scope. The wire-scope half is unsatisfiable for a dangerous capability, so every follow-up after a successful PtySpawn was rejected — an interactive shell could never send stdin, and the cleanup signal always failed. The dispatcher already derives a session verb's real target from node state and documents that the wire scope is ignored for them, so the fix is to stop gating session verbs on the wire scope; the session scope remains the authoritative confinement. A dispatch test pins the node-wire-scope case that used to fail. Token-authenticated callers hit a third wall: send_pty_write/resize/signal dropped the capability token, so a caller with no on-node grant could spawn but never write. The token is now threaded through the manager and re-presented on every session verb, including the cleanup kill. Also fixes a PTY leak in the websocket path: if subscribe() returned None after a successful spawn the handler returned without killing the session, orphaning a process on the node. The kill now runs before the early return. The one-shot exec never surfaces the process exit code (the exec data plane has no exit frame), so the Exec docstring no longer claims the CLI exits with it. * fix(backend-local): sandbox VFS paths against .. escape from the root absolute_path did root.join(nested) with the leading slash stripped, so a caller path like /notes/../../../etc/passwd resolved through the VFS, dropped the mount prefix, and let the OS walk the .. components out of the backend root. The local backend exists to confine a subtree; this broke that invariant. gdrive and S3 walk entries by name so they are unaffected, which is also why a blanket .. rejection at the CLI/FFI boundary would be wrong — only the OS-backed local backend has the escape. Every local-backend path now goes through resolve_under_root, which walks the relative components under the root, folds . away, climbs .. while it has a component to pop, and refuses the moment a .. would rise above the root. metadata, upload, create_dir, move_entry, and list_children all route through it, so writes and reads can no longer leave the sandbox. Tests cover an escaping create_dir and metadata (rejected, nothing written outside the root) and a non-escaping /a/../b that folds to /b normally. * feat(cli): drive the remote shell as a real raw-mode PTY The interactive shell read local stdin through BufReader::lines, so it only forwarded a line after Enter, the local terminal echoed and line-buffered every keystroke, and ^C never reached the remote — it just killed the local cascade. terminal_size was a const stub returning None, so the spawned PTY always opened at 80x24 regardless of the real terminal. The shell now enables raw mode (crossterm) when stdin is a TTY and forwards stdin byte-for-byte, so keystrokes, arrow keys, and ^C (0x03, which the remote PTY turns into SIGINT for the child) all reach the session. Output is rendered straight to the terminal. Local resizes are detected by polling terminal::size on a 250ms tick — an ioctl that does not consume input, so it does not contend with the byte reader the way crossterm's event loop would — and forwarded as PtyResize. Raw mode is restored on exit through a Drop guard, including on error. terminal_size now probes the real size via crossterm, so the spawn opens at the live dimensions. When stdin is piped (non-TTY) raw mode is skipped and input is forwarded as a byte stream. * docs(backend-p2p): fully-qualify the send_pty_write intra-doc links The "See [send_pty_write]" references in the PtyResize/PtyKill doc comments did not resolve as bare intra-doc links, breaking `cargo doc -D warnings`. Point them at SyncEngine::send_pty_write. * feat(cli): add a status/file/pin TUI over the engine There was no terminal UI at all; the only interactive surfaces were the OS mount and the PWA. `cascade tui` opens an alternate-screen, raw-mode interface that drives the same engine the daemon and the file verbs use, so no mount needs to be running. The top pane shows daemon state and the configured backends (name, type, mount) read from the state database; the bottom pane browses the VFS — descend into directories, ascend back out — and can pin or unpin the selected entry through the same CacheManager the pin/unpin commands use. Keys: q/Ctrl-C quit, j/k or arrows move, enter descend, h/Left ascend, p pin, P unpin, r refresh. The terminal is always restored on exit, even on error. The VFS listing and engine construction are shared with the file verbs (files::build_engine / files::list_dir) so the TUI and ls present the same view; path helpers are unit-tested. * fix(web): resolve PWA lint errors in the terminal and file-management surfaces The new TS tripped the repo's strict eslint config, which the verify gate had not run (it did typecheck/test/build but not `pnpm run lint`). Replaced type assertions with runtime narrowing and the generic querySelector, dropped an unnecessary null check on the non-nullable DataTransfer.files, stringified a number in a template literal, and removed a dead eslint-disable comment. * fix(cli): quote the local backend path in the round-trip test for Windows The test embedded the temp dir path in a double-quoted TOML string, so a Windows path full of backslashes failed to parse (backslash is the escape character in a TOML basic string). A literal (single-quoted) string treats backslashes verbatim and parses on every platform. * ci(android): disable the emulator package verifier before the e2e install The instrumented test run intermittently died before any test executed with INSTALL_FAILED_VERIFICATION_FAILURE: the emulator's package integrity verification timed out committing the debug APK install. There is nothing to verify on a self-signed debug APK on a throwaway emulator, so turn the verifier off (and adb-install verification) before running connectedDebug AndroidTest.
1 parent 3316b7e commit 25ec0d8

42 files changed

Lines changed: 4618 additions & 122 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,15 @@ jobs:
366366
arch: x86_64
367367
target: default
368368
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
369-
script: ./gradlew :app:connectedDebugAndroidTest --no-daemon
369+
# Disable the package verifier before installing the test APK. The
370+
# emulator's integrity verification stalls intermittently under load
371+
# and surfaces as INSTALL_FAILED_VERIFICATION_FAILURE, which fails the
372+
# instrumented test run before any test executes. There is nothing to
373+
# verify on a self-signed debug APK on a throwaway emulator.
374+
script: >-
375+
adb shell settings put global package_verifier_enable 0 &&
376+
adb shell settings put global verifier_verify_adb_installs 0 &&
377+
./gradlew :app:connectedDebugAndroidTest --no-daemon
370378
working-directory: android
371379

372380
# Attaches the Android debug APK to the GitHub release. Runs only when the

0 commit comments

Comments
 (0)