SSH preparation at Git transport boundaries
Pinned upstream 7338078f8ddd924b8cddee35f512f2286072136d:
AppUtils.cpp::DoFetch, DoPull, DoPush and LaunchPAgent.
Source Fetch All loads every configured remote key before one fetch invocation;
Push loads each remote's key immediately before that remote's push. The return
from source LaunchPAgent is ignored by these callers. Complete native error and
retry decisions remain a coordinator responsibility, not proven source parity.
Core Fetch, Fetch for Rebase, Pull, Push and remote-branch lookup now accept an
optional awaited SSHTransportPreparation callback. Callers without a callback
retain normal Git environment behavior. Fetch/Pull/Push and Fetch branch browsing
now supply an operation-owned native coordinator when Auto-load SSH key is enabled.
Fetch validates its options, then supplies either the selected destination or all remote names once before running Git. Fetch for Rebase forwards the callback through its selected-branch fetch; target capture and recovery reads do not re-run preparation. Pull supplies its destination before the transport command. Branch lookup supplies its destination before ls-remote. Push validates the plan, retains its existing saved-config effects, then prepares one remote before its first push. All-branches plus tags shares that session for both commands.
The callback can suspend for native grant or passphrase decisions and reenter
the repository actor for settings reads. It returns an owned SSHAgentSession
or nil. A returned session is retained through the command, including both Push
branches/tags invocations and error/cancellation paths. Git receives its private
socket and an empty SSH_AGENT_PID; an inherited login-agent PID is not paired
with that socket. A coordinator may retain a shared session to accumulate keys,
and must eventually close/release it. File grants must remain held during key
loading. No credentials are added to Git argv or environment.
Preparation receives the caller's cancellation token, or an owned token when none was supplied. Cancellation is checked before and after awaiting the response so a late response cannot launch Git. Preparation errors abort the current transport. Push wraps such failures with prior completed destinations and stops later remotes; successful earlier pushes and prior saved config are retained. This hook behavior is not a claim that native error decisions exactly match the source callers' ignored Pageant return value.
Six new tests use a real private agent, generated fixture key and a controlled Git wrapper. The wrapper refuses to query any agent unless its socket is inside the fixture, verifies the public fixture identity before executing real Git, and asserts an inherited sentinel PID is cleared. Local repositories verify Fetch, Fetch for Rebase, Pull and branch lookup, Fetch All's one callback, Push's ordered partial success, shared branches/tags lifetime, validation/pre-cancel/late-cancel, and live Git helper/child plus agent reaping. No SSH server is contacted: local Git effects and Unix-agent access prove the preparation channel, not network SSH authentication. Nil-callback Fetch/Pull/Push regression tests also run.
Native auto-load coordinator
Fetch/Pull and Push expose Auto-load SSH key, available only when the private agent, loader and response helper resolve. It defaults on for an available runtime and is saved per repository and dialog mode. Each submission captures a factory; subsequent checkbox changes cannot change that operation. Each retry creates a fresh coordinator. Remote branch browsing also prepares its selected remote. Arbitrary URLs without a configured remote key do not select an identity.
The coordinator reads the native remote.<name>.turtlegitsshkeyfile setting and
holds the private read-only bookmark lease through loading. It creates a private
agent lazily, batches Fetch All identities, and accumulates Push identities in
remote order. A successfully loaded file is deduplicated by byte-exact path,
SHA-256 content digest, device, inode, size and nanosecond modification time.
Replacing/modifying a key reloads it; the surrounding file checks do not prove
protection against all concurrent file changes.
Loading first refuses interactive askpass. macOS OpenSSH can report that refusal
with status 1 and no diagnostic. A bounded 256-byte header check recognizes
encrypted OpenSSH (including CRLF), encrypted PKCS#8 and traditional encrypted
PEM before requesting a response in that case. Recognized incorrect-passphrase
diagnostics also request a retry. The loader uses invocation-local LC_ALL=C.
Unrecognized headers and other loader errors remain errors; this is not a complete
key-format parser. A native secure response window owns one response, clears on
Cancel/closure and shows a retry explanation after an incorrect response.
Prompt Cancel, operation cancellation and parent invalidation stop preparation; late responses cannot launch Git. The private agent closes when transport ends. Invalid grants/keys and loader failures abort instead of silently continuing as the source callers can after ignoring Pageant's return. Earlier successful pushes and saved config remain retained. This is an explicit macOS adaptation.
Hidden native fixtures exercise real encrypted keys, wrong/correct responses, replacement-key reload, balanced mock bookmark leases, token/forced closure, shipping model Fetch/Pull/Push/browse effects and checkbox snapshots with system and bundled Git. A shipping Push controller is closed while awaiting a response: no transport or late success callback may follow. Windows are not ordered or shown; sheet focus, physical keyboard behavior and signed bookmark access remain unverified. No login agent, user key or real SSH server is used.
App Store runtime resolution still refuses missing bundled OpenSSH agent/add; auto-load is disabled there until packaging is complete. Unsigned Debug uses the system OpenSSH tools plus the embedded response helper.
Remote tags and Browse References deletion
Remote tag listing and accepted deletion now use the same awaited preparation boundary. Tag names are validated before loading any key; each catalog, deletion and post-deletion refresh owns a fresh coordinator and agent. These dialogs add no auto-load checkbox. Available native runtimes automatically prepare a configured native key; absent/unavailable native keys retain ordinary Git behavior. This extends macOS key loading to the source remote-tag workflow; the pinned DeleteRemoteTagDlg itself does not call LaunchPAgent.
BrowseRefs remote-branch deletion follows the source's sorted remote groups: one preparation immediately before each remote's batch Push. Local branch/tag deletions perform no preparation. One operation-owned coordinator can retain loaded identities across groups; any preparation/Push failure stops later groups, retaining earlier server effects. The single-reference Core convenience route forwards the same optional hook. Log's separate history-reference deletion route is described below.
Native owners present encrypted-key responses using their existing window or progress window. Forced closure cancels their existing token, rejects late prompt answers and closes the agent. Physical nested-sheet and signed acceptance remain unverified. Preparation remains optional for Core callers.
Log reference deletion
Log retains the source DeleteRef remote/local-tracking choices. Only the remote server choice prepares its configured remote before Push. An operation-owned coordinator can retain loaded identities across the source All-label sequence; local branch/tag/generic-ref and stash choices do not load keys. Runtime availability determines automatic loading; no additional checkbox is introduced.
One owned token now covers reference validation, current/merged reads, snapshot reads, destructive commands and the shared stash/reflog batch helpers. Closing Log cancels that token, dismisses attached sheets and prevents late errors, acknowledgements or reloads. Shared batch deletion stops on cancellation rather than treating it as an ordinary command failure and continuing to another entry. Earlier successful deletions remain retained.
After suspended SSH preparation the backend checks the local tracking reference snapshot again before Push. A changed snapshot aborts that transport; this is a native guard, not a claim that the remote server ref cannot change concurrently. The existing source All-label behavior remains: reported remote/stash errors can continue, ordinary/local-tracking errors stop, and Cancel stops the sequence. Signed access, physical sheet interactions and all races remain unverified.
Remaining: broader SyncDlg SSH consumers and signed submodule Update inherited-agent compatibility, Keychain, host-key/password prompts, physical UI/sheet acceptance, signed App Store/Finder and real network authentication. Concurrent config/ref changes across suspended preparation and complete source failure equivalence remain unverified.
Clone selected identity
Pinned CloneDlg restores the auto-load preference/key history; CloneCommand loads that explicit key before a repository/remote exists and stores it before Log. Native Clone supplies an explicit-key coordinator preparation with an empty remote list. Validation precedes preparation, and preparation/cancellation failure cannot start Clone. Both direct and captured-progress paths own a fresh coordinator; Retry creates another. The read-only identity store supplies the selected grant.
After success, native Core writes remote.<origin>.turtlegitsshkeyfile using the literal key path and precomposeunicode=false, including bare/default-origin clones. It leaves core.sshCommand and Windows puttykeyfile unset. Legacy Core sshKey callers retain the previous shell-command mode unless loadSSHKeyWithAgent is selected. The native mode requires a returned session; missing runtime fails before cloning. Post-clone key-config write failure retains the destination and reports failure, an explicit difference from source's StorePuttyKey error box followed by continued result actions.
Hidden receiver coverage includes direct/progress effects and forced options-window closure during an encrypted-key response, no late adoption/error/permission changes, and private agent/grant cleanup. Actual server, modal picker ownership, displayed sheets, recursive SSH/SVN, signed scope and post-clone metadata cancellation remain unverified. See CLONE-PARITY.md and qa/ssh-clone-2026-10-09.json.
Submodule Add now shares explicit-key preparation before a child remote exists, then saves the native origin key on the added child. Its captured native owner holds grants and fences closure/late results. See Add parity for source Force precedence, verification and remaining progress/signed limits.
Saved authentication continuation — 2026-10-13
The shared encrypted-key response now includes source-style Save authentication, off by default. An app-private Keychain store records only successfully loaded responses, indexed by SHA-256 of exact key-file bytes. Rejected stored responses return to the secure prompt; unchecked manual responses leave storage intact. Read failures permit a manual response, while save failures stop preparation before transport. The captured owner checks cancellation around asynchronous reads/writes and observable key changes around loading. Global operation counts retain Quit ownership through forced window closure.
A native Saved SSH Authentication window exposes labels and selective confirmed removal through metadata-only reads. It is reachable from the application menu and Credential settings; writes are immediate and not Settings drafts. Private agent identities already loaded are unaffected by removal. This is a macOS adaptation of authentication persistence, not completion of generic HTTP, SSH account-password, host-key or certificate dialogs.
See user guide. The grouped native batch passed six cases across system and bundled Git, including actual encrypted loading, successful-save/reuse, rejected responses, retirement during a held saved read, Quit ownership and selective removal. Debug and unsigned AppStore builds passed, as did the AppStore bundle contract (202 native items, 296 icons). See dated QA. Live signed Keychain, physical acceptance and full authentication parity remain unverified; this continuation is not certified as complete. Earlier Keychain gap statements describe earlier checkpoints.
Shipping remote picker CI repair and cached SSH verification (2026-10-13)
Hosted run 38434623838 failed the native coordinator fixture's old model-only
Fetch Browse assertion. Browse now delegates to the owned RemoteReferenceWindowController,
so the fixture had no callback and could not exercise its key preparation. The
updated check creates the shipping Fetch controller, opens its remote picker,
answers the actual encrypted-key prompt, verifies its live main reference and
accepts it back into Fetch. Other transport ordering, cancellation, agent and
lease cleanup assertions remain in place. No shipping app behavior was changed.
The standalone SSH test command now delegates to the cached app-object native
batch rather than compiling every app Swift file again. It retains both Git-engine
arguments, private Git configuration, a 300-second receiver limit and process-group
cleanup on success/failure/timeout. The SSH fixture uses a short private /tmp
directory because agent Unix-domain sockets have a path-length limit; the first
cached-run attempt exposed this limit and was reaped before the path repair.
This fixes the identified fixture path and reduces repeated native compilation. A local focused pass does not establish a full hosted workflow pass or signed SSH acceptance. Batch evidence records final checks, failures and owned receiver cleanup.
Hosted receiver readiness correction — October 15, 2026
Hosted run 38664961820 failed at the remote-reference picker in the SSH family. The local receiver reproduced that failure: waiting for Fetch.busy alone did not include its separately owned remote metadata request. The fixture now waits for the same complete readiness gate used by Browse. It also awaits Push source classification after editing the source and Clone's authorization phase before asserting direct completion. Those later mismatches became visible only after the first failure was fixed. Shipping ownership guards remain unchanged.
Both engine cases passed the entire SSH family after these corrections, including original prompt, exact transport ordering, refs, results, private agent cleanup, late-response, Clone and submodule checks. Failure diagnostics now include the source line and picker readiness snapshot. See recorded results. A new hosted pass is still required; local success does not establish signed/public SSH acceptance.
Session replacement and hosted marker handoff — October 16, 2026
Preparation now replaces closed/exited private agents and reloads their identities instead of returning a stale loaded-key cache. Concurrent closure waits for shared worker cleanup. Startup rejects non-socket placeholders and owned helpers cannot leave descendants on normal exit.
Hosted macOS run 38922360712, job 117049148805, failed
SSHAskpassTests.testForcedCloseDuringCredentialLoadingRemovesEnvelopeAndChildren:
the readiness marker existed before its two PIDs had been written. Related slow
loader fixtures now publish the complete PID marker by same-directory rename.
The original credential permissions, two-PID, liveness, worker termination and
credential-removal assertions remain. No longer delay or weaker assertion is used.
The grouped native lifecycle runner uses the built app objects, actual bundled OpenSSH and private fixture keys; coordinator and saved-credential cases preserve their original checks. Local results do not establish a complete hosted CI pass or signed Finder/App Store acceptance.
The grouped checkpoint passed 22 Core tests, both Debug builds and all six native case/engine runs, with every owned receiver reaped. See the lifecycle batch record for scope and the separate remaining hosted Merge view failure.