Conversation
- Default file pane view is now Grid (was Details), with a one-time migration that flips existing installs still on the old default while respecting any explicit choice made afterward. - fileIcons: add distinct glyphs/colours for design files (Photoshop, Illustrator, Figma, XD, Affinity, InDesign, CorelDRAW), 3D/CAD (blend/obj/fbx/gltf/stl/…), and more languages (Haskell/Clojure/Julia/Terraform/…) — so a PSD no longer looks like a JPG. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Known file types now show their actual Material Icon Theme icon — the Photoshop mark on a .psd, the Python logo on a .py, react_ts on .tsx, docker on a Dockerfile — the same set VS Code file managers use. The prebuilt manifest drives filename/extension → icon resolution (longest-extension match, e.g. app.d.ts), and the SVGs bundle as hashed assets via import.meta.glob (offline, no network). Folders, symlinks, and unmatched files keep the lucide glyphs. MIT-licensed; notice added in THIRD_PARTY_LICENSES.md. Verified: tsc clean, vite build emits the SVG assets, and the extension→icon mapping resolves to real files (psd/ai/py/fig/docker/blend/…). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implementation-ready plan for an additive Iconify layer (offline-bundled, permissive-licence sets) giving recognizable brand/protocol logos on connections. Explicitly scoped NOT to touch file-type icons (Material Icon Theme is more complete + carries the extension mapping Iconify lacks) or the lucide UI icons. Added to ROADMAP as Track E. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cross-backend disk-usage treemap + size-ranked tree, opening as a workspace tab like the SSH terminal. Reuses the RemoteFs walk so it works on SFTP/S3/FTP/Agent/ local; the differentiator is remote servers/buckets, with a shell du/find fast path (SSH + exec-gated Agent) and object-store flat listing for scale. Canvas treemap. Added to ROADMAP as Track F. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Plan 7 Directory Diff: any-backend (incl. remote↔remote) diff surfaced in GUI, faro-cli, and as an MCP faro_diff tool; reuses sync.rs's diff. - Plan 8 Fleet Search: name+content search, exec rg/grep fast path + walk fallback, GUI/CLI/MCP. - Plan 9 Fleet Skills: reshape of multi-server runner into AI-authorable, MCP-native reusable automations, built on the bridge's saved-commands + faro_exec + approvals; safety-gated. - Plan 10 Scoped connection sharing: time-boxed, path-jailed, read-only sharing via the agent's pairing/policy/revocation (no login system); browser view deferred as 10b. - ROADMAP: Tracks G-J + two near-term quick wins (editable chmod dialog, CLI conflict-policy flags). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The real sharing use case (remote employee, link-based, over the internet) needs auth regardless of delivery, so the whole track is parked rather than shipping the LAN-only half. Scoped-grant model stays the backbone for when login lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Safety hardening for the continuous folder-sync engine (Plan 2, Track A). Exclude patterns: each pair gains a gitignore-style `exclude` list. Matching files are filtered out of BOTH copies and deletes in the reconciler, so an excluded path is never uploaded and — critically — never mirror-deleted on the far side. Basename patterns (`node_modules`, `*.log`) match at any depth; path patterns (`/dist`, `build/**`) anchor at the root and cover descendants. Mirror-delete guard: gates the destructive half of a Mirror sync while letting uploads through. - Empty/unreadable source root (unmounted drive, deleted folder, typo) no longer wipes the destination — the diff's "everything is gone" is refused. - A configurable per-reconcile delete cap (`mirrorDeleteCap`, default 100) bounds the blast radius; over-cap deletes are withheld with a warning surfaced on the pair. The exclude + guard decision is factored into `apply_safety`, exercised by an on-disk test that runs the real `sync::plan` over temp dirs and asserts the filtering, the cap, and the empty-source refusal against actual files. Frontend: New-sync-pair form gains an exclude textarea and (under Mirror) a max-deletes-per-sync input; the pair row shows an exclude count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… Plan 3 Cut the persistent state index out of Plan 2 (folder sync) into a new Plan 3 — a shared scan engine + faro.db foundation that disk-usage, diff, and search all build on, rather than sync-private plumbing. Renumber every plan file to execution/build order and fix all cross-references (titles, inline Plan-N mentions, .md links): 3 = scan+index foundation 7 = fleet search 4 = disk usage 8 = fleet skills 5 = additional backends 9 = on-demand virtual folders 6 = directory diff 10 = icons, 11 = sharing (deferred) Add a "Plan build order" index to the ROADMAP and reorder its recommended sequence to match. Plan 2 is now marked code-complete (Phases 1-2 + safety); its remaining work is runtime verification, not code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduces `CLAUDE.md` as a repo working agreement covering Faro architecture, branch/release workflow, autonomy expectations, build/verification commands, and codebase layout. Updates `.gitignore` to exclude `.pr/` so local PR draft files from the `create-pr` skill are kept out of commits.
Add rusqlite (bundled) and a db.rs that opens one faro.db under the app data dir with forward-only, user_version-versioned migrations. First table is sync_state — folder-sync's per-file memory, keyed by (pair_id, rel_path) with the source-side size/mtime plus an opaque change token. No behavior change yet; later plans add tables as additive migrations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New scan.rs owns the recursive RemoteFs walk that sync.rs::plan used to do inline. It lists directories with bounded concurrency (latency, not CPU, dominates on SFTP/FTP) and exposes a progress callback + cooperative cancel token for the visible consumers to come (disk usage, diff, search). sync::plan now calls scan::walk_tree — behavior-preserving; the on-disk planner test still passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ChangeSignal (MtimeSize/Etag/Hash) to Capabilities and an optional etag to DirEntry, so callers stop inferring "did this change" from mtime alone. Every backend declares honestly: object stores report Etag and populate the token from object_store's e_tag; SFTP/FTP/local/agent report MtimeSize. The scan engine now carries the etag through to ScanEntry. Mirrored in the file-ui DirEntry/Capabilities types as optional fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reconciler now loads the pair's sync_state, plans via sync::plan_indexed (which consults the index for same-size edits the live size/mtime diff misses, flagging them SyncReason::Edited), and snapshots the source tree back into the index after each successful pass — seeding it on first run and pruning removed files. This closes the same-size-edit gap and makes relaunch a no-op instead of a full re-upload. Removing a pair clears its index rows. Backends declare their change signal (ETag for object stores, mtime+size otherwise); the index compares the opaque token when present. Added the "edited" reason to the frontend SyncReason union + badge. Verified: a real-Db + real-files test shows the stateless diff missing a same-size edit under a newer dest mtime, the index catching it, and a seeded index declining to re-upload; a file-based Db::open test covers the startup path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A ScanManager (shaped like TransferManager: Arc in AppState, scans in a Mutex<HashMap>, progress via diskscan:// events, cancel via a shared CancelToken) recursively sizes a directory over any RemoteFs by reusing the shared scan engine (scan::walk) and folds the flat file list into an aggregated size tree (DuNode: name/path/kind/size/children, sorted largest-first). Phase 1 is the generic walk that works on every backend; the ScanStrategy enum is wired for the exec/object fast paths in Phase 3. Commands: diskscan_start/status/tree/cancel/forget, registered in lib.rs. scan::ScanProgress gains bytes_found (additive; the sync poller ignores it) so the explorer can show a live byte total. Unit-tested tree aggregation + largest-first sort + root-path handling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A full-screen Disk Usage overlay (DiskUsageHost, driven by a dedicated diskScanStore that owns the scan lifecycle: progress events, cancel, drill-down, colour mode). The centrepiece is a Canvas squarified treemap (DiskTreemap) — recursive binary-split layout, theme-aware fills read from the live CSS tokens, hover readout + click-to-drill — beside a size-ranked "biggest items" list with percentage-of-parent bars. Breadcrumb navigates the drilled path; Rescan/Cancel and a by-type/by-depth colour toggle live in the header. Entry point follows the openTerminal pattern: an optional analyzeDiskUsage hook on the file-ui FileSystemAdapter, implemented in the Tauri adapter to open the explorer, surfaced as a FilePane toolbar button (current dir) + a directory context-menu item. materialIconUrl is now exported from @faro/file-ui so the list matches the browser's file icons. Typed ipc wrappers + onDiskScanEvent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ting
The scan now picks the fastest strategy from the live Session, falling back to
the generic walk on any failure so a scan never hard-fails:
- Object stores (S3/Azure): one flat `store.list(prefix)` returns every key +
size in a single streamed pass — no recursion. Progress ticks as keys arrive.
- SSH: `find <root> -type f -printf '%s\t%p\n'` over the exec channel, parsed
into the same flat file tree the walk produces — orders of magnitude fewer
round-trips than an SFTP walk. A non-zero exit with output (unreadable subdir)
is still accepted; a truly empty failure falls back.
- Faro Agent: the same `find`, via the daemon's bounded Exec (gated by
allowExec); denial/timeout/truncation fall back to the walk.
The chosen strategy shows as the header badge; a fallback records a short note
("exec disabled — used walk") surfaced next to it. diskscan_start now resolves
the Session and threads it through ScanManager::start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Right-clicking a treemap cell or a ranked-list row opens a context menu: - Reveal / Open in file browser — a one-shot revealTarget signal on layoutStore that FileBrowser + DualPaneBrowser consume to point the active pane at the item's folder (closing the explorer). - Copy path — to the clipboard. - Delete — confirmed via the shared ConfirmModal, then pruned from the tree in place (subtracting the freed bytes from every ancestor) so the map + list update instantly without a rescan. The scan root can't be deleted from here. Colour-by-type/depth and Rescan already shipped in Phase 2. "Remember last scan per connection" is intentionally deferred — the entry point always carries the current directory, so its marginal value doesn't justify a faro.db table yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A tokio integration test walks an actual temp directory over LocalFs and folds it into the aggregated size tree (the generic strategy every backend falls back to), plus a parser test that `find`'s output produces the identical tree shape — so the SSH/agent fast path and the walk agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Azure Blob and the aws/r2/b2 presets already shipped, so the plan was stale. Reordered by current cost (native GCS is now a one-builder move via object_store's gcp feature), added new backends (Box, pCloud, NFS stretch, read-only HTTP source, WebDAV provider presets, more S3 presets), resequenced the OAuth tier by API friction (Dropbox first — path-based), and synced ROADMAP Track B. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phases 1–4 land: the scan engine + generic walk, the Canvas treemap explorer + size list, the exec-find / object-flat fast paths, and the reveal/copy/delete actions. Core scan behaviour is runtime-verified against a real filesystem and the app boots clean; a GUI click-through on a live backend is the remaining manual check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Wasabi, DigitalOcean Spaces, MinIO, Storj, Hetzner, Scaleway, Oracle OCI, IBM COS, Supabase, and a generic self-hosted S3 preset to S3_PROVIDER_PRESETS, each with an endpoint template + default region. guessProvider now recognizes their endpoint patterns so an imported/edited profile preselects the right button, and the provider grid renders every preset from the table (vendor sub-label per row) instead of a hardcoded three. No backend code: the object-store connect path already treats any custom endpoint as path-style S3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GCS is the same one-builder move that shipped Azure: enable object_store's gcp feature and add gcs_connect() using GoogleCloudStorageBuilder. The bucket rides in profile.bucket like an S3 bucket / Azure container; the service-account key comes from either a JSON file path (Key auth) or the pasted JSON (Password auth). A GCS session is an ObjectSession, so browse, transfer, sync, disk-usage, and rename/delete all work through the existing ObjectFs + object transfer arms unchanged. Frontend: add the gcs Protocol (default port 443, label GCS, object-store group), a GcsSection in the New Connection editor with a file/paste key toggle, and route gcs through every protocol switch (rail badge gs://, bridge/import/search icons, mock capabilities). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new RemoteFs impl over WebDAV (Nextcloud, ownCloud, Hetzner Storage Box, and the generic long tail). One protocol, real directories, rename via MOVE, no chmod; change detection uses the per-resource ETag with mtime+size as the fallback for servers that omit getetag. Backend: - session/webdav.rs — WebdavSession (shared reqwest client, base URL carrying the DAV root, Basic/Bearer auth). Connect validates creds + URL with a depth-0 PROPFIND. url_for percent-encodes each path segment. - remotefs/webdav.rs — WebdavFs: list_dir (PROPFIND depth 1), rename (MOVE, no-clobber), delete (DELETE), create_dir (MKCOL), capabilities. A namespace- prefix-agnostic multistatus parser + a dependency-free RFC1123 date parser. - transfer.rs — streaming GET download (real progress) + streaming PUT upload; HEAD-based size/overwrite probes. - Session::Webdav variant threaded through session connect/disconnect, fs_for_session (commands + transfer), and editor.rs edit-in-place. Frontend: webdav Protocol (label WebDAV, HTTP :443, Globe icon), a WebdavSection in the New Connection editor with provider presets (Nextcloud/ownCloud/Storage Box/generic) that prefill the DAV URL template and a Basic/Bearer auth toggle; rail address shows the server URL. Verification: 5 parser unit tests (Nextcloud, Apache mod_dav, full-URL hrefs, HTTP-date, etag cleaning) + a live end-to-end #[ignore] test exercising connect -> MKCOL -> PUT -> PROPFIND -> GET -> MOVE -> DELETE through Faro's own WebdavFs, run green against a local wsgidav server with Basic auth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phases 0 (S3 presets), 1 (native GCS), and 2 (WebDAV) are built and verified. Note the Phase 3 SMB build-time finding: pavao/libsmbclient has no MSVC build and the pure-Rust smb crate is immature, so SMB is deferred pending a viable client + a NAS to verify against. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Browse any static file server as a Faro connection. A new RemoteFs impl that lists via autoindex and reads via GET; every mutation errors (read-only). Backend: - session/http.rs — HttpSession (shared reqwest client, optional Basic auth). Connect decides Listing vs DirectFile mode from the URL shape (trailing slash / a filename extension) so pasting a direct artifact URL works with no listing, and probes reachability + auth with a HEAD. - remotefs/http.rs — HttpFs: list_dir parses nginx/Apache autoindex HTML (anchor-based, handles <pre> and <table>) or nginx JSON autoindex; DirectFile mode HEADs the single file. rename/delete/create_dir/chmod all return a read-only error. change_signal Etag (from the file's own ETag/Last-Modified). - transfer.rs — streaming GET download; upload errors; HEAD-based size. - Session::Http threaded through connect/disconnect, both fs_for_session copies, and editor.rs (download works, save errors). - Shared webdav::parse_http_date (now pub(crate)) for Last-Modified / JSON mtime. Frontend: http Protocol (label HTTP, read-only :443, Download icon), an HttpSection with a URL field + optional Basic-auth toggle; rail address shows the URL. Verification: 5 parser unit tests (nginx HTML, Apache table with human sizes, nginx JSON, size tokens) + a live #[ignore] end-to-end test run green against python -m http.server in both listing and direct-file modes — list, GET round-trip, and refused mutations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oauth.rs: a provider-agnostic OAuth 2.0 loopback + PKCE (S256) authorization- code helper with no external OAuth crate — a fixed-port (:53682) local listener catches the redirect, the code is exchanged against a configurable token endpoint, and refresh is a second form POST. Endpoints are OAuthConfig fields so tests can point them at a local mock. Tokens persist in the OS keychain via the keyring crate (native backends only — no dbus dependency), never in profile JSON. Verified: PKCE S256 matches the RFC 7636 reference vector; authorize-URL, token-parsing, and expiry unit tests pass; a keychain write->read->delete round-trip runs green against Windows Credential Manager. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dropbox API v2 is path-addressed, so it slots straight into the trait. - session/dropbox.rs — DropboxSession: bearer-token RPC/content helpers with transparent token refresh (proactive on near-expiry + retry-once on 401), re-persisting refreshed tokens to the keychain. Endpoints fall back to env overrides so the flow can be pointed at a mock. account_label/exists/size helpers. Tokens loaded from the keychain at connect (errors clearly if the connection was never authorized). - remotefs/dropbox.rs — DropboxFs: list_folder (+ /continue pagination), move_v2, delete_v2, create_folder_v2; rev as the change token; ISO-8601 server_modified parsing. Faro↔Dropbox path mapping (/ ⇒ ""). - transfer.rs — streaming download (/2/files/download) + simple upload (/2/files/upload, overwrite; >150 MB refused with a clear message pending chunked upload_session); size/overwrite probes via get_metadata. - Session::Dropbox threaded through connect/disconnect, both fs_for_session copies, and editor.rs edit-in-place. Verified by unit tests (path mapping, ISO-8601, list_folder parsing incl. deleted-tombstone drop). OAuth infra reused from Phase 5a. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dropbox_authorize command runs the interactive OAuth flow (browser + loopback redirect), stores tokens in the OS keychain keyed by profile id, and returns the account label — mirroring agent pairing, so a cancelled flow leaves no saved connection. delete_profile now clears a Dropbox profile's keychain tokens. Frontend: dropbox Protocol (label Dropbox, Box icon, OAuth hint), a DropboxSection with a 'Connect with Dropbox' button that authorizes then gates Save on success and shows 'Connected as <account>'; rail address shows the account. Typed ipc.dropboxAuthorize wrapper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-demand virtual folders (OneDrive-style placeholders). Adds the "virtualfs" cargo feature (OFF by default; the windows crate + Cloud Filter FFI only compile with --features virtualfs) and a cross-platform VirtualFs subsystem mirroring agent_host.rs: persisted registered sync roots, orphan-safe reconcile (pure, unit-tested), a Hydrator abstraction that reuses the per-backend TransferManager download path, and virtualfs_supported/status/free_up_space commands. The native provider is behind a platform module — unsupported.rs is an inert stub so the default build compiles and an on-demand pair degrades to a clear "not available" rather than an error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a per-pair SyncMode to folder sync (default Mirror, so existing pairs are untouched). OnDemand pairs skip the eager reconcile loop and are handed to the VirtualFs provider instead: auto_start, upsert, remove, and set_enabled now all call reconcile_virtualfs, which pushes the enabled on-demand pair set into VirtualFs so it registers new roots and unregisters orphaned ones in one place. mode rides along in PairView via the existing flatten, so the UI can offer the toggle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The native on-demand provider, built on the safe cloud-filter 0.0.6
wrapper over cldapi.dll (swapped in for the raw windows crate to keep
the unsafe surface tiny — this code can only be verified manually in
Explorer). Per on-demand pair: register a per-pair sync root
(Faro.<pair-id> + user SID, so unregister is rebuildable from the id
alone for exact orphan cleanup), eagerly seed top-level placeholders
from a RemoteFs list_dir walk, then connect a SyncFilter whose
- fetch_data hydrates on access by downloading through the shared
TransferManager path and streaming it to the OS in 4 KiB-aligned
chunks (callbacks run on OS threads and bridge to Faro's async
backends via a captured tokio Handle + block_on),
- fetch_placeholders lazily populates subdirectories on expand,
- dehydrate allows "free up space" (mark_pin Unpinned over the tree),
- delete/rename stay local — an on-demand folder is a live view, never
mutating the backend.
Path blobs are validated against the pair's remote root before any
backend touch. Feature-gated (--features virtualfs); the default build
is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frontend for on-demand virtual folders. Adds the SyncMode type + mode field on SyncPair, the VirtualFsRootStatus type, and typed IPC wrappers (virtualFsSupported / virtualFsStatus / virtualFsFreeUpSpace). The sync pair form offers a Mirror | On-demand toggle — shown only where it works (virtualFsSupported, i.e. a Windows virtualfs build) — and hides the mirror-only direction/strategy/exclude/poll fields when on-demand is picked. Each on-demand pair row shows a cloud badge, a "hydrates on open" status, and a Free-up-space action in place of Sync-now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…safe) A runtime round-trip test caught a real blocker: cloud-filter registers sync roots through the WinRT StorageProviderSyncRootManager, which requires package identity — so from unpackaged Faro (Tauri/NSIS) Register returned Ok but silently registered nothing (GetSyncRootInformationForId → NOT_FOUND, active_roots showed only the real OneDrive). Fix: register/unregister via the Win32 CfRegisterSyncRoot / CfUnregisterSyncRoot (the API the plan specified — no package identity needed), keyed by path, and keep cloud-filter only for connect + placeholders + hydration callbacks (its Session::connect already uses the path-based Win32 CfConnectSyncRoot, so it composes cleanly). Policies default to Partial hydration + Partial population = fully on-demand. The register→CfGetSyncRootInfoByPath→unregister round-trip now passes on real cldapi from an unpackaged test process, verifying the orphan-safety primitive reconcile depends on. Full hydration still needs manual Explorer verification. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erify left) Update the plan doc status from "DESIGNED, NOT BUILT" to reflect the feature-flagged Windows Cloud Filter provider, document the Win32-vs-WinRT package-identity finding, and record what's runtime-verified vs left for manual Explorer testing. Mirror the same in ROADMAP (Plan 9 row + Track C). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
faro-agentd's exec() clamped a caller timeout to a private 10-min cap, so a bridge-accepted `--timeout-ms 900000` was silently shortened on paired-agent targets. Move EXEC_TIMEOUT_MS_MIN/MAX into faro-agent-proto and clamp against it in both the daemon and the bridge, so the documented 15-min ceiling holds on every target. Add a daemon unit test proving 12–15 min timeouts now pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app and faro-cli ship as separate downloads, so the CLI silently lags after an app update — which is what produced the session's cryptic `unexpected argument '--timeout-ms'`. The bridge already publishes its app version in agent-endpoint.json; faro-cli now reads it, compares to its own build version, and prints a one-line stderr staleness warning before running any `agent`/`skill` command. Best-effort semver compare — absent/unparseable versions suppress the check rather than false-alarm. Unit-tested and verified at runtime with a crafted endpoint file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
faro-cli agent upload/upload-dir/download now detect a Windows-drive-prefixed remote path (C:\… / C:/…) against a non-Windows server — the tell-tale of MSYS path conversion rewriting a POSIX /var/www argument before faro-cli sees it — and refuse with an actionable hint (MSYS_NO_PATHCONV=1, leading //, or `agent write`) instead of silently uploading to a nonsense directory. The /info OS lookup only runs on the drive-prefixed error path, so the common good path pays nothing. Escape documented in docs/remote-agent.md; decision logic unit-tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stics)
Adds `faro-cli agent script <server> <file>` and `agent exec --file/--stdin`:
the CLI reads the script's raw bytes locally and ships them as an opaque
base64 payload to a new bridge route `/exec_script`, which decodes and runs
them verbatim in the target's shell — `base64 -d | sh` on SSH (the tracked
exec path already does this), `sh -c`/`powershell -Command` on an agent. Since
the user's local shell never re-parses the program, heredocs, nested quotes
and newlines survive. The session's breakage was really the CLI's
`command.join(" ")` on shell-split args, which this bypasses entirely.
Gated as an Exec but, like a Write, never auto-approved by the safe-read-only
heuristic (only allow-all) — a script is multi-statement. The approval prompt
shows a header + the script body (capped); the console/audit show a friendly
label, never the body. Also exposed as the MCP `faro_exec_script` tool so the
in-app AI gets the same affordance. exec_core_agent gained a label param.
Verified: daemon runs a multi-line PowerShell script with nested quotes
verbatim (exit 0, output intact); base64 wire round-trip is byte-exact;
approval summary survives multibyte truncation. Documented in remote-agent.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds `faro-cli agent write <server> <remote-path> [--from-file|--stdin| --content] [--overwrite]`, the MCP `faro_write` tool, and a new bridge route /write. SSH targets stream via SFTP create; Faro Agent targets via a single truncating WriteChunk. This is the direct answer to "a dedicated edit command would have let me drop a small debug script or patch on the server" — no staging local file, no upload path to mangle. Content rides the wire as base64 so any bytes survive; refuses to overwrite an existing file unless asked. Gated as a Write (never auto-approved except allow-all). The CLI path also runs the Phase 3 MSYS mangling guard on the remote path. The agent write path is the same WriteChunk round-trip the end-to-end harness verifies byte-exact over a real Noise channel; the SSH path reuses the production upload's SFTP create. Documented in remote-agent.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds `faro-cli self-update [--tag <v>] [--check]`: resolves the release asset matching this OS/arch (the same faro-cli-<os>-<arch> assets the release workflow ships), downloads it from GitHub over HTTPS, and swaps it in. The swap writes the new bytes to a sibling temp file, then renames — atomically over the target on Unix, and on Windows (where a running exe can't be overwritten) moves the current exe aside and the new one in, rolling back on failure. --check only compares this binary's version to GitHub's latest. Enables ureq's rustls TLS (previously localhost-http only) so the CLI can reach GitHub. Verified live: `self-update --check` reports v1.3.19 → latest v1.3.21 over HTTPS; a full self-update on a temp copy downloaded and swapped in the release binary, which then reported v1.3.21. Swap mechanism + asset-URL building are also unit-tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erence Adds a stateful `cli_updater` subsystem shaped like agent_host.rs: on launch it locates the installed faro-cli (sidecar next to the app, else PATH via where/which), reads its --version, and compares to the app version. Per the persisted cliUpdate preference it prompts (ask, default), updates silently (auto), or stays quiet (off). It emits "cli-updater://status"; updating delegates to `faro-cli self-update` when the CLI is present, or downloads the matching asset into the app data dir when it's missing. Frontend: typed ipc wrappers + onCliUpdaterStatus listener, a cliUpdaterStore, a non-blocking <CliUpdatePrompt> above the status bar (Update now / Always update automatically / dismiss) that appears only when installed && stale && ask, and a Settings → faro-cli section (status card + Update/Check + the ask/auto/off preference). The missing-CLI install affordance lives in Settings rather than a global banner, to avoid nagging users who don't use the CLI. Verified live: launched the real app with a stale CLI on PATH — the startup check logged `installed=true cli_version=1.3.7 app_version=1.3.19 stale=true mode=Ask`, exactly the state that raises the prompt. locate+version tested against a real binary; tsc clean; cargo check clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds `faro-cli fetch <url> [--profile <name>]`: performs an authenticated GET by reusing a saved HTTP(S) profile's stored Basic-Auth credentials (Plan 5 Phase 4's HttpSession applies the header), so an agent can read a rendered page on an auth-walled staging site instead of injecting debug logging. The profile is matched by the URL's host, or chosen with --profile; the body goes to stdout and a non-2xx status exits non-zero. Credentials are never echoed. Verified end-to-end against a local Basic-Auth server: `fetch` returned the protected page via the saved profile (both host-matched and explicit --profile), with no password leaked to stderr. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`faro-cli agent exec <server> --detach "<cmd>"` (and MCP `faro_exec` with
detach=true) launches the command server-side and returns a job id at once
instead of blocking on the exec timeout — retiring the manual
`nohup … & ; tail -f log` loop for multi-minute backfills/migrations.
SSH-first, pure convention (no protocol change): the command runs detached
via `setsid`/`nohup` into a per-job dir `~/.faro/jobs/<id>/{cmd,out,err,exit,
pid}`. `faro-cli agent job <server> <id>` (MCP `faro_job`) polls the captured
stdout/stderr (512 KiB cap each) and mirrors the exit code once finished;
`faro-cli agent jobs <server>` lists running/finished jobs. Launch base64s
the command so no quoting breaks the wrapper; the poll base64s out/err so no
marker collides with content; job ids are UUID-charset-validated before they
touch the shell path. Job dirs older than 7 days are pruned on next launch.
Gated as an Exec (never auto-approved except allow-all); poll/list are Reads.
Detached exec on a paired Faro Agent target returns a clear "not yet" error —
the ExecStart/Poll/Kill protocol arm is the documented next step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the daemon-target half of background jobs so `--detach` / faro_job work against a paired machine, not just SSH. New protocol ops (additive, no version bump) ExecStart/ExecPoll/ExecKill; the daemon spawns the command, streams its stdout/stderr into capped in-memory buffers, records the exit code, and answers polls by job id. A shared JobStore (on the Daemon behind an Arc) means a job started on one channel survives the controller re-dialing; finished jobs are reaped on a TTL and the store is bounded. Killable via a oneshot to the waiter task. The one gap versus the SSH arm's on-disk dir: a daemon restart forgets in-flight jobs (a later poll returns not_found). Wiring: op_exec_detached / op_job in bridge.rs now route agent targets through the daemon (SSH path unchanged); faro_job accepts agent sessions (Exec, not SshOnly); MCP schemas updated. Because the ops are additive, existing agent ops still work against an older daemon — only detached-job requests fail, with a clear "update faro-agentd" message. Verified: JobStore unit tests (start/poll/kill on a real Windows child) + a new end-to-end test drives pair → ExecStart → poll-to-exit → not_found over the real Noise channel. Live run against the paired phone (needs the Android daemon rebuilt from this tree) is left to the maintainer, per the sibling plans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 4 background jobs now land on both arms (SSH job dir + paired-agent ExecStart/Poll/Kill). Live SSH-box + phone-agent smoke tests remain the maintainer's step, per the sibling plans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-only The agent arm now backs `--detach` / `agent job`, so drop the "SSH only" from their help. `agent jobs` (list) stays SSH-only and keeps its note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The demo build's `invoke` mock resolved unhandled commands to null. Once the status bar started reading `useSync().pairs` and calling `.filter` on it unconditionally, the null return from `foldersync_list` crashed StatusBar on boot and blanked the whole app (every screenshot came out black). Handle the five `foldersync_*` commands the way the real backend does — return the pair list — so the demo renders again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The editor was a tall 32rem single column with a 13-button protocol grid stacked above the form — cramped now that the backend list keeps growing. Rework it into a 52rem two-pane dialog like Settings / Agent Bridge: a grouped, scrollable protocol rail on the left (Servers · Object storage · Web · Cloud drives · Machine) and the form on the right, with the selected protocol's label + port in the pane header. New backends slot into a group instead of making the dialog taller. Screenshots regenerated (all shots refreshed; new-connection.png shows the new layout). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The README still described eight backends and a file-manager-plus-sync feature set; the app now spans thirteen backends and a lot more. Bring it current: - Backends table rebuilt — S3-compatible (13 vendor presets), Azure Blob, GCS, WebDAV, read-only HTTP, the Dropbox/OneDrive/Google Drive/Box OAuth clouds, and Faro Agent, with capability notes. - New sections: Explore/diff/search (Disk Usage Explorer, Directory Diff incl. remote↔remote, Fleet Search), continuous Folder Sync, and Fleet Skills. - Highlights, CLI (diff/search/agent/skill/fetch/self-update), Architecture, Layout, and Roadmap updated to match what actually ships. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The disk-usage treemap is the standout new UI and had no shot. Teach the demo/mock build to drive it: a fictional /var/www/api size tree + a done ScanSnapshot in the mock data, the five diskscan_* commands wired in the invoke mock, and the diskScanStore exposed on window.__demo so the capture script can open the explorer straight into a finished scan. Adds docs/screenshots/ disk-usage.png (force-added past the docs/screenshots/*.png ignore, like the other curated shots) and features it near the top of the README's Screenshots section, expanded by default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t, take main's version bumps)
Add an in-app SSH key generator to the New Connection editor's private-key
auth, so users never have to leave Faro (or open PuTTYgen) to create a key.
- New keys module (src-tauri/src/keys.rs): generate Ed25519 / RSA keypairs,
write the private key in native OpenSSH format (encrypted with bcrypt-pbkdf +
aes256-ctr when a passphrase is given) plus a .pub beside it, and return the
public-key line ready for the server's ~/.ssh/authorized_keys. Uses the
ssh-key crate rather than russh for writing: russh only emits PKCS#8 PEM,
which OpenSSH itself refuses to load for Ed25519 ("error in libcrypto"). The
format ssh-key writes is read back by both OpenSSH's ssh/ssh-keygen and
russh's own load_secret_key (the SSH connect path), verified by tests.
- Commands: generate_ssh_key, ssh_public_key_for (copy the public half of an
existing key without regenerating), ssh_key_defaults (suggest a free
~/.ssh/faro_ed25519 path).
- ProfileEditor: a "Generate new key…" panel (type, save location, optional
passphrase) that points the connection at the new key and shows the public
key with a Copy button + authorized_keys nudge; a "Copy public key" action
and a Browse button for existing keys.
- Connect path now expands a leading ~ in a key path, so both generated
absolute paths and stored ~/.ssh/... paths load (load_secret_key does not).
Tests cover the Ed25519 encrypted round-trip through the real connect-path
loader, RSA generation, and an interop check that the system ssh-keygen accepts
what Faro writes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `.marketing/` to `.gitignore` with a clarifying comment so local marketing drafts and channel notes stay out of version control, consistent with other local scratch directories.
# Conflicts: # src/components/ProfileEditor.tsx
There was a problem hiding this comment.
Pull request overview
This PR adds first-class, in-app SSH keypair generation and public-key derivation to streamline SFTP key-based auth setup, while also fixing ~-prefixed key paths during SSH connect by expanding them before loading.
Changes:
- Added a new Rust
keysmodule and Tauri commands to generate OpenSSH-format Ed25519/RSA keys and derive public keys/fingerprints. - Updated the SFTP key-auth UI in
ProfileEditorwith a generator flow, overwrite handling, and clipboard copy for the public key. - Fixed SSH key auth to expand
~/.ssh/...paths beforerussh_keys::load_secret_key.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/lib/types.ts | Adds frontend types for key generation/derivation IPC payloads. |
| src/lib/ipc.ts | Adds typed IPC wrappers for ssh-key defaults, generation, and public-key derivation. |
| src/components/ProfileEditor.tsx | Replaces basic key-path inputs with a full key-auth section (generate/derive/copy). |
| src-tauri/src/session/mod.rs | Expands ~ in stored key paths before loading secret keys. |
| src-tauri/src/lib.rs | Registers the new keys module and command handlers. |
| src-tauri/src/keys.rs | Implements OpenSSH-format key generation, public key derivation, tilde expansion, and tests. |
| src-tauri/src/commands.rs | Exposes key-related Tauri commands (defaults/generate/derive). |
| src-tauri/Cargo.toml | Adds direct ssh-key dependency for OpenSSH private-key output. |
| src-tauri/Cargo.lock | Updates lockfile for the new dependency (but currently has workspace version mismatches). |
| .gitignore | Ignores .marketing/ local scratch directory. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+82
to
+85
| /// Generate a new SSH keypair, write the private key (PKCS#8 PEM, encrypted when | ||
| /// a passphrase is given) plus a `.pub` beside it, and return the public-key line | ||
| /// to install on the server. Key generation is CPU-bound (RSA-4096 especially), | ||
| /// so it runs on the blocking pool rather than the async runtime. |
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.
Setting up key auth used to mean leaving Faro, wrestling PuTTYgen or
ssh-keygeninto producing the right format, remembering where you saved the thing, and then pasting a public key you hoped was correct. This PR folds that whole detour into the New Connection editor: pick Ed25519 (or RSA for older servers), optionally set a passphrase, and Faro writes the keypair, points the connection at it, and hands back the public-key line already copied to your clipboard. The tricky part was format — russh can only write PKCS#8 PEM, which OpenSSH itself refuses to load for Ed25519 ("error in libcrypto"), so a key generated the obvious way would work inside Faro and nowhere else. We generate through thessh-keycrate instead, which writes real-----BEGIN OPENSSH PRIVATE KEY-----files (bcrypt-pbkdf + aes256-ctr when encrypted, exactly likessh-keygen) that both OpenSSH and russh's own loader read back. A stored~/.ssh/…path now also resolves on connect, which it quietly didn't before.Highlights
authorized_keys— with itsSHA256:…fingerprint shown so you can confirm it.sshandssh-keygentoo — not just inside Faro.~/.ssh/id_...path now connect correctly — the leading~is expanded on load.Technical changes
src-tauri/src/keys.rsmodule (registered inlib.rs):generate,public_key_for,defaults, andexpand_tilde. Private keys are written in native OpenSSH format via thessh-keycrate (encrypted with bcrypt-pbkdf + aes256-ctr when a passphrase is given); public keys are emitted as standard one-line OpenSSH (ssh-ed25519 AAAA… comment).ssh-key = "0.6"dependency (ed25519,rsa,encryption,getrandom) — already present as a russh transitive, now pulled directly for the generator. See theCargo.tomlcomment for why russh's own PKCS#8 PEM output can't be used.commands.rs:ssh_key_defaults(suggests~/.ssh+ a non-colliding filename),generate_ssh_key, andssh_public_key_for. The two key-deriving commands run onspawn_blockingbecause generation is CPU-bound (RSA-4096 in particular) and would otherwise stall the async runtime.generaterefuses to overwrite an existing key unlessoverwrite: trueis set, so an existing key is never clobbered by accident.session/mod.rs: theAuthMethod::Keyconnect path now runs the stored path throughkeys::expand_tildebeforerussh_keys::load_secret_key, which does no tilde expansion of its own — fixing silent failures for~/.ssh/...paths and letting generator-written keys load.types.ts(SshKeyType,GenerateKeyRequest,GeneratedKey,SshKeyDefaults) mirror the Rust structs; typed IPC wrapperssshKeyDefaults,generateSshKey,sshPublicKeyForinipc.ts(no rawinvokein the component).ProfileEditor.tsx: key-auth section gains the generator UI — algorithm toggle, passphrase field, save-path input (prefilled fromssh_key_defaults), an overwrite escape hatch surfaced only after an "already exists" error, and a copyable public-key box with fingerprint. Clipboard copy on both generate and derive, with toasts for success and for the encrypted-key / passphrase-needed failure cases..gitignore: ignore the local.marketing/scratch directory.