Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/init-ux-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,25 @@ uses those keys. Final commit remains the only write boundary for staged secret
values, and the final readiness summary should continue to report follow-up
credential work without leaking values.

All credential-bearing init flows should show equivalent non-secret destination
context before collecting secret values. This includes Git credentials, reviewer
PAT/GitHub App credentials, and LLM API keys handled by the shared credential
collector.

Destination summaries should include:

- credential storage ref
- resolved secrets-management profile label/id when a named profile applies
- resolved backend display label, including platform-specific automatic OS
default copy such as `Automatic OS default (macOS Keychain)`
- configured 1Password vault, item title prefix, item tag, item field title, and
other non-secret routing labels when present

Destination summaries must be non-fatal. If a profile/backend destination cannot
be resolved, the flow should show non-secret unavailable copy and continue to the
existing credential choice. They must never read or display backend token values;
environment variable names may be shown only as backend-auth env var names.

## Draft-Local Reuse Rules

LLM runtimes and reviewer entities are reusable **within the current interactive
Expand Down Expand Up @@ -300,6 +319,9 @@ Instead:
profile is using its Git account or a subscription runtime
- any advanced path should still explain that these labels are non-secret
pointers to keyring entries, not the secrets themselves
- users who need to change where secrets are stored should configure/select a
secrets-management profile rather than entering GitHub App IDs, private keys,
or API keys in the top-level secrets-management workflow

## Global Settings Area

