Merge branch 'main' into claude/vault-unlock-logout-autosync-a84c35
ci / build and test (push) Failing after 3s

Four files needed a hand, and all four were two branches adding something in
the same place rather than either changing what the other did.

The shell's constructor now takes both new parameters: main's SFTP session
factory, which it must have because it builds the transfers view model, and
this branch's optional resume handler, which stays last so every existing test
that constructs a shell without one still gets a shell that can only be online
because somebody signed in during this run. App.axaml.cs, ShellFlowTests and
QuickConnectTests pass the pair; the layout suite keeps both of its new fields.

Signing out now detaches the transfers screen exactly as locking does, and the
confirmation says that an open transfer session survives it. That is the same
policy both sides already argue for their own case: signing out destroys this
machine's copy of the vault, not work that authenticated before it.

QuickConnectTests did not compile on main — the SFTP commit added a constructor
parameter and the quick-connect suite, merged from a parallel branch just
before it, was still calling the old one. Fixed here rather than worked around,
since the merged tree has to build.

dotnet build, dotnet test and dotnet format --verify-no-changes are all clean:
980 tests, including the end-to-end suite against real containers.
This commit is contained in:
2026-07-31 11:16:49 +02:00
41 changed files with 5464 additions and 141 deletions
+38 -2
View File
@@ -65,8 +65,9 @@ src/
DodoSSH.Client.Storage the local cache: ciphertext mirror, outbox, offline unlock material
DodoSSH.Client.Sync the pull/apply/push loop and the conflict policy
DodoSSH.Client.Session where a profile lives, unlocking it, and getting one in the first place
DodoSSH.Client.Ssh connections, PTY shells, host key trust
DodoSSH.Client.Ssh connections, PTY shells, SFTP, host key trust
DodoSSH.Client.Terminal the loopback data plane and credit-based flow control
DodoSSH.Client.Transfer the transfer queue, part files and resume, and the local file listing
DodoSSH.Client.App Avalonia; the only project that knows about a UI toolkit
tests/ one test project per source project
docs/adr/ architecture decision records
@@ -165,6 +166,37 @@ authentication asks for the password every time, because nothing in the interfac
credential yet (they do sync — there is just no editor for one); and unlock asks for the passphrase on every
launch, because no device key is registered.
### Moving files
**FILES** in the nav rail is a two-pane browser: this machine on the left, the host on the right, and a
queue underneath. Choose a host, press **CONNECT**, then select a file in either pane and press the arrow
pointing the way you want it to go.
Two things about it are worth expecting rather than discovering.
**It is a second connection, not a second channel.** SSH itself would allow the SFTP subsystem to open
beside a shell on the transport that is already up; SSH.NET does not offer that — its `SftpClient` owns its
own transport — so pressing CONNECT here authenticates again. The host records a second login, and a host
whose password you type each time will ask for it again on this screen. Host key trust is shared: a
fingerprint approved for a terminal is approved here, and one approved here reaches your other machines with
the next sync.
**Nothing is written at its final name until it is complete.** Every transfer goes to a `.dodossh-part` file
beside its destination and is renamed into place at the end, so an interrupted transfer can never be
mistaken for a finished one — which matters most for what people actually use this for, which is copying a
build artefact onto a server and then running it. A destination that already exists is refused outright
rather than overwritten; the remote pane has **DELETE** and **MKDIR** so that refusal is not a dead end.
**RESUME** on a stopped transfer carries on from what the part file already holds.
Resume works within a run of the application and not across a restart, and that limit is deliberate: nothing
records which source wrote a part file, and resuming one on the strength of its name matching is how a
corrupt artefact gets delivered with nothing reporting a failure. A part file found at startup is started
over.
What is not here: transferring a directory, dragging between the panes, and routing a transfer through a
bastion — the last needs jump hosts the connection layer has not got. All three are in
[`docs/design-import-gaps.md`](docs/design-import-gaps.md).
### End-to-end verification
One suite runs against a real server rather than a stub. It needs a Docker daemon and nothing else, so it
@@ -245,7 +277,11 @@ off-Windows.
a key are written at version 2 and become read-only on an older build. Hosts that do not are still
written at version 1, byte-identically to before the field existed — which is what keeps upgrading one
machine from making a team's whole vault uneditable everywhere else.
- **M2 — full personal vault**, robust sync, relay.
- **M2 — full personal vault**, robust sync, relay. *File transfer done:* an SFTP session, a two-pane file
browser with a real remote listing — names, sizes, modification times and `drwxr-xr-x` permission bits —
and a queue that moves one file at a time with progress, throughput and resume. See
[Moving files](#moving-files) for the two things about it worth knowing before you use it, both of which
are consequences rather than choices.
- **M3 — teams**, sharing, ACLs.
- **M4 — hardening and ops**, packaging, self-hosting guide.
- **M5 — multi-provider OIDC**, key rotation, per-item content keys.