Add typed SSH remote endpoints

This commit is contained in:
2026-08-25 19:12:34 +02:00
parent f0d6a04be1
commit f636f3b551
16 changed files with 1712 additions and 310 deletions

View File

@@ -62,13 +62,38 @@ configured editor.
IronStorage. Both must be present together and cannot contain line breaks or
angle brackets. The built-in IronStorage identity is used when both are absent.
Git remotes are HTTPS-only. URLs containing user information, passwords,
queries, or fragments are rejected. `server_id` and `application_id` are opaque
references used to retrieve credentials from the operating-system secret
store; duplicate names and duplicate reference pairs are errors.
HTTPS remote URLs containing user information, passwords, queries, or fragments
are rejected. `server_id` and `application_id` are opaque
HTTPS-only references used to retrieve credentials from the operating-system
secret store; duplicate names and duplicate reference pairs are errors.
The HTTPS account name is stored inside the protected credential record, not in
TOML. OpenPGP passphrases are addressed by the resolved primary fingerprint.
SSH remotes use either `ssh://[user@]host[:port]/path` or scp-like
`[user@]host:path` syntax and omit the HTTPS credential fields:
```toml
[[git.remotes]]
name = "origin"
url = "git@git.example.test:alice/password-store.git"
```
The typed endpoint model is always available so an SSH remote remains readable
in configuration even when the binary was built without SSH. Such a build
returns a typed unsupported-transport error before connection or repository
mutation. The optional storage `ssh` feature contains `russh` 0.63.1 and Tokio;
`russh` default features are disabled and the Ring backend plus RSA key support
are selected explicitly.
For SSH, URI paths are absolute, scp-like paths without a leading slash are
relative to the remote account, and `~`/`~user` paths retain tilde-expansion
semantics. Bracketed IPv6 and explicit URI ports are accepted. Host names are
IDNA-normalized, repository paths may contain Unicode, and usernames are
restricted to ASCII letters, digits, `.`, `_`, and `-`. Empty paths, control
bytes, credentials, queries, fragments, ambiguous unbracketed IPv6 or colon
paths, leading-option paths, local paths, URL rewrites, separate push URLs,
helper transports, and unknown schemes fail closed.
`clipboard_timeout_seconds` controls the native clipboard presentation lease.
It defaults to 45 seconds for upstream `pass` compatibility and must be between
1 and 300 seconds. The CLI remains alive for the lease so Linux can serve its

View File

@@ -20,13 +20,22 @@ committer. Only their affected paths are staged, unrelated index state is
preserved, no-op mutations create no commit, and commit failures restore the
index so the storage transaction can roll back its files.
## Remote endpoint contract
Storage parses credential-free HTTPS URLs plus feature-gated `ssh://` and
scp-like SSH URLs into one typed endpoint contract. Local paths, `git://`,
`file://`, helper transports, URL rewrites, separate push URLs, embedded
credentials, and unknown schemes are rejected before transport. A build
without the `ssh` feature reports SSH as unsupported before connection or
repository mutation instead of treating its configuration as malformed.
## HTTPS transport
Remote URLs must be absolute, credential-free HTTPS URLs. SSH, scp syntax,
`git://`, `file://`, local paths, helper transports, URL rewrites, separate push
URLs, and unknown schemes are rejected before transport. Credentials are
requested with the configured server ID and application ID and remain outside
Git configuration.
HTTPS credentials are requested with the configured server ID and application
ID and remain outside Git configuration. The SSH session, host-verification,
authentication, and pack-protocol implementations are separate milestone work;
until those layers are present, network operations on SSH endpoints return the
typed unsupported-transport result.
Fetch uses the embedded Rust smart-HTTP client with an explicit credential
callback, so Git's credential cascade is never entered. Push implements the