Skip to content

feat(api): add programmatic file copy (VM.CopyTo/CopyFrom) over SSH#14

Merged
pilat merged 1 commit into
mainfrom
feat/programmatic-copy
Jun 14, 2026
Merged

feat(api): add programmatic file copy (VM.CopyTo/CopyFrom) over SSH#14
pilat merged 1 commit into
mainfrom
feat/programmatic-copy

Conversation

@pilat

@pilat pilat commented Jun 14, 2026

Copy link
Copy Markdown
Owner

fleetbox is library-first — every capability is supposed to live in the Go API, with the CLI just wrapping it. File copy broke that rule: it only existed as fleetbox cp shelling out to the system scp binary, so a library caller had no way to move a file in or out of a running VM, and the project's own dogfood test had to shell out to scp and hand-rebuild the key path just to get its test binary into the guest. WithFixture only ever covered read-only host→guest at boot — there was no copy-out, and no copy-in to a running VM.

This adds VM.CopyTo and VM.CopyFrom to the public API — universal: a file or a whole directory, in either direction. They stream tar over the SSH connection fleetbox already holds, so there are no new module dependencies and the only thing the guest needs is tar, which every image in the catalog ships. File modes are preserved (an executable stays executable); ownership is not. The CLI cp and the dogfood now ride the same primitive, so file copy no longer shells out to scp at all — the CLI keeps its scp-like "copy into a directory" convenience, but only as a thin layer above the exact-path library method.

It's purely client-side — the control protocol, the helper, and the backends are untouched — so it behaves identically on macOS and Linux. The exact-destination semantics, the rejected alternatives (sftp, keeping the scp shell-out), and the deliberately deferred bits (reader/writer variants, honoring ctx mid-transfer, tolerating GNU tar's "file changed as we read it") are written up in ADR-0026.

Checklist

  • Changed the public API, package list, CLI surface, on-disk layout, or dependencies → ARCHITECTURE.md updated in this PR
  • Made a new, hard-to-reverse design decision → added an ADR under docs/adr/ (next sequential number)
  • Breaking change (! in the title) → the description spells out what callers must change

Summary by CodeRabbit

Release Notes

  • New Features

    • Added file transfer capabilities to the programmatic VM API for copying files to and from virtual machines
    • Enhanced the cp command with an improved file transfer mechanism
    • File permissions and modes are now preserved during all copy operations
  • Documentation

    • Updated API and architecture documentation to reflect new file transfer capabilities and behavior

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pilat, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 45 minutes and 59 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 14d45c4a-01ab-401d-861f-1fe16e64a81c

📥 Commits

Reviewing files that changed from the base of the PR and between d0955f5 and b4b1a13.

📒 Files selected for processing (13)
  • ARCHITECTURE.md
  • README.md
  • cluster_cap_test.go
  • cmd/fleetbox/cp.go
  • cmd/fleetbox/cp_test.go
  • cmd/fleetbox/ssh.go
  • docs/adr/0026-programmatic-copy-tar-over-ssh.md
  • fleetbox.go
  • fleetboxtest/conformance_vm_test.go
  • fleetboxtest/nested_test.go
  • internal/orchestrator/orchestrator.go
  • internal/sshkey/copy.go
  • internal/sshkey/copy_test.go
📝 Walkthrough

Walkthrough

Introduces VM.CopyTo and VM.CopyFrom to the public API, implemented as an in-process tar archive streamed over an SSH session in internal/sshkey. The CLI cp command is reworked to use this primitive directly instead of shelling out to scp. ADR-0026 and documentation are updated accordingly.

Changes

Programmatic file copy via tar-over-SSH

Layer / File(s) Summary
ADR-0026 design decision
docs/adr/0026-programmatic-copy-tar-over-ssh.md
New ADR documents the decision, pinned semantics, rejected alternatives (sftp, scp shell-out), and consequences including zero new module dependencies and deferred items such as mid-transfer ctx cancellation.
Core sshkey copy implementation
internal/sshkey/copy.go
Adds Client.CopyTo/CopyFrom entry points, io.Pipe-based copy orchestration without full buffering, guest shell command string generation with shellQuote, buildTar/writeTarEntry for host-side archive creation, extractTar with safeJoin/checkSymlink path confinement and deferred directory mode restoration, and toGuest/fromGuest SSH execution helpers capturing stderr.
Core sshkey unit and security tests
internal/sshkey/copy_test.go
Adds fakeGuest/recordingGuest transport doubles; table-driven tests for command generation, file/directory/symlink round-trips, mode preservation, large-file streaming, input validation, guest error propagation, path traversal rejection, symlink escape rejection, and read-only directory mode restoration.
Public API: vmState interface, VM methods, orchestrator
fleetbox.go, internal/orchestrator/orchestrator.go
vmState interface gains CopyTo/CopyFrom; VM exposes exported methods with documented semantics; orchestrator implements both by dialing SSH with a 30s timeout and delegating to the sshkey client.
CLI cp command migration
cmd/fleetbox/cp.go, cmd/fleetbox/cp_test.go, cmd/fleetbox/ssh.go
runCp reworked to parse VM IP, dial via sshkey.Client, and dispatch CopyFrom/CopyTo; adds remotePath, resolveLocalDest, and isExistingDir helpers for scp-style directory destination behavior; TestResolveLocalDest table-driven test added.
Integration: conformance, nested test, stubs, and docs
fleetboxtest/conformance_vm_test.go, fleetboxtest/nested_test.go, cluster_cap_test.go, ARCHITECTURE.md, README.md
TestVMConformance adds end-to-end CopyTo/CopyFrom assertions over a real SSH guest; TestNestedLinuxBoot migrates binary delivery from scp to outer.CopyTo; fakeVMState stub gains no-op CopyTo/CopyFrom; architecture and README updated to describe the new API, implementation invariants, and known limitations.

