Merge branch 'main' into fix/creds-selfheal

`feat(ssh): probe ~/.ssh default identity keys (#507)` refactored the same
function #486 changes, so the passphrase self-heal is re-expressed in the new
shape rather than bolted back on.

Main split the decode step out of `try_identity_file` into
`load_identity(contents, raw_path, source, cached) -> IdentityLoad`, keyed on a
new `KeySource`: an explicit key's failures are said aloud and may ask for a
passphrase, a discovered `~/.ssh` default's are silent, because russh can only
try an encrypted key by signing and a prompt for a key nobody asked for is a
prompt the server may not even want.

#486's retry now lives in that matrix. `IdentityLoad::NeedsPassphrase` carries
a `rejected` flag, and an explicit key whose *cached* passphrase does not open
the file returns `NeedsPassphrase { rejected: true }` instead of `Unusable` —
one prompt path serves both ways of getting there, the typed answer still gets
its attempt, and a typed answer that fails is the hard failure it always was,
via `round.unusable`. A discovered key keeps skipping in silence whether its
cached passphrase is missing or stale: the self-heal must not turn one stale
`~/.ssh` keychain entry into a sheet on every connection, so only explicit keys
drive it. `no_discovered_key_ever_asks_for_a_passphrase` pins that seam.
This commit is contained in:
l0ng-ai
2026-08-11 21:26:27 +08:00
73 changed files with 8029 additions and 652 deletions
+22 -2
View File
@@ -214,9 +214,29 @@ DMG="dist/tty7-${VERSION}-macos-${ARCH}.dmg"
STAGE="dist/dmg-stage"
rm -rf "$STAGE"
mkdir "$STAGE"
cp -R "$APP" "$STAGE/"
# `mv`, not `cp -R`: this is the peak, and a second full copy of the bundle is
# the most expensive thing on the volume that nobody needs. Nothing reads
# dist/tty7.app after this point — the zip above is what the updater ships and
# what nightly.yml verifies (it extracts that, not this), and release.yml only
# knows about tty7.app as an intermediate to keep out of the upload globs.
mv "$APP" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
hdiutil create -volname "tty7" -srcfolder "$STAGE" -ov -format UDZO "$DMG"
# Size the image explicitly. Left to itself, `-srcfolder` measures the bytes it
# is about to copy and asks for about that much, which does not cover what the
# filesystem spends carrying them — so the copy runs the *volume* out of room
# partway through and hdiutil reports "No space left on device". The path in
# that message is under /Volumes/tty7, not on the host: three nightlies died
# here on 2026-08-10 with 105 GiB free on the runner. It is a threshold, not a
# cliff — the x86_64 binaries are the larger pair and crossed it first, while
# arm64 went on building fine just underneath.
#
# Doubling the content and adding 64 MiB is far more slack than the shortfall
# needs, and it is close to free: the image is compressed on the way out, so
# measured against a stage of this shape, 127 MiB of empty volume cost 672 KiB
# in the published DMG.
STAGE_KB="$(du -sk "$STAGE" | awk '{print $1}')"
hdiutil create -volname "tty7" -srcfolder "$STAGE" -ov -format UDZO \
-size "$(( STAGE_KB * 2 + 65536 ))k" "$DMG"
rm -rf "$STAGE"
if [[ -n "$SIGN_ID" && -n "${APPLE_CERTIFICATE:-}" ]]; then
codesign --force --timestamp --sign "$SIGN_ID" "$DMG"