Skip to content

feat(minimald): support sftp to session filesystem - #267

Merged
twitchyliquid64 merged 2 commits into
mainfrom
tom/sftp
Jun 1, 2026
Merged

feat(minimald): support sftp to session filesystem#267
twitchyliquid64 merged 2 commits into
mainfrom
tom/sftp

Conversation

@twitchyliquid64

@twitchyliquid64 twitchyliquid64 commented May 31, 2026

Copy link
Copy Markdown
Member

Relies on earlier PR, review that first to get a better diffbase: #265

The sftp handler implements enough that you can scp in/out of the session:

xxx@lptp:~$ MINIMAL_SESSION_ID=019e75e7-0804-7d43-88d5-f8acec59534a scp -o SendEnv=MINIMAL_SESSION_ID \
     -o ProxyCommand='socat - UNIX-CONNECT:/home/xxx/.local/state/minimal/providers/local-0/ssh.sock' \
     -o 'StrictHostKeyChecking=no' -o 'UserKnownHostsFile=/dev/null' \
         functional_gemma_test.py username@whatever:
Warning: Permanently added 'local' (ED25519) to the list of known hosts.
functional_gemma_test.py

xxx@lptp:~$ diff ~/.local/state/minimal/sessions/9534a/tree/functional_gemma_test.py ~/functional_gemma_test.py
xxx@lptp:~$

Mounting via sshfs and stuff should also work, just havent tested.

Summary by CodeRabbit

  • New Features

    • Added SFTP file-transfer support with workspace isolation.
    • Added RPC for creating sessions programmatically.
  • Bug Fixes / Improvements

    • Enforced global uniqueness of session names.
    • Improved on-disk session index and lookup for reliable listing and name resolution.

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2027b011-2045-4cb4-9029-fc25ccf8db8e

📥 Commits

Reviewing files that changed from the base of the PR and between 9521c53 and 0fec7e8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • Cargo.toml
  • crates/minimald/Cargo.toml
  • crates/minimald/src/connection.rs
  • crates/minimald/src/lib.rs
  • crates/minimald/src/sftp.rs
  • crates/minimald/src/test_harness.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/minimald/Cargo.toml
  • crates/minimald/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • Cargo.toml
  • crates/minimald/src/connection.rs
  • crates/minimald/src/sftp.rs

📝 Walkthrough

Walkthrough

This PR implements SFTP subsystem support in minimald by adding session name uniqueness enforcement, a CreateSession RPC, complete SFTP file operations with workspace-scoped path safety, and SSH subsystem integration across session storage, RPC/manager layers, SFTP handler, tests, and dependency updates.

Changes

SFTP Subsystem with Session Management

Layer / File(s) Summary
Session storage index with name uniqueness
crates/sessions/src/lib.rs, crates/sessions/src/store.rs
Session index evolves from a single UUID map to a dual-keyed Index struct supporting both short-form and name-based lookups with persistent serde serialization. Session creation now enforces global name uniqueness, rejecting duplicates with AlreadyExists.
CreateSession RPC and session manager
crates/minimald/src/rpc.rs, crates/minimald/src/sessions.rs
New CreateSession RPC types and OneshotSshRpc impl wired into the dispatcher; sessions manager actor gains ManagerMessage::CreateSession variant and create_session public method delegating to the updated store; includes test verifying create→list/lookup.
SFTP error handling and path resolution
crates/minimald/src/sftp.rs (constants, error, session foundation)
SftpError maps I/O kinds and protocol violations to russh_sftp StatusCodes; SftpSession implements per-channel handle state, workspace-scoped path normalization via path_absolutize, canonicalization helpers, and escape-path rejection.
SFTP Handler trait implementation
crates/minimald/src/sftp.rs (handler methods)
Full russh_sftp::server::Handler covering realpath/stat, open/close, read/write with offsets, directory listing with READDIR_BATCH pagination, and filesystem mutations (mkdir/rmdir/remove/rename/setstat/fsetstat) operating on resolved workspace paths.
SFTP SSH subsystem integration
crates/minimald/src/lib.rs, crates/minimald/src/connection.rs, crates/minimald/src/sftp.rs (entrypoint)
mod sftp; added; subsystem_request recognizes SFTP and routes to handle_sftp_subsystem, which reads MINIMAL_SESSION_ID from channel env, resolves the session workspace via the sessions manager, and spawns russh_sftp::server::run with an initialized SftpSession.
Dependencies and test infrastructure
Cargo.toml, crates/minimald/Cargo.toml, crates/minimald/src/test_harness.rs, crates/minimald/src/sftp.rs (tests)
Workspace dependencies added (russh-sftp = "2.1", bytes, path-absolutize); test harness adds open_sftp to obtain a russh_sftp client session; integration tests validate write/read round-trip, realpath(".") canonicalization, and workspace-escape rejection.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • gominimal/minimal#235: Related changes to crates/minimald/src/connection.rs subsystem dispatch and SSH subsystem routing.
  • gominimal/minimal#248: Introduced sessions path primitives used by the SFTP subsystem (e.g., DaemonAbsPath).
  • gominimal/minimal#227: Scaffolds the ConnectionHandler that this PR extends for SFTP subsystem dispatch.

