Public Access
Give the phone both pickers, and settle who signs the APK
The files screen could browse a remote and delete on it, and that was all: there is no browsable local filesystem on Android for a second pane to show, so the gesture the desktop is built around — choose on the left, press the arrow — has nothing to stand on. What replaces it is the platform's own two pickers. ADD FILES is ACTION_OPEN_DOCUMENT, so a document is pointed at wherever it lives and goes to the directory showing; SAVE FILE is ACTION_CREATE_DOCUMENT for the selected row. Both stage through the application's cache, and that copy is a requirement rather than a shortcut. android-port.md predicted a picked document would be a third IRemoteFileStore beside SFTP and S3; it cannot be. FileTransferQueue seeks, because an upload resumes from the byte the last attempt reached, and a content:// URI has no path behind it, no length worth trusting, no promised seek and no grant that survives the document being edited underneath it. Copying first costs one class in the head and nothing at all in the shared layers, where the alternative was every resume rule rewritten around a stream that cannot rewind. The copy is deleted when the transfer completes, kept while it is stopped so RESUME still has something to read, and swept at the next launch — which is the one moment emptying that directory is provably safe, since nothing has queued anything yet. Coming out had a decision going in did not: when to ask where it goes. The save picker is raised before the transfer, so the download runs into the same staging directory and hands its bytes to a callback the head supplied, held against the transfer id so a RETRY still lands where the person pointed. Asking afterwards would put the picker minutes from the button that caused it and, on a phone, usually while the application is backgrounded and Android will not show one at all. The cost is that the picker creates its file when it is dismissed, so a download that then fails leaves an empty one there; that is said on the screen, in the README and in the manual checks rather than left to be discovered. A delivery that fails keeps the staged bytes for the sweep instead of throwing away the one copy of something just fetched over somebody's network. The foreground service counts transfers now, which is the half of it that matters most here: a shell survives backgrounding because somebody is looking at it, and an upload has to survive precisely when nobody is. Queued counts as active, so putting five files in and locking the phone moves five files. The seam was built for this and wired to () => 0 because nothing could fill the queue. Alongside it, ADR 0010 answers the second question android-port.md left open, and it had to be answered before the first release rather than at upload time: a new Play app must use App Bundles and therefore Play App Signing, and an installed app can only be updated by a package signed with the same key, so the first release picks an identity for good. The project holds the key, offline and never in CI — the workflow's package step now says so where somebody would break it — and a DodoSSH deployment never serves the client, because a download link on your own server hands the binary that holds the plaintext to the party the whole threat model is about. The README's M1 gap note was stale in both halves and is replaced by what is actually true: credentials have an editor and a REMEMBER tick, and the device key registers into the TPM under a CNG policy that makes the consent dialog a condition of using it. What is left is the floor rather than a gap — no TPM, or no Windows, means the passphrase on every launch.
This commit is contained in:
+44
-13
@@ -28,7 +28,9 @@ verified is that it compiles, links, packages, and carries the right natives.
|
||||
|
||||
**The shape agreed:** a **phone-first** client that is the keychain plus a **terminal**, with sessions and
|
||||
transfers protected by a **foreground service**. File transfer is not in the first scope; when it arrives it
|
||||
is **one remote pane** with Android's document picker for moving files in and out.
|
||||
is **one remote pane** with Android's document picker for moving files in and out. *It has since arrived,
|
||||
both ways:* the pane, the queue, `ACTION_OPEN_DOCUMENT` going in and `ACTION_CREATE_DOCUMENT` coming out,
|
||||
with the foreground service now counting transfers as well as shells.
|
||||
|
||||
**What was actually checked**, so the rest can be read with the right amount of trust:
|
||||
|
||||
@@ -132,7 +134,9 @@ mean the transfer queue's local half (`LocalDirectory`, the drive list, the brea
|
||||
code.
|
||||
|
||||
Note what *does* carry: `FileTransferQueue` itself, and `IRemoteFileStore` — Phase 6 already proved that
|
||||
seam holds two very different remotes, and a `Uri`-backed Android document would be a third.
|
||||
seam holds two very different remotes, and a `Uri`-backed Android document would be a third. (**⚠️ That last
|
||||
clause is wrong, and the build is what corrected it**: a document URI cannot resume, so what shipped stages a
|
||||
copy and hands over a path. See the note under the decision below.)
|
||||
|
||||
**Decided: one remote pane and the document picker**, and out of the first scope. See
|
||||
[Decisions](#file-transfer-when-it-comes-one-pane-and-the-document-picker).
|
||||
@@ -256,6 +260,24 @@ and `IRemoteFileStore` both carry over unchanged. Phase 6 already put a bucket b
|
||||
an SFTP host, so a picker-granted document is a third implementation of a seam that has been exercised twice.
|
||||
What is desktop-only is the *left* pane — `LocalDirectory`, the drive list, the breadcrumb trail.
|
||||
|
||||
> **⚠️ Corrected by the build. Both directions shipped, and neither as a third `IRemoteFileStore`.** A document
|
||||
> URI cannot sit behind that interface honestly: the queue seeks, because an upload resumes from the byte the
|
||||
> last attempt reached, and a `content://` stream promises no seek, no stable length, and no grant that
|
||||
> survives the document being edited underneath it. So `DocumentStaging` copies the chosen document into the
|
||||
> application's cache and hands the queue an ordinary path — which cost one class in the head and *nothing*
|
||||
> in the shared layers, rather than a third implementation of a seam and every resume rule rewritten to cope
|
||||
> with a stream that cannot rewind. The copy is deleted when the transfer completes, kept while it is stopped
|
||||
> so RESUME can read it, and swept at the next launch.
|
||||
>
|
||||
> **Outbound is the mirror image, with one decision the inbound half did not have to take: when to ask.** The
|
||||
> save picker is raised *before* the transfer, so `QueueDeliveredDownload` runs into the same staging
|
||||
> directory and hands the finished bytes to a callback the head supplied. Asking afterwards would put the
|
||||
> picker minutes away from the button that caused it and, on a phone, frequently in the background — where
|
||||
> Android will not show one. The cost is that `ACTION_CREATE_DOCUMENT` creates its file when it is
|
||||
> dismissed, so a download that then fails leaves an empty one; that is said on the screen and in the
|
||||
> README rather than left to be found. A delivery that fails keeps the staged bytes for the sweep instead of
|
||||
> deleting the one copy of something that was just fetched over somebody's network.
|
||||
|
||||
### Sessions survive backgrounding, via a foreground service
|
||||
|
||||
A persistent notification for as long as a shell or a transfer is live.
|
||||
@@ -562,9 +584,11 @@ What is left, in the order it matters:
|
||||
|
||||
- **Running any of it on a device.** Still the one that is not optional, and still true: nothing here has
|
||||
ever been launched on hardware or an emulator. Everything below is reasoning from the code.
|
||||
- **The document picker.** The files screen browses a remote, downloads from it and runs the queue; putting
|
||||
a file *on* a host from the phone needs `ACTION_OPEN_DOCUMENT` behind a third `IRemoteFileStore`-shaped
|
||||
source, which is the decision recorded above and the obvious next piece of work.
|
||||
- ~~**The save picker — the other half of file transfer.**~~ **Built**, along with the half before it: both
|
||||
pickers go through Avalonia's storage provider and both stage through the cache. See the correction under
|
||||
the decision above for the shape and for the one thing about it a person will notice — an empty file where
|
||||
a failed download was pointed. What is *not* built is a folder picker for several downloads at once: the
|
||||
save picker names one destination, so SAVE FILE takes the selected row.
|
||||
- **Editors.** There is no host editor and no keychain item editor on the phone, so both are create-on-
|
||||
desktop-and-sync. That is why the v2 design's `+` buttons on HOSTS and on the keychain are not drawn.
|
||||
- **Pins and import**, which v2 does not draw either. Teams *is* drawn, behind MORE — it was the one of
|
||||
@@ -575,7 +599,9 @@ What is left, in the order it matters:
|
||||
|
||||
## Still open
|
||||
|
||||
Neither of these blocks the spike, and both want answering before there is anything to release.
|
||||
Neither of these blocked the spike, and both wanted answering before there was anything to release. Both
|
||||
now have answers, kept here under the questions that produced them — the heading stays because what is
|
||||
worth reading is which way each went and why, not that the list is empty.
|
||||
|
||||
- ~~**Which Android versions.**~~ **✅ Settled: minSdk 28, targetSdk 36**, and the reasoning divided in two.
|
||||
|
||||
@@ -591,13 +617,18 @@ Neither of these blocks the spike, and both want answering before there is anyth
|
||||
devices that mostly cannot hold a hardware-backed key anyway — which is the one thing the store is for.
|
||||
API 28 and 29 still cost one branch each in `BiometricGate`, because allowed-authenticator lists arrived
|
||||
in 30.
|
||||
- **How it is distributed, and what that does to the supply-chain story.** ADR 0001 says plainly that an
|
||||
operator who wants the secrets attacks the client rather than the crypto, and that release signing with a
|
||||
key **not held by the server** is what that costs. Play App Signing means Google holds the release key.
|
||||
That is not necessarily wrong — it is a different, and in some ways better-audited, trust arrangement —
|
||||
but it is a change to a documented security property of this product, and it should be reasoned about in
|
||||
an ADR rather than discovered at upload time. Sideloading a self-signed APK preserves the current story and
|
||||
costs reach.
|
||||
- ~~**How it is distributed, and what that does to the supply-chain story.**~~ **✅ Settled in
|
||||
[ADR 0010](adr/0010-android-distribution.md): the project holds the release key, the deployment never
|
||||
serves the client, and Play is deferred.** The question was whether Play App Signing — Google generating
|
||||
and holding the release key — is a change to the security property ADR 0001 documents. It is, and the ADR
|
||||
takes it as a one-way door rather than a setting: a new Play app must use App Bundles and therefore Play
|
||||
App Signing, an installed app can only be updated by a package signed with the same key, so the first
|
||||
release picks one identity for good.
|
||||
|
||||
Two things follow for this head *now*, before there is anything to release. CI keeps signing with the
|
||||
debug key and must never gain a keystore secret. And no download link for the APK may ever be served by a
|
||||
DodoSSH deployment — that hands the client binary to the party the threat model is about, which is a
|
||||
worse arrangement than either of the two the question was originally between.
|
||||
|
||||
## Smaller things, decided by default
|
||||
|
||||
|
||||
Reference in New Issue
Block a user