feat(vault): manage links for read-through external vaults - #419
Merged
Conversation
External vaults hold links -- a key you choose, paired with a path the provider understands -- rather than secret material. This adds the commands and views for working with them. `flow secret link NAME REFERENCE` and `flow secret unlink NAME`. `set` refuses on a linked vault and names `link` in the message, and it checks before collecting a value: prompting someone to type a secret only to reject it, or taking one already typed, is a poor way to deliver that news. `remove` still works and now says what it actually does -- the prompt and the success message both say the secret itself is not deleted, which meant opening the vault before prompting rather than after. Listing a read-through vault no longer resolves every key. It used to call GetSecret per entry even when the values were about to be masked, which on a linked vault is one provider command per row -- for 1Password, one biometric prompt per row. NewSecretList now takes a resolve flag and shows the reference instead, which is the useful part anyway. Verified by listing with GPG deliberately unreachable: it succeeds. That listing also used to swallow GetSecret errors and skip the entry, so a link whose target had been deleted in the provider silently vanished rather than showing as something to fix. Broken links now stay in the list and render as broken in the detail view. The secret detail view drops rename and edit for linked secrets -- both wrote secret material -- and offers re-link instead. Delete is labelled unlink. NewExternalVault fills in a storage path for the link registry, matching where every other provider keeps its state. A config authored elsewhere has no business knowing flow's cache layout. go.mod is deliberately not in this commit: it needs vault v0.4.0, which is not tagged yet, and the branch carries a local replace meanwhile. This commit does not build on its own -- the dependency bump is the next one. Verified end to end against a real pass store: created an external vault, linked team/db/password (a nested path the old design could not express as a key), read it back, listed it, unlinked it, and confirmed the .gpg file and its value were untouched. A reference containing $(id) is refused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A UI that wants to browse the provider again later needs the preset the vault was created from and the values it was created with -- its region, its 1Password vault name. Without them it has to ask the user to restate things they already entered once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
NewLinkedSecret decorated NewSecret's result by asserting back to the concrete type, which errcheck flags as an unchecked assertion. It now builds the struct directly and NewSecret delegates to it. views.go no longer needs the cyclop suppression -- moving the read-through views into linked.go took the branching with them. The secrets guide still described external vaults as writable stores. Rewritten around links: how references look per provider, that one 1Password item becomes several links, and that set/remove no longer do what they used to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jahvon
marked this pull request as ready for review
July 27, 2026 00:46
jahvon
added a commit
that referenced
this pull request
Jul 27, 2026
Follow-up to #419. Two bugs that together made the desktop's vault dropdown come up empty and disabled. ## An external vault created before v0.4.0 cannot be opened Those configs have no `storage_path`, and `NewExternalVaultProvider` now requires one. The default was applied when *creating* a vault but not when *opening* one, so every pre-existing external vault broke on upgrade ## One unopenable vault hid every other one `PrintVaultList` called `Fatalf` on the first vault it could not open, so a single bad vault produced no output at all rather than one bad row. The TUI list view did the same via `NewErrorView`. A vault that will not open is now listed with an `error` field, and the rest still load. This matters beyond the bug above — a missing key env var or a revoked age identity would have blanked the whole list the same way.
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.
Companion to flowexec/vault#21, which turns external vaults into read-through reference registries: they hold links — a key you choose, paired with a path the provider understands — rather than secret material. This is the flow side.
Commands
flow secret link NAME REFERENCEandflow secret unlink NAME.setrefuses on a linked vault and nameslinkin the message. It checks before collecting a value — prompting someone to type a secret only to reject it, or taking one already typed, is a poor way to deliver that news.removestill works and now says what it actually does. Both the confirmation prompt and the success message state that the secret itself is not deleted, which meant opening the vault before prompting rather than after: what removal means depends on the vault.Listing no longer resolves every secret
NewSecretListcalledGetSecretper entry even when the values were about to be masked. On a linked vault that is one provider command per row — for 1Password, potentially one biometric prompt per row — to render a column of identical dots. It now takes aresolveflag and shows the reference instead, which is the part worth reading.Verified by listing with GPG deliberately unreachable: it succeeds.
That same loop also swallowed
GetSecreterrors and skipped the entry, so a link whose target had been deleted in the provider silently vanished from the listing rather than showing as something to fix. Broken links now stay in the list and render as broken in the detail view.Views
The secret detail view drops rename and edit for linked secrets — both wrote secret material — and offers re-link instead. Delete is labelled unlink. The list view shows
Name → Reference.The vault view reports that an external vault is read-only, how many links it holds, which preset produced it and with what values, and names any inert write-era commands left in an older config.
Storage path
NewExternalVaultfills in a storage path for the link registry, matching where every other provider keeps its state. A config authored elsewhere — rendered from a preset, or written by hand — has no business knowing flow's cache layout, so it arrives without one.Deliberately unchanged
SecretRef,RefToParts,ValidateIdentifier,ValidateSecretKey.Because the key is a local alias and the path lives in the link, a key never needs to contain
/. That matters:RefToPartsrejects a 3-segment ref outright, andSecretRef("v/a/b/c").Key()silently returns"a". An earlier iteration of this work was heading toward loosening both. It turned out to be unnecessary.Verification
go build ./...and the fullgo test ./internal/... ./cmd/...suite pass locally against the vault branch.End to end against a real
passstore: created an external vault, linkedteam/db/password(a nested path the old design could not express as a key), read it back, listed it, unlinked it, and confirmed the.gpgfile and its value were untouched. A reference containing$(id)is refused.Blocked on
go.modoff the localreplacethis was developed againstgo.modis deliberately absent from these commits —gomoddirectivesforbids committing the replace, and the dependency does not exist yet. The bump is the next commit, and this leaves draft when it lands.🤖 Generated with Claude Code