Sequence Diagram(s)

sequenceDiagram
  participant caller as Caller (VM / CLI cp)
  participant orch as orchestrator.VM
  participant sshkey as sshkey.Client
  participant pipe as io.Pipe
  participant guest as Guest (tar -x / tar -c)

  rect rgba(34, 139, 34, 0.5)
    Note over caller,guest: CopyTo (host → guest)
    caller->>orch: CopyTo(ctx, hostPath, guestPath)
    orch->>sshkey: Dial SSH (30s timeout)
    sshkey->>pipe: open pipe writer/reader
    sshkey->>sshkey: buildTar(hostPath) → pipe writer (goroutine)
    sshkey->>guest: toGuest: pipe reader → tar -x stdin
    guest-->>sshkey: exit code + stderr
    sshkey-->>orch: error or nil
    orch-->>caller: error or nil
  end

  rect rgba(70, 130, 180, 0.5)
    Note over caller,guest: CopyFrom (guest → host)
    caller->>orch: CopyFrom(ctx, guestPath, hostPath)
    orch->>sshkey: Dial SSH (30s timeout)
    sshkey->>guest: fromGuest: run tar -c (goroutine)
    guest-->>pipe: stdout tar stream
    sshkey->>sshkey: extractTar(pipe reader, hostPath)
    sshkey-->>orch: error or nil
    orch-->>caller: error or nil
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pilat/fleetbox#8: Previously implemented the cp command using scp shell-out with name:/path remote syntax — the same cmd/fleetbox/cp.go entry point this PR rewrites to use internal/sshkey tar-over-SSH.

Poem

🐇 Hop hop, no more scp shell games,
I stream my tar through SSH frames!
CopyTo, CopyFrom, both safe and neat,
path traversal attempts? Hard to beat!
io.Pipe keeps memory light,
This bunny copies archives right! 🗃️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding programmatic file copy methods (VM.CopyTo/CopyFrom) to the public API over SSH.
Description check ✅ Passed The description thoroughly explains the problem solved, the implementation approach, and design rationale. It explicitly addresses all three checklist items: API changes documented in ARCHITECTURE.md, ADR-0026 created, and confirms no breaking changes despite the exclamation mark criteria being present.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/programmatic-copy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@pilat pilat self-assigned this Jun 14, 2026

@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: 2

🤖 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 `@cmd/fleetbox/cp.go`:
- Around line 121-123: The code does not guard against the case where guestPath
is `/` (the remote root), which causes path.Base(guestPath) to return `/` and
subsequently resolves localDst to an unsafe target of `/`. Add a guard check
after the guestPath assignment (in the line where guestPath := remotePath(src)
is called) to detect and handle the case where guestPath equals `/` before
proceeding to the resolveLocalDest call. This prevents the unsafe and surprising
behavior of attempting to copy to the root directory.

In `@internal/sshkey/copy.go`:
- Around line 334-351: The writeRegular function and the related directory
creation function lack symlink safety checks, allowing malicious tars to escape
the destination root by following existing symlinks in the path. Before creating
directories with os.MkdirAll and before creating files with os.OpenFile in
writeRegular, add verification that path components are not symlinks by using
os.Lstat to check each path segment and rejecting any symlinks encountered.
Apply the same symlink detection logic to the directory creation function that
handles directory entries (the function referenced in the "Also applies to"
note), ensuring that no symlink traversal can occur when materializing any entry
type (regular files, directories, or symlinks).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 82f22cbc-3bf7-4554-b80a-7adf5d0467d9

📥 Commits

Reviewing files that changed from the base of the PR and between 6345840 and d0955f5.

📒 Files selected for processing (13)
  • ARCHITECTURE.md
  • README.md
  • cluster_cap_test.go
  • cmd/fleetbox/cp.go
  • cmd/fleetbox/cp_test.go
  • cmd/fleetbox/ssh.go
  • docs/adr/0026-programmatic-copy-tar-over-ssh.md
  • fleetbox.go
  • fleetboxtest/conformance_vm_test.go
  • fleetboxtest/nested_test.go
  • internal/orchestrator/orchestrator.go
  • internal/sshkey/copy.go
  • internal/sshkey/copy_test.go

Comment thread cmd/fleetbox/cp.go
Comment thread internal/sshkey/copy.go
@pilat pilat force-pushed the feat/programmatic-copy branch from d0955f5 to b4b1a13 Compare June 14, 2026 01:53
@pilat pilat merged commit e24ec47 into main Jun 14, 2026
3 checks passed
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.

1 participant