Commit 25ec0d8
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
- .github/workflows
- android/app/src
- main/java/co/mearman/cascade
- test/java/co/mearman/cascade
- apps/web
- src
- api
- components
- routes
- crates
- backend-local/src
- backend-p2p/src
- sync
- cascade-ffi
- bindings
- kotlin/uniffi/cascade_ffi
- swift
- src
- cascade-web-api
- src
- routes
- tests
- cascade
- src/cli
- engine/src
- engine
- manage
- p2p/src
- swift/CascadeFileProvideriOS/Extension/Sources
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
366 | 366 | | |
367 | 367 | | |
368 | 368 | | |
369 | | - | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
370 | 378 | | |
371 | 379 | | |
372 | 380 | | |
| |||
0 commit comments