Quiet keyless SSH links, add aether link --key, share one auth resolver - #32
Merged
Conversation
…e one resolver A successful Tailscale link printed "parse ssh key ... passphrase protected" because the client parsed ~/.ssh/id_ed25519 up front and reported every problem even when the server never asked for a key. The new cli.ResolveAuth records what it examined and speaks only when the handshake is refused: the error then lists the ssh-agent outcome, each default key file (id_ed25519, id_ecdsa, id_rsa) and what stopped it, the server's banner, and the two fixes (ssh-add, --key). aether link --key <private-key> selects one key deterministically: only that key is offered, an agent copy counts only when it is the same key, and a .pub file is refused with a clear message. The absolute path is saved with the default or named link and kept on relink. The sync daemon now uses the same resolver instead of parsing one file itself. Test helpers set only XDG_CONFIG_HOME and AppData, so on macOS os.UserConfigDir still pointed at the real ~/Library/Application Support and tests overwrote the developer's config. internal/testhome sets every home and config variable and asserts the lookup stays inside the scratch directory. Docs now give the macOS config path, describe key discovery and --key, and call the key a private key throughout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HaEkHna6S1R7uyf4xBoWti
… the saved key applies TestLinkKeyPersistsAndRelinks built a relative --key with filepath.Rel from the checkout to the temp dir, which fails on Windows CI where the two sit on different drives. The test now runs from the key's directory and passes a bare file name. aether link --key auto forgets a saved key and returns the link, or the named profile, to ssh-agent and default-file discovery; before, the only way back was editing config.json. The auth error for a refused explicit key now names that way out and lists only the chosen file. Config.Named no longer overlays the top-level key onto a profile that saved none. A profile linked by discovery stayed on discovery until the default link chose a file, at which point gui --server <profile> offered only that file and was refused by a server that never saw it; it also made --key auto on a named profile a no-op. Docs no longer say later commands inherit the saved key. It covers Aether's own SSH connections and the daemon event channel when the same file is passed to daemon run --key; git push, the fetch behind aether pull, and the daemon's git traffic run the system git over OpenSSH, which needs ssh-add or an IdentityFile entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mjx8gvQA4YnryGieZTfM87
…heritance Commit 7b727a2 made Named ignore the top-level key for every profile without one. Config files written before per-profile keys existed have empty profile keys that relied on inheriting that field, so the change would have silently switched them to discovery. NamedLink gains AutoKey, written as auto_key only when true. Named clears the effective key for a profile carrying the marker and keeps the non-empty overlay for everything else, so legacy files resolve exactly as before. aether link --name saves the marker when the profile used discovery, including --key auto, and clears it when --key names a file. savedKey resolves through Named, so relinking a legacy profile without --key keeps the inherited key in effect and writes it into the profile, while a discovering profile stays on discovery. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mjx8gvQA4YnryGieZTfM87
…annot reach the real config The localops link test overwrote the developer's real config on macOS when run from a checkout whose helper set only XDG_CONFIG_HOME and AppData; os.UserConfigDir reads HOME there. testhome.Isolate fixed that on this branch, but any helper that misses one platform's variable makes the same mistake again. cli.Path now honors AETHER_CONFIG_DIR first: one variable, the same on every platform, with no test-only hook in production code. Isolate sets it beside the home variables, and both useTempConfigDir helpers keep asserting the resolved path landed in the scratch directory. TestLinkRepoLeavesRealConfigAlone writes a sentinel where the platform lookup says the real config lives (a scratch stand-in), points HOME, XDG_CONFIG_HOME, and AppData back at it after isolation, and proves LinkRepo writes only the pinned path; the cli package proves the override wins and that clearing it restores the platform lookup. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mjx8gvQA4YnryGieZTfM87
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
aether guino longer printsparse ssh key ... passphrase protectedwhen the server accepts the connection without a key.cli.ResolveAuthrecords what it examined and only speaks when the handshake is refused.id_ed25519,id_ecdsa,id_rsa) and what stopped it, the server's banner (no Aether member for this keywas previously dropped by the client), and the fixes:ssh-add <key>or--key <private-key>.aether link --key <private-key>. Deterministic: only that key is offered; an agent key counts only when it is the same key (matched by public key, so a passphrase-protected--keyworks afterssh-add)..pubfiles are refused with a clear message. The absolute path is saved with the default or named profile and kept when relinking that profile without--key.--key: every agent key, then~/.ssh/id_ed25519,id_ecdsa,id_rsa. A missing, malformed, or locked candidate no longer blocks a later usable one. Keys already offered by the agent are not offered twice.aether daemon runuses the same resolver instead of parsing one file; host-key policy stays strict there (no TOFU for an unattended process).internal/testhome.IsolatesetsHOME,USERPROFILE,XDG_CONFIG_HOME,AppData, clearsSSH_AUTH_SOCK, and fails the test ifos.UserConfigDir/os.UserHomeDirresolve outside the scratch dir. Previously tests on macOS overwrote the real~/Library/Application Support/aether/config.json.--keytakes the private key, updated troubleshooting rows.Out of scope, unchanged: TOFU host-key behavior, keyless/tailnet semantics, OpenSSH config (
Include,Match,ProxyJump).Test plan
make fmt-check,make vet,make lint(withGOTOOLCHAIN=go1.25.13),make public-auditpass.make test: all packages pass exceptinternal/coord,internal/mcpbridge,internal/protocol,internal/scheduler, which fail identically onorigin/mainon this macOS host (temp-path symlink / socket-length issues, unrelated).go test -race -tags integrationforinternal/cli,internal/syncd,internal/localops,cmd/aether,internal/testhomepasses. Docker is not available here, so the Docker-backed integration suite was not run.id_ed25519, usableid_rsa, no agent, keyless server, no stderr beyond the TOFU notice); discovery reaching a later candidate; explicit key ignoring unrelated agent keys; locked explicit key unlocked by a matching agent key; failure diagnostics including the server banner; invite with no key;CheckKeypublic-key rejection; link key persistence in top-level and named profiles plus relink; daemon discovery and daemon actionable error; testhome isolation.🤖 Generated with Claude Code
https://claude.ai/code/session_01HaEkHna6S1R7uyf4xBoWti