Suggested reviewers

  • norrietaylor
  • evanspearman
  • 0chroma

Poem

🐰 I stitched a tunnel of bits and keys,
I guarded names and rooted trees,
Files hop in, read and then stay—
Paths kept tidy, none stray away.
Hooray for SFTP, hooray for play!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(minimald): support sftp to session filesystem' clearly and specifically describes the main change—adding SFTP support to expose session filesystems over SSH, which aligns with the PR's core objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
crates/minimald/src/test_harness.rs (1)

151-151: ⚡ Quick win

Use the SFTP subsystem name constant instead of a hardcoded string.

The subsystem name is hardcoded as "sftp" here, but should use crate::sftp::SUBSYSTEM_NAME for consistency with the subsystem dispatch logic in connection.rs (line 370). This ensures the test and production code stay in sync if the subsystem name ever changes.

♻️ Suggested fix
-        channel.request_subsystem(true, "sftp").await.unwrap();
+        channel.request_subsystem(true, crate::sftp::SUBSYSTEM_NAME).await.unwrap();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/test_harness.rs` at line 151, The test uses a hardcoded
subsystem string in channel.request_subsystem(true, "sftp").await.unwrap(); —
replace the literal "sftp" with the subsystem name constant
crate::sftp::SUBSYSTEM_NAME to keep tests consistent with the production
dispatch logic; update the call to use crate::sftp::SUBSYSTEM_NAME wherever
channel.request_subsystem is invoked in this test (referencing the
channel.request_subsystem function and the crate::sftp::SUBSYSTEM_NAME
constant).
crates/minimald/src/sftp.rs (1)

278-312: 🏗️ Heavy lift

READDIR_BATCH doesn't actually cap memory here.

opendir reads the entire directory into Vec<File> before the first readdir reply, so a large directory can still block the request and consume unbounded memory despite the batching constant. Keeping a tokio::fs::ReadDir in OpenHandle and pulling one batch per readdir would make the limit real.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/sftp.rs` around lines 278 - 312, opendir currently
eagerly reads the whole directory into a Vec<File>, bypassing READDIR_BATCH;
change OpenHandle::Dir to store a tokio::fs::ReadDir (and any queued leading
entries like "." and optional "..") instead of Vec<File>, have opendir create
and mint an OpenHandle::Dir containing the ReadDir and the precomputed "."/".."
entries, and update readdir to consume up to READDIR_BATCH entries by repeatedly
calling ReadDir::next_entry().await to produce File values on demand, draining
queued leading entries first and returning SftpError::Eof only when both the
queue and the ReadDir are exhausted; update mint_handle/dir_mut usage to match
the new OpenHandle::Dir shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/minimald/src/sftp.rs`:
- Around line 120-136: The resolve method currently uses candidate.absolutize()
which only performs lexical normalization and thus allows escaping via in-tree
symlinks; replace that with a real filesystem canonicalization
(std::fs::canonicalize) so symlinks are resolved and the comparison is performed
against the canonicalized workspace path (which you should canonicalize once or
ensure canonicalized before comparing). Update resolve to canonicalize the
candidate path and the workspace (or use a pre-canonicalized self.workspace),
map canonicalize errors to SftpError::Io (same as current mapping), and then
check starts_with against the canonical workspace, returning
SftpError::PathTraversal if it escapes.
- Around line 247-262: The server is allocating buf of size len directly from
the client in async fn read which lets a malicious client request enormous
allocations; introduce a sane maximum (e.g. const MAX_READ_BYTES) and clamp the
requested length before allocation (let cap = std::cmp::min(len as usize,
MAX_READ_BYTES)), allocate buf with cap, perform the read into that buffer,
truncate to the number of bytes read, and return Data { id, data: buf };
preserve the existing EOF handling (return Err(SftpError::Eof) when n == 0).

In `@crates/sessions/src/lib.rs`:
- Around line 22-23: The Record struct currently uses #[serde(default =
"Uuid::nil")] pub id: Uuid which causes missing legacy ids to become Uuid::nil()
and break ID-based lookup; change the deserialization strategy to treat the id
as optional (e.g., #[serde(default)] pub id: Option<Uuid>) and, in the
load/migration path that reads persisted records (the code that
constructs/returns Record instances used by List and GetRecord), detect missing
id and backfill it with a real id (Uuid::new_v4())—store or persist the updated
Record if appropriate so subsequent lookups work; update call sites that read
record.id to handle Option (or normalize to non-Option after backfill) to
restore correct ID-based lookup behavior.

In `@crates/sessions/src/store.rs`:
- Around line 110-114: DiskLoader::new currently assumes the on-disk index
deserializes to the new Index struct (with short_to_uuid and name_to_uuid) which
breaks upgrades from the old shape (a bare BTreeMap<String,Uuid>); change
DiskLoader::new to first attempt deserializing the file as Index, and if that
fails, attempt deserializing the old BTreeMap<String,Uuid> shape, then build a
new Index by setting short_to_uuid from that map and reconstructing name_to_uuid
by scanning each session's record.json (using each session's stored name) in the
sessions directory (or vice versa), inserting entries into name_to_uuid while
handling duplicates consistently with current rules; after reconstruction,
overwrite the on-disk index with the new Index format so future loads succeed
and proceed normally.

---

Nitpick comments:
In `@crates/minimald/src/sftp.rs`:
- Around line 278-312: opendir currently eagerly reads the whole directory into
a Vec<File>, bypassing READDIR_BATCH; change OpenHandle::Dir to store a
tokio::fs::ReadDir (and any queued leading entries like "." and optional "..")
instead of Vec<File>, have opendir create and mint an OpenHandle::Dir containing
the ReadDir and the precomputed "."/".." entries, and update readdir to consume
up to READDIR_BATCH entries by repeatedly calling ReadDir::next_entry().await to
produce File values on demand, draining queued leading entries first and
returning SftpError::Eof only when both the queue and the ReadDir are exhausted;
update mint_handle/dir_mut usage to match the new OpenHandle::Dir shape.

In `@crates/minimald/src/test_harness.rs`:
- Line 151: The test uses a hardcoded subsystem string in
channel.request_subsystem(true, "sftp").await.unwrap(); — replace the literal
"sftp" with the subsystem name constant crate::sftp::SUBSYSTEM_NAME to keep
tests consistent with the production dispatch logic; update the call to use
crate::sftp::SUBSYSTEM_NAME wherever channel.request_subsystem is invoked in
this test (referencing the channel.request_subsystem function and the
crate::sftp::SUBSYSTEM_NAME constant).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a739d92-3c7d-4a58-a6be-822b6bd84dec

📥 Commits

Reviewing files that changed from the base of the PR and between cc168e9 and 9521c53.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • crates/minimald/Cargo.toml
  • crates/minimald/src/connection.rs
  • crates/minimald/src/lib.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/sessions.rs
  • crates/minimald/src/sftp.rs
  • crates/minimald/src/test_harness.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/store.rs

Comment thread crates/minimald/src/sftp.rs
Comment thread crates/minimald/src/sftp.rs
Comment thread crates/sessions/src/lib.rs
Comment thread crates/sessions/src/store.rs
@twitchyliquid64
twitchyliquid64 merged commit 882ff29 into main Jun 1, 2026
8 checks passed
@twitchyliquid64
twitchyliquid64 deleted the tom/sftp branch June 1, 2026 16:14
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.

2 participants