A reader with only the repository walked the handoff and the remaining plan tasks against the code. Mark the plan and spec lines that later rulings superseded, add corrections for Tasks 18 to 21, fix stale TODO and PERMISSIONS entries, ignore the skill workspace, point AGENTS.md at the handoff and take the test account usernames out of it. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
44 KiB
status, date
| status | date |
|---|---|
| approved | 2026-11-11 |
001 - Proton Drive: a maintainable, SDK-conformant backend
Fork-only document. Approved by the fork owner on 2026-11-11 with the decisions recorded in section 14. Decisions marked (call) were made by the author of this spec and accepted with it; none has been agreed with rclone's maintainers.
Scope decided by the owner: get a working backend in this fork first. Nothing is posted upstream for now, the owner is not offering permanent maintainership, and the fake server stays in the fork. The design still follows rclone's conventions so that upstreaming remains possible later.
Implementation status (2026-11-11): paused after plan Task 17 of 21.
M1 to M3 are built; M4's swap is done (the backend runs on the native
client and passes the contract suite against the fake) but its remaining
parts - wider suites, removal of the old libraries, user documentation and
live verification - are not. M5 to M7 are not started. State, rulings and
how to resume: docs/plans/001-protondrive-tier1-HANDOFF.md.
1. Intent
Rclone's Proton Drive backend works today, but on borrowed time and on one person's goodwill:
- It is Tier 4, "Community" maintained. Nick Craig-Wood (ncw) is the only
committer to the two libraries it depends on,
rclone/Proton-API-Bridgeandrclone/go-proton-api. Those forks have 15 open, unreviewed pull requests between them, including the fix for a permanent upload deadlock (rclone #10224). - Proton has put dates on the current approach. The
/drive/shares/{shareId}/links|folders|files|eventsroutes the bridge uses are to be removed within 12 months of 2026-09-16. Uploads only succeed because Proton exempts rclone from the mandatory block verification step, and that exemption ends once third parties have had a chance to adopt the SDK. A breaking cryptography change is targeted for end 2026 / early 2027. - Proton's stated rule for third parties is: adopt the official SDK, or "fully emulate its safety, correctness, and availability features". Proton will not ship or accept a Go SDK (ProtonDriveApps/sdk#19 was closed in two days), and names rclone users as its top API consumers "by far".
- Rclone will not take cgo (ncw, #9252). The official SDK exists only in C# and TypeScript.
The intent is to replace the backend's internals with a native Go implementation that follows the official SDK's behaviour closely enough that Proton has no reason to throttle or block it, lives entirely inside the rclone tree so rclone's maintainers can maintain it like any other backend, and comes with enough hermetic testing that a change can be trusted without a Proton account.
2. Approach, and what was rejected
Chosen: a native Go port inside backend/protondrive, using the official
TypeScript SDK as the behavioural reference, built on lib/rest,
lib/pacer, lib/dircache, fs/fshttp, and Proton's own MIT Go crypto
libraries (ProtonMail/gopenpgp/v3, ProtonMail/go-srp). The two bridge
libraries are removed from go.mod.
Why this one:
- It is the only option that satisfies both hard constraints at once: no cgo (rclone) and SDK-equivalent behaviour (Proton).
- It fits what CONTRIBUTING.md asks of a backend:
lib/rest, thefshttptransport, the layout ofbackend/box, API types inapi/types.go. - It removes two external repositories that one maintainer carries alone. ncw has already asked Proton, unanswered, "do you think we should start from scratch and port the c# SDK to Go?" (forum thread 53609, post 12).
- Proton staff confirmed gopenpgp v3 supports the current format and is used by official clients. The SDK is MIT, so porting from it is licence-clean.
| Rejected | Why |
|---|---|
| C# SDK through its C interface | Needs cgo. Proton calls the C interface "implementation detail". No rename, copy or revisions. NativeAOT does not cover rclone's mips, BSD, plan9, solaris or aix targets. |
| TypeScript SDK in an embedded JS or WASM runtime (wazero) | ncw's only stated opening, but never done in rclone or anywhere else. The SDK's crypto dependency @protontech/crypto is GPL-3.0, which cannot ship in MIT rclone. Needs a large Web API polyfill surface. |
| Sidecar process running the official SDK | Uses the real SDK, but rclone has no precedent for shipping, downloading or auto-spawning a helper binary; it breaks librclone and mobile; ncw has not answered the proposal in #9252. Stays a fallback if Proton ever blocks non-SDK clients outright. |
| Keep patching the bridge forks | Lowest friction per patch, and what happens today. But the bridge is built on the routes being removed, has no event or cache model, and its review queue is the bottleneck. It prolongs the situation this spec exists to end. |
| A separate community Go SDK module that rclone imports | Reusable by others, but recreates the external-repo maintenance problem and the replace-directive friction ncw rejected in #9260. The port is instead kept free of fs types below a clear seam (section 5) so it can be extracted later. (call) |
Build on clayliddell/proton-drive-go-sdk |
Thin layer over go-proton-api with gopenpgp v2, a deprecated move route, no events, cache or backoff, and test defaults that spoof a first-party app version. Useful to read, not to depend on. |
3. Goals
- Same backend, same config. Backend name
protondrive, every existing config key, existing saved sessions and the default filename encoding keep working with no user action. - SDK conformance. Only the routes the reference SDK itself calls (mostly
drive/v2/volumes/..., plus the few unversioned ones it still uses such asdrive/blocksanddrive/volumes/{id}/trash), and none of the deprecated share-scoped link, folder, file or event routes; block verification tokens; the SDK's draft, revision, manifest and extended-attribute formats; its retry, backoff and parallelism limits; an honestexternal-drive-rclone@...app version. - Proton-friendly traffic. Metadata caching and event-driven invalidation, so a repeat run does not re-walk the tree.
- Meet every code-level Tier 1 attribute in
docs/content/tiers.md: feature score of at least 5/7, all integration tests green with nothing ignored, pacer-based error handling, hash and modtime integrity, full docs. - Fix the recurring failures listed in section 12.
- Hermetic tests. The full backend contract suite runs against an in-process fake Proton API on every commit, with no credentials.
- Fewer dependencies and no larger binary than today.
- Upstreamable. Reviewable commits, rclone conventions, no fork-only material in the upstream branch.
4. Non-goals and anti-goals
Non-goals for this spec (the design must not preclude them):
- Shared with me (#7918), Photos (#7832), Devices/Computers, public links, sharing management, thumbnails, revision history commands.
OpenChunkWriter/ multipart upload (F7). Blocks are independent, but the revision needs a SHA1 of the whole plaintext, which cannot be computed from out-of-order chunks. (call)ListR. Proton asks clients not to do recursive traversals.- The post-2026 "mostly symmetric" crypto model. It is unpublished. Section 5 isolates crypto so it can be added without touching the rclone-facing code.
- Exporting native Proton Docs and Sheets.
Anti-goals - any of these makes the work a failure even if tests pass:
- Requiring cgo, a helper binary, or a
replacedirective. - Identifying as a first-party client or as the official SDK (no spoofed
x-pm-appversion, nox-pm-drive-sdk-versionheader - Proton asked for the latter explicitly in #9260). - Writing data the official web, desktop or mobile clients cannot read (the v1.75.0 incident, #9844).
- Decrypted names, keys or passwords written to disk or to logs.
- Backend-specific code in
fs/orvfs/. - Silently changing how existing remote files map to rclone paths.
5. Architecture
One Go package, backend/protondrive, laid out like other multi-file
backends (internxt, pikpak). (call) The only sub-packages are api
(wire types) and protontest (the fake server of section 9.2, imported only
by tests); the seam is by file and by a rule.
| File | Responsibility | May import fs? |
|---|---|---|
protondrive.go |
Fs, Object, options, features, dircache glue, error mapping to fs sentinels |
yes |
api/types.go |
Wire request/response structs and API error type | no |
session.go |
Login (browser fork, SRP), token refresh, credential persistence, app version, server-time tracking | yes (config only) |
account.go |
User, addresses, key salts, unlocking user and address keys, public-key lookup cache | no |
crypto.go |
Every OpenPGP operation: node keys, passphrases, names, name hash, content keys, blocks, manifest, extended attributes | no |
nodes.go |
Volume/share bootstrap, node fetch and decrypt, children listing, create folder, rename, move, copy, trash, delete | no |
upload.go |
Draft, verification, block pipeline, commit, cleanup | no |
download.go |
Block listing, verification, decryption, ranged reads | no |
events.go |
Event cursor, polling, cache invalidation, change notification | no |
cache.go |
In-memory and persistent metadata cache | no |
The seam rule: protondrive.go contains no cryptography and no URL
paths; the other files do not return or accept fs.Object/fs.Fs. This is
what lets the new crypto model land in crypto.go plus wire types, and what
keeps a later extraction into a standalone module possible.
No package-level mutable state: sessions, keys, caches and the events poller
all hang off Fs. (The current _mapper and _saltedKeyPass globals
cross-contaminate two Proton remotes in one process.) The single exception is
one package-level mutex that serialises key unlocking across Fs instances,
which is the fix ncw suggested for #9816. There is no process-wide session
or key cache; rclone's own fs/cache already shares an Fs between users of
the same remote and root. (call)
Reference pin. A constant-free note in the package doc comment records
the SDK tag the port was last checked against (starting at js/v0.23.0).
docs/specs/ gets a short conformance checklist per SDK module; re-checking
it against new SDK tags is a recurring maintainer task.
Data flow
rclone core -> Fs/Object (protondrive.go)
| dircache: path -> node ID
v
nodes / upload / download <-> cache <- events poller
| |
crypto.go lib/rest + pacer (API host) lib/rest (storage host)
| |
gopenpgp v3 fshttp transport -> drive-api.proton.me
6. Behaviour
6.1 Authentication and sessions
- Browser sign-in is the default for new remotes. (As built: only when
usernameandpasswordare not both set, so scripted setup does not wait on a browser; andrclone configon a remote with a saved session asks before signing in again.)rclone configruns the session-fork flow used by Proton's official CLI: request a fork, show the user a URL to open on any device, poll, decrypt the returned key password. Client IDexternal-drive. Password, 2FA and CAPTCHA are handled by Proton's page, and rclone never sees or stores the password. Implemented as aConfigstate machine likebackend/internxt. Upstream PR #9522 proposes the same flow on the old stack; coordinate with its author. - Password sign-in stays for existing configs and headless automation:
username,password,mailbox_password,2fa,otp_secret_key, via SRP. A CAPTCHA response (code 9001) produces an error that tells the user to runrclone config reconnectand use browser sign-in. - Saved sessions keep working. The existing
client_uid,client_access_token,client_refresh_tokenandclient_salted_key_passkeys are read and written with their current meaning, so an upgraded rclone does not force a re-login and a downgrade still works. - Refresh is safe across
Fsinstances and processes. Refresh tokens are single-use. Before refreshing, re-read the config and adopt a newer token if another instance already refreshed, followinglib/oauthutil'sreReadTokenpattern that ncw asked for in #9881. Refresh is single-flight perFs. A failed refresh never wipes credentials that another process may have just rotated. - Key unlock is serialised. Unlocking user and address keys is expensive
(#9816: concurrent
rcdrequests were OOM-killed). It happens once perFs, the result is held for the life of thatFs, and unlocks in differentFsinstances run one at a time, so peak memory is one unlock. - Server time. The
Dateheader of API responses sets the clock used for key generation and signature checks, as the official CLI does. This is the fix for code 200501 on machines with a fast clock. - Third-party notice. Proton requires apps that prompt for credentials to say they are not official. The config prompt and the docs page carry: "rclone is a third-party application not officially supported by Proton."
- App version.
external-drive-rclone@<semver>-<channel>. Proton's published grammar allows only the channelsstable,betaandalpha; today's code sends-devfor development builds. Development builds map toalpha. (call) - to be confirmed with Proton in step 13.5. Theapp_versionoption stays for compatibility but its help text says misuse can get the client blocked. - Test endpoint. A hidden advanced option
endpointoverrides the API host so tests can reach the fake server. It is not documented for users: Proton's rules forbid pointing a client at other domains. (call)
6.2 Listing, cache and events
- Listing a folder fetches child IDs, then node metadata in batches of up to
100, and decrypts names with the parent's key.
ListPis implemented so large folders stream. - In-memory cache of decrypted nodes and node keys per
Fs, replacing the bridge cache.enable_cachingkeeps its meaning. - Events. While an
Fsis alive, volume events are polled no more often than every 30 seconds (Proton's stated floor), and only when the cache is non-empty orChangeNotifyis active. Events invalidate cache and dircache entries, and feedChangeNotifysomountandservesee remote changes. This removes today's "cache is never invalidated" caveat. - Persistent metadata cache (later milestone, opt-in first). Stores the
server's encrypted node records and the event cursor on disk through
lib/kv. Nothing decrypted is ever persisted. On start, the backend replays events since the cursor instead of re-listing. Atree_refreshevent, an expired cursor (about a month) or any decode error drops the cache.lib/kvis unavailable on plan9 and js; there the option is a no-op. (call) This is the piece that answers Proton's main complaint and the "17 minutes for a no-op copy" reports. - Native Proton Docs/Sheets nodes have no content blocks. They are left out
of listings, with one
NOTICEper run. (call) (Today they segfault or fail every sync: #7959.) - A node whose name cannot be decrypted is skipped with an
ERRORlog and counted, never fatal for the whole listing.
6.3 Upload
Follows the SDK's upload module step for step:
- Create a draft in the parent with the encrypted name, name hash, node
key, passphrase, content key packet and a stable per-remote draft client
ID (new hidden config key
draft_client_uid; the session UID changes on re-login and so cannot be used). (call) - Name conflict (code 2500): if it is a leftover draft from this client ID,
delete it and retry once. If it is another client's draft, fail unless
replace_existing_draftis set. If it is a real file,Updatecreates a new revision of that node. This ends the orphan-draft loop (#9622, #9132). - Fetch the verification code; for every 4 MiB block: encrypt, sign, SHA-256, check the block decrypts, compute the verifier token.
- Request upload URLs in batches; upload blocks to the storage host with at
most
upload_concurrencyin flight per file (new advanced option, default 4, max 5 as in the SDK), and at most 10 blocks in flight across the wholeFs(the SDK's client-wide budget of about 40 MiB). With a high--transfers, extra transfers wait their turn; they do not fail. (call) Blocks are buffered fromlib/poolso a failed block is re-sent from memory. A storage 404 means the URL expired: request a new one. Every wait is bounded by the context; no path can block forever (#10224). - Before commit, check block count and byte total against what was read, and the SHA1 against the source's if it has one.
- Sign the manifest and commit with extended attributes: modification time, size, block sizes, SHA1. If the commit call errors, check whether the revision is in fact active before reporting failure.
- On any failure or cancellation, delete the draft.
The SDK's single-request path for files under 128 KiB (files/small,
gated by a server feature flag) is not part of the first milestones; it is
added in M6 as a request-count optimisation. (call)
Update on an existing object creates a new revision of the same node, so
the node ID and history survive. PutStream is supported: unknown size
simply omits the intended size. The output format is exactly what the
reference SDK tag writes (v4 node key, v6 PKESK content key, SEIPDv2
AES-GCM blocks; names, passphrases, hash keys and attributes without AEAD).
6.4 Download
- Block metadata is paged. Each encrypted block's SHA-256 is checked against the server's value before decryption; a mismatch is a retryable error.
- Ranged reads map an offset to a block using the recorded block sizes, so
NoMultiThreadingis dropped and multi-thread downloads work. At most 10 blocks are in flight for download across theFs(SDK value). - A full, sequential read lists every block and verifies the manifest
signature over the listed hashes before releasing the first byte; each
block is then checked against its listed hash as it arrives, and a block
listed again (for a new URL) with another hash is an integrity error.
Failure is an error. An advanced option
ignore_signature_errorsdowngrades content and authorship signature failures to a loggedERRORfor users with legacy or shared data (#8003, #7344). (call) A range covering the whole file from offset 0 (asbytes=0-) is checked as a full read, still returning only the bytes asked for. Other ranged reads check each block against its listed hash but cannot verify the manifest; this is documented, as it is in the SDK. - A file may list at most 2^19 blocks (2 TiB in 4 MiB blocks, more than any one file a Proton plan can hold), whatever its unsigned size fields claim, which bounds how many blocks' metadata a full read holds before the manifest check (about 250-350 MiB at the cap with realistic tokens and URLs; their length, and API response bodies, aren't bounded yet).
- A server serving an earlier revision whole (blocks, manifest and extended attributes, all validly signed) under the current revision ID is undetectable: nothing signed binds them to the revision. The SDK cannot detect it either; it is a protocol limit to raise with Proton.
- Old-format files (pre-AEAD) remain readable.
Closereleases everything; no reader outlives itsObject.Opencaller.
6.5 Other operations
| Operation | Behaviour |
|---|---|
| Mkdir | Create folder; modification time set at creation |
| Rmdir | Trash an empty folder; fs.ErrorDirNotFound when missing. Emptiness is asked of the server, uncached, counting every child including ones rclone does not list |
| Remove, Purge | Move to trash, as today (Proton has no direct permanent delete) |
| CleanUp | Empty trash |
| Move, DirMove | Server-side move and rename with re-encryption to the new parent |
| Copy | Server-side copy (new; SDK copyNodes) |
| About | Used and total space |
| SetModTime | fs.ErrorCantSetModTime - the time lives in the revision |
| Hashes | SHA1 (claimed digest from extended attributes) |
| Precision | 1 second, unchanged (call) |
| Metadata (later milestone) | Read mtime, btime, content-type; write mtime and content-type at upload |
| Shutdown | Stop the event poller, flush the persistent cache. Never clears keys: the Fs stays usable, because rclone's fs cache shuts down entries that rc jobs may still hold |
| Disconnect | Log out and clear saved session keys; later use fails with a "not signed in" error naming the remote |
Feature score target: Hash, Modtime, Stream upload, Copy/Move, DirMove = 5/7 at the first upstream milestone; Metadata makes 6/7.
6.6 Errors, retries, pacing
- Separate pacers for the API host and the storage host, so block uploads
under
--bwlimitdo not starve metadata calls (#9832). - 429: honour
Retry-Afterthroughpacer.RetryAfterError. - 5xx and network errors: bounded retries with backoff, mirroring the SDK's limits. 4xx is not retried except 408, 429 and the storage-URL 404.
- Node-metadata batch fetches run at most 15 at a time (SDK value).
- API codes map to rclone errors in one table: 2501 to
fs.ErrorObjectNotFound/fs.ErrorDirNotFound; quota and limit codes (200001-200004, 200300, 200301) to no-retry errors with Proton's message; 2000/2028 ("app version not supported", unmet requirements) to a fatal error telling the user to upgrade rclone. - The
x-pm-drive-requirementsresponse headers are logged at debug level so a pending server-side enforcement is visible in bug reports.
6.7 Options
Kept unchanged: username, password, mailbox_password, 2fa,
otp_secret_key, the four client_* keys, encoding,
original_file_size, app_version, replace_existing_draft,
enable_caching.
Added (all advanced): upload_concurrency, ignore_signature_errors,
metadata_cache (persistent cache, default off until proven),
draft_client_uid (hidden), endpoint (hidden, tests only).
Filename rules: non-empty, at most 255 characters, no /. The encoding
default does not change. If rclone test info against the new code shows
the default is wrong, that is raised as a separate, explicitly
compatibility-reviewed change.
7. Security
- Threat model includes the server. End-to-end encryption means a
compromised or malicious server must not be able to feed rclone altered
content undetected: encrypted-block hashes, manifest signatures, name and
passphrase signatures are verified as the SDK verifies them. Server-supplied
names containing
/,..or NUL are rejected, never joined into paths. Trust is rooted in the user's own addresses, whose keys the account holds: the My Files share must be created, and its root folder signed, by one of them, else everything is treated as unverified (readable and writable only withignore_signature_errors). A key that isn't verified makes everything beneath it unverified, and rclone never moves, copies, renames or writes into such nodes without the option. Limit: a file or folder signed under another user's email is checked against public keys the server itself supplies, so a hostile server can forge those; Proton's SDK shares this limit, and key transparency or address-key pinning is the eventual answer. - Least privilege, as far as Proton allows. Proton offers no scoped tokens (sdk#7), so a session is a full account session. Browser sign-in means the password is never held; that is the recommended setup in the docs. The key password and tokens stay in the rclone config, which the docs tell the user to encrypt.
- No secrets in logs. The backend's own log lines never contain
passwords, key passwords, tokens, session keys, decrypted passphrases or
upload tokens, at any
-vlevel. This is tested (section 9.6). rclone's--dumpflags are a different matter:fshttpredacts only theAuthorizationheader, Proton carries tokens in JSON bodies and in thepm-storage-tokenheader, and changingfshttpis out of scope. The docs page says plainly that--dumpoutput for this backend contains secrets. - Nothing decrypted on disk. The persistent cache holds ciphertext only.
- No telemetry is sent to Proton or anyone else.
- TLS is rclone's standard
fshttptransport, without certificate pinning, like every other backend. (call) - Supply chain. Two forked modules and their transitive dependencies
(resty, gluon, go-message and others) leave
go.mod.gopenpgp/v3andgo-srpbecome direct dependencies; both are Proton-maintained and already in the build.
8. Constraints
- Pure Go; builds on every target of
bin/cross-compile.go. No new_unsupported.gostubs. - Go 1.26 minimum, as
go.mod. - All HTTP through
fs/fshttp, so--dump,--tpslimit, proxies and the test fault injector apply. - Memory: at most 10 upload blocks of 4 MiB (+ ciphertext overhead) buffered
per
Fs, taken fromlib/pooland always returned. - rclone code style, comment style and commit message rules from
AGENTS.md. Docs option sections are generated, not hand-edited. - CONTRIBUTING's AI policy: the human submitter must understand every line. Work is therefore delivered in small commits a person can actually review.
9. Testing
Testing is the half of this project that makes the other half maintainable. There are seven layers; 9.1 to 9.4 run on every commit with no network.
9.1 Unit tests with fixed vectors
Table-driven tests for every function in crypto.go, session.go and the
error mapping:
- name hash (HMAC-SHA256, including names with non-ASCII, combining characters and the #9622 failing shapes);
- key password derivation (bcrypt output slice);
- fork payload decryption (AES-256-GCM, AAD
fork); - verifier token (XOR over code length, zero padding, block shorter than code);
- manifest construction (thumbnail-before-block ordering);
- extended attribute encode/decode, including unknown extra fields preserved;
- node key, passphrase, content key and block round trips, and reading pre-AEAD fixtures;
- app version formatting; API code to error mapping; retry decision table.
Vectors come from two sources so the port cannot merely agree with itself.
The SDK's mirrored unit tests (88 files under client/js) supply cases for
the non-cryptographic logic: extended attributes, verifier, manifest
ordering, draft conflicts, error mapping, retry. They mock OpenPGP, so they
are not a crypto oracle. That role falls to golden fixtures captured
once from a throwaway Proton account - raw API responses plus the account's
test-only keys - for nodes created by the official web app, the official
CLI and the old rclone backend. Fixtures live in
backend/protondrive/testdata/ and contain no real user data.
Until a test account exists, the crypto oracle is differential testing
against the old libraries: while rclone/Proton-API-Bridge and
rclone/go-proton-api are still in go.mod (they pass rclone's live
integration tests today), test-only code uses them to produce keys, names,
passphrases, content keys, blocks and manifests that the new code must read,
and to read what the new code writes. These tests are deleted in the M4
commit that removes the libraries; by then golden fixtures must have
replaced them. No milestone is called done on differential tests alone
where a live check is listed for it.
9.2 Fake Proton server
backend/protondrive/protontest is a stateful httptest implementation of
the routes the backend uses, importable by tests and reached through the
hidden endpoint option: fork and
SRP auth, refresh, user/addresses/salts/keys, volumes and shares, node
fetch, children, folders, files, revisions, verification, block URLs,
storage upload/download, rename, move, copy, trash, events.
It does not trust the client. It is strict where the real service is strict:
- rejects a block whose verifier token is wrong;
- rejects a commit whose manifest does not match the uploaded block hashes or whose block count/size is inconsistent;
- enforces name-hash uniqueness per folder (code 2500 with conflict details) and draft ownership;
- issues single-use refresh tokens and expiring storage URLs;
- returns 410 for every legacy
/drive/shares/{id}/links|folders|files|eventsroute, so a regression onto removed routes fails immediately; - rejects requests whose app version does not match Proton's published
grammar for
external-drive-rclone@, and any request carryingx-pm-drive-sdk-version; - counts every request by route.
It can be scripted to inject: 429 with Retry-After, 5xx, connection
resets, slow responses, 401 mid-operation, expired storage URLs, a commit
that succeeds but returns an error, CAPTCHA 9001, a skewed Date header,
and hostile responses (9.6).
ProtonMail/go-srp ships server-side SRP (server.go), which the fake uses
for password login.
9.3 Full contract suite, hermetically
fstests.Run runs against the fake server on every go test, with
QuickTestOK: true and a generated in-memory remote config. No network
backend in rclone does this today; it means make quicktest exercises
encoding, ranges, retries (FsPutRetry and its pool-balance assertion),
aborted uploads leaving nothing behind (FsPutError), PutStream, Copy,
Move, DirMove, Purge, ChangeNotify and Shutdown for Proton Drive.
The same tests run with -race. In fork CI only, the fs/operations,
fs/sync and vfs suites also run against the fake: a small main under
protontest serves it standalone, started by a
fstest/testserver/init.d/TestProtonDriveFake script like the other test
servers. That script and remote are fork-only unless upstream wants them.
9.4 Behaviour and regression tests against the fake
One named test per failure this project exists to fix. Rows about events belong to M5 and rows about the persistent cache to M6; everything else is due by M4.
| Test | Guards |
|---|---|
| Block upload gets a 502, then succeeds; and gets 502 forever | #10224 - completes, or fails within a bound; never hangs; no goroutine left |
| Upload fails after draft creation, then is retried | #9622/#9132 - no orphan draft, second attempt succeeds |
| Commit returns an error but the revision is active | no duplicate, reported as success |
Two Fs on one remote, and two processes on one config file, refresh at once |
#9880/#8135 - both keep working, credentials never wiped |
Many NewFs calls concurrently |
#9816 - unlocks never overlap; memory bounded to one |
| Password with spaces, quotes, non-ASCII, 72+ bytes | #8993/#9207 |
| Server clock ahead/behind by hours | 200501 |
| CAPTCHA on password login | clear, actionable error |
| Folder and file created "by the SDK" fixtures | #9622 name-hash lookup |
| Docs node, node with undecryptable name, node with bad signature | #7959, #8003 - skip or warn, never crash |
| File overwritten by directory and the reverse | #9438 |
| Context cancelled at every stage of upload and download | no leak, draft removed, pool balanced |
Events: created, updated, deleted, moved, tree_refresh, cursor expired |
cache, dircache and ChangeNotify all correct |
| Persistent cache: corrupt file, wrong account, old schema | dropped and rebuilt, never trusted |
Request budgets. Using the fake's counters, tests assert upper bounds that encode Proton's traffic expectations, for example:
- listing a folder of N files costs at most
P + ceil(N/100) + 1requests, where P is the number of child-ID pages the server returned; - a second listing with a warm cache costs zero metadata requests;
- a no-op
syncof a tree with D folders and N files, cold, stays within a stated linear bound, and with the persistent cache warm costs a constant number of requests; - an idle
Fsmakes at most one events request per 30 seconds, and none when nothing is cached or watching.
The bounds are written down when the code first meets them and only ever tightened.
9.5 Fuzzing
Go native fuzz targets for everything that parses server-controlled bytes: extended attributes, API error bodies, fork payloads, node decoding, event decoding. Seeded from the golden fixtures; a short fuzz run is part of fork CI, long runs are manual.
9.6 Security tests
- Hostile server. The fake returns: a block with a wrong hash; blocks
swapped or truncated; a block list shorter than the manifest; an older
revision's blocks under the current revision; a block encrypted with
another key; a name decrypting to
../x,a/bor containing NUL; a 10 MiB "name"; an attacker-signed passphrase; a folder with a server-made key and anonymous files beneath it. Each must produce an integrity error, not data: a full read releases no byte before the error. (A whole earlier revision served under the current ID is undetectable; see 6.4.) - Secret hygiene. A test runs login, upload and download at
-vv(without--dump), then searches the captured log for the known password, key password, tokens, session keys and upload tokens. A second test checks that after browser sign-in the config holds no password, and that after password sign-in the password is stored only obscured, as today. (Session tokens and the key password are in the config by design.) - Persistent cache (M6). After a full run, the cache file contains none of the plaintext file names used in the test.
govulncheck ./...andgolangci-lint runclean, as in upstream CI.
9.7 Live tests
Against real Proton accounts dedicated to testing:
- Contract and integration:
go run ./fstest/test_all -backends protondriveclean, with noignore:entries, and with thecmd/bisyncandcmd/gitannexexclusions removed fromfstest/test_all/config.yamlonce they pass. This run's report goes in the upstream PR, as CONTRIBUTING requires. - Account matrix: free account; paid account; two-password mode; custom-domain address (#9434); account with 2FA; an account holding pre-2024 data. Whatever cannot be obtained is listed as untested in the PR.
- Interop with official clients, both directions. Upload with rclone, download with Proton's official CLI and compare; create folders and files with the official CLI, then list, read, overwrite, move and delete them with rclone; a manual checklist confirms the web app opens rclone-written files and shows no signature warnings. Automated behind an environment variable because it needs the CLI binary and an interactive first login.
- Upgrade and downgrade: a config and a tree created by the current release are used by the new code without re-login, and the released version still reads what the new code wrote.
- Scale and soak: 10,000 small files; one 5 GiB file;
rclone mountunder a read/write workload for an hour while another client changes the tree; a restic and a bisync session. Record request counts, 429 counts, peak memory and wall time. - Benchmark against the tier's "within 2x S3" line using the same workloads on an S3 remote; publish the numbers in the PR even where Proton's block and crypto model makes 2x unreachable.
- Build:
go run bin/cross-compile.go -compile-onlyfor all targets, and a before/afterrclonebinary size comparison.
9.8 Fork CI
The fork has no CI today (upstream workflows are gated to rclone/rclone
and the remote is Gitea). Add Gitea Actions workflows:
- on push: build,
go vet,golangci-lint, unit + fake-server suites with-race, short fuzz,govulncheck; - nightly: live
test_allfor protondrive with secrets, serialised to one run at a time, followed by a cleanup that empties the test account; - weekly: check for a new SDK tag and open a reminder to walk the conformance checklist.
10. Documentation
docs/content/protondrive.md: rewrite the hand-written parts - sign-in methods, third-party notice, what is verified, caching and events, limits, and removal of the stale "Beta", transcript andhenrybear327library text. Option sections stay generated.docs/data/backends/protondrive.yaml: features and score updated withbin/manage_backends.py; the tier field is the maintainers' to change.docs/content/changelog.mdis the maintainers' at release.- Fork:
docs/ARCHITECTURE.mdanddocs/DECISIONS.mdupdated in step.
11. Delivery
Milestones. Each ends with all earlier tests still green.
| Milestone | Outcome | |
|---|---|---|
| M0 | Scaffolding | Fake server skeleton; differential test harness against the old libraries; fork CI. Golden fixtures as soon as a test account exists |
| M1 | Session and account | Both sign-in methods, refresh, key unlock, on the fake and live |
| M2 | Read path | Listing, NewObject, download with verification and ranges |
| M3 | Write path | Upload, revisions, drafts, verification tokens, PutStream |
| M4 | Mutations and swap | Mkdir, Rmdir, Move, DirMove, Copy, Purge, CleanUp, About; bridge libraries removed; hermetic and live suites green; the fork's backend is usable for real data |
| M5 | Events | In-memory cache invalidation, ChangeNotify, Shutdown |
| M6 | Persistent cache and metadata | metadata_cache, Metadata feature, small-file upload path, bisync and gitannex green |
| M7 | New crypto model | When Proton publishes it in the SDK |
The fork branch replaces the old implementation in place from M1 onward -
master and released rclone remain the reference for the old behaviour -
so there is never a second package or a selector. M1 to M3 are verified on
the fake and, read-only, live; the branch is not usable for real data until
M4. Upstream receives it as one commit series at M4, as rclone did for the
S3 SDK v2 and Azure SDK migrations. (call)
The steps only a person can do - creating the test account, configuring it
as the TestProtonDrive: remote in their own rclone config (credentials are
never pasted into an agent conversation or committed), and the first
interactive login for fixture capture and interop - are the fork owner's.
Everything that needs the live service (9.7, criteria 3, 6 and 7) waits for
that and is reported as not done until it has run.
Update 2026-11-11: the owner created two test accounts, a free one
(TestProtonDrive:) and a paid one (TestProtonDrivePaid:), and supplied
them configured with username and password on the development machine.
Early live probes against the free account have run (handoff section 6);
the formal live verification (9.7, plan Task 21) has not.
12. Acceptance criteria
The first upstream milestone (M4) is done when:
go.modno longer containsrclone/Proton-API-Bridgeorrclone/go-proton-api, has noreplace, and the release binary is not larger than before.- All targets cross-compile without cgo.
- A config file written by rclone v1.76 works unchanged, without re-login.
- The fake server never sees a legacy route, a missing verifier, a spoofed app version or an SDK-version header.
- Hermetic
fstestsis green with-race, in fork CI so arefs/operations,fs/syncandvfs, and every test in 9.4 and 9.6 not marked M5 or M6 passes. - A live
test_allrun for protondrive is clean with noignore:entries. - Interop passes in both directions with the official CLI and the web app.
- Features:
PutStream,Copy,Move,DirMove,Purge,About,CleanUp,ListP, multi-thread download; SHA1; feature score at least 5. - The docs page is current, and carries the third-party notice.
- Each of these issues has a passing regression test or a recorded live verification: #10224, #9622, #9880, #9816, #9832, #9132, #8993/#9207, #7959, #9438.
The spec as a whole is done at M6, plus: ChangeNotify works under mount;
request budgets hold; bisync and gitannex run green and are no longer
excluded; Metadata gives 6/7.
What this cannot deliver by itself. Two Tier 1 attributes are not code:
"Maintainers >= 2" and "API source: Official". The tier label is the rclone
maintainers' decision. A realistic outcome is Tier 2 on merge, with Tier 1
depending on a second maintainer and on Proton's SDK reaching general
availability for third parties. Even Tier 2 lists "API source: Official", so
it relies on the discretion tiers.md allows; if the maintainers read a Go
port of the SDK as unofficial, the ceiling is Tier 3 regardless of quality.
13. Upstream path
Deferred by the owner's decision. Kept as the route to take if and when the fork's backend is offered upstream.
- Before M1, ask. CONTRIBUTING requires big features to be discussed first. Post a summary of sections 1-6 and 9 on #9252 and in forum thread 53609 (where ncw and Proton's engineers talk), and ask ncw the questions in section 14. Nothing here has been posted yet.
- Offer maintainership. ncw has asked for a Proton Drive maintainer since October 2025. The offer, a test account for the integration server and a CODEOWNERS entry are part of the proposal, not an afterthought.
- Credit and coordinate with authors of overlapping open PRs: #9522 (browser login), #9881 (refresh race), #9851 (name-hash fallback), #9834 (bandwidth), #9081 (verification tokens).
- The upstream branch is cut from
masterand contains onlybackend/protondrive/,go.mod,go.sum,docs/content/protondrive.md,docs/data/backends/protondrive.yamlandfstest/test_all/config.yamlchanges - none of the fork-only paths inAGENTS.md. - Tell Proton. The same forum thread is where to state the app version, the routes used and the event polling interval, and to ask when the block-verification exemption can be dropped for the new version.
14. Open questions
Answered by the fork owner on 2026-11-11:
- Maintainership: no permanent maintenance is planned, so no offer is made upstream. Tier 1's "Maintainers >= 2" is therefore out of reach through this fork; the code-level attributes remain the target.
- Test accounts: a free Proton account will be created for testing, possibly a new member of the owner's Proton family plan. The live matrix in 9.7 starts as "free account" only; the other account types are recorded as untested.
- Upstream post: not now. Build something working first.
- Fake server: accepted at roughly a third of the code volume, kept in the fork and not offered upstream.
Added 2026-11-11: a paid account was also provided, so the live matrix in 9.7 is free plus paid. The paid account has not been signed in to yet.
Still open, for whenever the upstream path is taken.
For rclone's maintainers (to ask in step 13.1):
- In-place replacement under the same backend name, or a period with both implementations selectable?
- Is an in-package fake server and a hermetic
fstestsrun welcome upstream, or should it stay in the fork? - Is a persistent on-disk metadata cache acceptable in a backend, given it stores ciphertext only?
- Is browser sign-in acceptable as the default for new remotes?
- Is a hidden
endpointoption for tests acceptable?
For Proton (to ask in step 13.5):
- Which channel should development builds send, given the grammar has no
dev?
15. Risks
| Risk | Response |
|---|---|
| Proton enforces "official SDK only" through the requirements headers or app-version blocks | Conformance work, honest identification and the open channel with Proton's engineers make this unlikely; the sidecar design is the documented fallback |
| The new crypto model lands before M4 | Crypto is isolated; M7 is planned; old-format reads stay. Timeline risk is real and outside our control |
| A port that is wrong in the same way as its fake | Golden fixtures from official clients, SDK-derived vectors, and two-way live interop |
| ncw's review bandwidth | Small commits, hermetic tests he can run, and an offered maintainer |
| SDK drift (0.x, breaking minors, roughly fortnightly releases) | Pinned reference tag, conformance checklist, weekly reminder |
| SRP password login is withdrawn for third-party app versions | Browser sign-in is already the default path |
| Proton's README says the SDK is "not yet ready for third-party production use" | The backend stays honestly labelled in the docs; Proton has said it "does not prohibit the usage of rclone" |
16. Sources
Researched 2026-11-11: rclone #9252, #8873, #8870, #9081, #9189, #9260,
#9522, #9622, #9816, #9832, #9844, #9880, #9881, #10224; forum threads
53609 and 52742; ProtonDriveApps/sdk at 05c9115 (README, client/js,
incubating/account, cli), its PR #19 and issues #5, #6, #7, #11, #17,
#21, #22, #35, #72, #74; docs/content/tiers.md, CONTRIBUTING.md,
fstest/fstests/fstests.go and backend/protondrive/ in this tree at
448d7361c.