Expand Down
68 changes: 52 additions & 16 deletions internal/cmd/credentialcmd/credentialcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ type initPromptContext struct {
ExistingProfileNames []string
DefaultProfileName string
ExistingConfig config.File
BackendArg string
BackendFlagSet bool
GitScopes map[string]initGitScopeDraft
ProfileGitScopes map[string]string
ReviewerEntities map[string]initReviewerEntityDraft
Expand Down Expand Up @@ -633,6 +635,7 @@ const (

type initCredentialSecretPrompt struct {
Entry initCredentialPlanEntry
Destination string
TargetHasRequired bool
TargetHasAnyKeys bool
ClipboardSupported bool
Expand All @@ -641,6 +644,7 @@ type initCredentialSecretPrompt struct {
type initSecretValuePrompt struct {
Entry initCredentialPlanEntry
Key string
Destination string
Optional bool
TargetHasKey bool
ClipboardSupported bool
Expand Down Expand Up @@ -3325,13 +3329,18 @@ func (p huhInitSecretPrompter) ChooseCredentialAction(prompt initCredentialSecre
huh.NewOption("Back to main menu", initCredentialSecretActionBack),
)
choice := options[0].Value
fields := []huh.Field{}
if destination := strings.TrimSpace(prompt.Destination); destination != "" {
fields = append(fields, huh.NewNote().Description(destination))
}
fields = append(fields,
huh.NewSelect[initCredentialSecretAction]().
Title(initCredentialSecretPromptTitle(prompt)).
Options(options...).
Value(&choice),
)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[initCredentialSecretAction]().
Title(initCredentialSecretPromptTitle(prompt)).
Options(options...).
Value(&choice),
),
huh.NewGroup(fields...),
)
back, err := runBackableInitForm(form, p.stdin, p.stderr)
if err != nil {
Expand Down Expand Up @@ -3373,13 +3382,18 @@ func (p huhInitSecretPrompter) ChooseSecretSource(prompt initSecretValuePrompt)
}
options = append(options, huh.NewOption("Back to credential choices", initSecretSourceBack))
choice := options[0].Value
fields := []huh.Field{}
if destination := strings.TrimSpace(prompt.Destination); destination != "" {
fields = append(fields, huh.NewNote().Description(destination))
}
fields = append(fields,
huh.NewSelect[initSecretSource]().
Title(initSecretSourcePromptTitle(prompt)).
Options(options...).
Value(&choice),
)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[initSecretSource]().
Title(initSecretSourcePromptTitle(prompt)).
Options(options...).
Value(&choice),
),
huh.NewGroup(fields...),
)
back, err := runBackableInitForm(form, p.stdin, p.stderr)
if err != nil {
Expand All @@ -3400,13 +3414,10 @@ func (p huhInitSecretPrompter) PasteSecret(prompt initSecretValuePrompt) (string
action := initDetailActionEdit
field := huh.NewInput().
Title(fmt.Sprintf("Paste %s%s", prompt.Key, initSecretsProfilePromptSuffix(prompt.Entry.SecretsProfile))).
Description("Secret input is hidden.").
Description(initSecretPasteDescription(prompt)).
Value(&value).
EchoMode(huh.EchoModePassword).
Validate(validateRequiredText("secret value is required"))
if prompt.Key == credentials.GitHubAppPrivateKeyKey {
field.Description("Secret input is hidden. Clipboard is recommended for multi-line private keys.")
}
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Expand Down Expand Up @@ -5742,10 +5753,21 @@ func collectInteractiveInitSecrets(_ *cobra.Command, opts *root.Options, deps in
if initCredentialEntryDeferredByDecision(plan.credentialDecisions, entry) {
continue
}
destinationBackend := ""
if opts != nil {
destinationBackend = opts.Backend
}
destination := initCredentialDestinationDescription(initCredentialDestinationContext{
Entry: entry,
Config: plan.cfg,
BackendArg: destinationBackend,
BackendFlagSet: plan.backendFlagSet,
})
credentialChoices:
for {
action, err := prompter.ChooseCredentialAction(initCredentialSecretPrompt{
Entry: entry,
Destination: destination,
ClipboardSupported: deps.clipboardSupported(),
})
if err != nil {
Expand Down Expand Up @@ -5778,6 +5800,7 @@ func collectInteractiveInitSecrets(_ *cobra.Command, opts *root.Options, deps in
if action == initCredentialSecretActionSetNow && targetHasAnyKeys {
action, err = prompter.ChooseCredentialAction(initCredentialSecretPrompt{
Entry: entry,
Destination: destination,
TargetHasRequired: targetHasRequired,
TargetHasAnyKeys: targetHasAnyKeys,
ClipboardSupported: deps.clipboardSupported(),
Expand Down Expand Up @@ -5818,6 +5841,7 @@ func collectInteractiveInitSecrets(_ *cobra.Command, opts *root.Options, deps in
source, err := prompter.ChooseSecretSource(initSecretValuePrompt{
Entry: entry,
Key: spec.Key,
Destination: destination,
Optional: !spec.Required,
TargetHasKey: targetHasKey,
ClipboardSupported: deps.clipboardSupported(),
Expand Down Expand Up @@ -5852,6 +5876,7 @@ func collectInteractiveInitSecrets(_ *cobra.Command, opts *root.Options, deps in
value, err := prompter.PasteSecret(initSecretValuePrompt{
Entry: entry,
Key: spec.Key,
Destination: destination,
Optional: !spec.Required,
TargetHasKey: targetHasKey,
ClipboardSupported: deps.clipboardSupported(),
Expand Down Expand Up @@ -6811,6 +6836,17 @@ func readSecretIngress(r io.Reader, stdin bool, envVar, stdinFlag, envFlag strin
return value, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-self-documenting-code-reviewer): In initSecretPasteDescription, the variable description changes semantic role mid-function: it starts as the hidden-input notice text and ends as the full composed display string. Assigning the notice to a named variable like hiddenInputNotice and composing the return value explicitly (e.g. return destination + "\n" + hiddenInputNotice) would make the combination unambiguous and easier to follow.

Reply to this thread when addressed.


func initSecretPasteDescription(prompt initSecretValuePrompt) string {
hiddenInputNotice := "Secret input is hidden."
if prompt.Key == credentials.GitHubAppPrivateKeyKey {
hiddenInputNotice = "Secret input is hidden. Clipboard is recommended for multi-line private keys."
}
if destination := strings.TrimSpace(prompt.Destination); destination != "" {
return destination + "\n" + hiddenInputNotice
}
return hiddenInputNotice
}

func readOptionalSecretIngress(r io.Reader, stdin bool, envVar, stdinFlag, envFlag string) (string, bool, error) {
if stdin && envVar != "" {
return "", false, fmt.Errorf("only one of %s or %s may be set", stdinFlag, envFlag)
Expand Down
Loading
Loading