feat(desktop): frictionless SSH onboarding, no env vars required - #143
Open
muscariello wants to merge 14 commits into
Open
feat(desktop): frictionless SSH onboarding, no env vars required#143muscariello wants to merge 14 commits into
muscariello wants to merge 14 commits into
Conversation
A fresh install could not reach a room: DID auth read SHADI_SLIM_AUTH, SLIM_HUMAN_SEED and SLIM_MEMBER_DIDS from the app's own environment, and mTLS material had to exist on disk. Clicking Start node reported a missing certificate, and the only way through was a shell script. Onboarding now produces all of it. identity_bootstrap takes a local SSH Ed25519 key and derives the human DID from its public half and one agent DID per name from its private half, stores the 32-byte root in the secret store (keychain or 1Password), generates mTLS material, and writes identity.json. slim.rs prefers that over the environment, falling back to the env contract when set so the CLI demos keep working. identity_discover_ssh_keys lists the Ed25519 keys already in ~/.ssh with their would-be DID, so setup is a choice from a list rather than a path to type; it reads only .pub files, so nothing probes private keys speculatively. Passing a GitHub handle cross-checks the key against github.com/<handle>.keys and refuses to claim a handle the key is not published under. identity_trust_github_handle is the invite side: name an account and its published ssh-ed25519 key gives that person's human DID. That records who we accept — it cannot admit their agents, whose DIDs derive from their private key, so member_dids() deliberately excludes trusted humans (#141). mTLS uses rcgen rather than shelling to openssl, which a clean Windows install lacks; existing files are left alone so re-running cannot invalidate certs a peer already trusts. Verified a live SLIM node accepts the rcgen-issued certs. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Onboarding only looked in ~/.ssh, which is a dead end for anyone who keeps keys in 1Password and lets its agent serve them — a real case, and the first one tried: no files there at all. The key source is now the user's choice of three: a key discovered in ~/.ssh, any file via the native picker (tauri-plugin-dialog), or a 1Password SSH Key item. BootstrapRequest takes a tagged KeySource instead of a bare path. 1Password items are read with `op item get --fields 'private key'` rather than through SHADI's own 1Password SecretStore, which cannot serve this: it expects base64 in an item's notesPlain, its own storage format, not how a native SSH Key item holds a key. Listing uses the public key field only, so browsing never reveals a private one, and it still yields the DID preview. Not being signed in is reported with `op signin` rather than a raw CLI error. `op` returns multi-line fields quoted and newline-escaped; two tests pin that a field mangled that way still parses and derives the same seed as the plain PEM. The passphrase field is always shown now. It was conditional on the ~/.ssh scan detecting encryption, which neither of the other sources can determine before reading the key. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
The note above the 1Password source read like a failure and led with an implementation detail — that SHADI's own 1Password SecretStore expects a different storage format — which tells the user nothing they can act on. It was reported as an error message. Removed. The vault-policy caveat it also carried already appears in the read failure itself, where it is actionable, and the format detail belongs in the code that has to know it. The item list now loads when the source is chosen instead of behind a button, since `op` prompting for Touch ID is the expected 1Password flow. It still never runs at startup. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Registering tauri-plugin-dialog is not enough: Tauri v2 gates each plugin command behind a capability, so Browse… failed with "dialog.open not allowed". Grants dialog:allow-open only, not dialog:default — the default bundle also permits save dialogs, message boxes and confirm prompts, none of which this app uses. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
`op` was invoked without --account, so it used whichever account is default. This machine has three configured, and the default is not the one holding the SSH key — the item list came back from the wrong vault with no indication why. Adds identity_list_1password_accounts and threads an account UUID through every `op` call, including the public-key read behind the DID preview. KeySource::OnePassword carries it. The picker deliberately has no default selection: `op` silently choosing one is what caused this. Accounts are listed as soon as the source is chosen, since that needs no unlock; reading a vault's items waits for an explicit account pick, because that can prompt for Touch ID. A sign-in error now names the account it applies to. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
The frontend sent kind "onepassword" but rename_all = "snake_case" turns OnePassword into one_password, so identity_bootstrap rejected every 1Password attempt with `unknown variant`. Aligns the frontend and adds a test that deserializes the exact JSON it sends, including nulls for the optional fields, and asserts the old misspelling still fails. Nothing checked the two sides agreed, which is why a one-word mismatch reached the user. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Picking the obviously-named 1Password item ("SSH Key - Github") and being
told it is not the published key left nothing to act on: the app knew both
DIDs and made the user guess which of their keys was the right one.
Entering a handle now resolves the DID it publishes and marks each
candidate — in both the ~/.ssh and 1Password lists — with ✓ or ✗, using
the public key the listing already reads. The line under the picker says
what the handle publishes and offers the other way out: clear the handle
and set up without the GitHub binding.
Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Trusting @msardara failed with "no ssh-ed25519 key found among 1 published SSH key(s) ... add one with: ssh-keygen -t ed25519". Both halves were unhelpful: it never said the published key is RSA, and it told the operator to generate a key when the account is someone else's. The error now names the algorithms found (found: ssh-rsa) and carries no remedy, since only the caller knows whose account it is. Trusting a handle appends "Ask @<handle> to add an Ed25519 key to their GitHub account"; verifying your own during bootstrap appends the self-directed version plus the option of clearing the handle. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Starting the desktop app printed "keychain get failed: The specified item could not be found in the keychain." three times before onboarding had run. Asking whether a secret is set yet is ordinary control flow, and the store was both logging it and reporting it as StorageFailure — so a genuinely broken keychain looked identical to an unset key. get and delete now map errSecItemNotFound to InvalidInput, matching what the 1Password backend already does for not-found, and only log real failures. The desktop's load_seed uses the distinction: a miss says run onboarding, anything else reports the underlying error instead of blaming setup. Two tests pinned the old conflation and now assert the split, plus one that a real backend failure is still StorageFailure. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Signed-off-by: Luca Muscariello <muscariello@ieee.org>
The desktop app is its own workspace, so the release bumps to shadi_identity, agentbridge, shadi_mas and agent_transport_slim left its Cargo.lock behind and 'cargo check --locked' refused to proceed. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Onboarding showed five inputs at once. The passphrase box appeared for every key, though discovery already reports which keys are encrypted, and the agent list, GitHub handle and endpoint all have working defaults. The default path is now pick a key and press the button; the rest sits behind Options, with a one-line summary of what it will use. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Onboarding assumed a key already existed, so a fresh machine had to leave the app for ssh-keygen before anything worked. identity_generate_ssh_key writes ~/.ssh/id_ed25519 at 0600 and its .pub, and the panel offers it when discovery finds nothing. It refuses to touch an existing key: overwriting one would orphan every agent DID already derived from it. The passphrase is optional, matching ssh-keygen with the prompt left blank. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
Reading only HOME made discovery report an empty ~/.ssh on Windows even when the machine had keys, and key generation fail with 'cannot locate a home directory'. The whole onboarding path was Unix-only. The 0600 mode stays Unix-only; on Windows the key inherits the user profile's ACLs, which is what ssh-keygen there relies on too. Signed-off-by: Luca Muscariello <muscariello@ieee.org>
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.
Closes #123.
A fresh install could not reach a room. DID auth read
SHADI_SLIM_AUTH,SLIM_HUMAN_SEEDandSLIM_MEMBER_DIDSfrom the app's own environment, and mTLS material had to already exist on disk — so Start node reported a missing certificate and the only way through was a shell script.What
identity_bootstrap— one step from an SSH Ed25519 key to a usable app: human DID from the public half, one agent DID per name from the private half, the 32-byte root into the secret store (keychain / 1Password), mTLS generated,identity.jsonwritten.slim.rsprefers this over the environment, and still honours the env contract when it's set so the CLI demos keep working.identity_discover_ssh_keys— lists the Ed25519 keys already in~/.sshwith the DID each would produce, so setup is a choice from a list. Reads only.pubfiles, so nothing probes private keys speculatively.github.com/<handle>.keys(no token) and refuses to claim a handle the key isn't published under.identity_trust_github_handle— the invite side: name an account, get that person's human DID from their published key.Two things worth reviewing
member_dids()deliberately excludes trusted humans. SLIM admission verifies the agent DID on the wire, and someone's agent DIDs derive from their private key — so trusting a handle records who we accept but cannot admit their agents. Putting their human DID in the allow-list would look like it worked. That's #141.mTLS uses rcgen, not
openssl— a clean Windows install has no openssl. Existing files are left alone, so re-running onboarding can't invalidate certs a peer already trusts.Test plan
bash docs/content/demos/desktop-onboarding-e2e.shpasses: derives from an SSH key, generates mTLS via the app's own code, and creates a room withSHADI_SLIM_AUTH/SLIM_HUMAN_SEED/SLIM_MEMBER_DIDSunset