返回 DeepSeek-Reasonix
remote_credentials.go
根目录 / internal / config / remote_credentials.go
1 package config
2
3 import (
4 "crypto/sha256"
5 "fmt"
6 "strings"
7 )
8
9 const (
10 remotePasswordCredentialKind = "PASSWORD"
11 remotePassphraseCredentialKind = "KEY_PASSPHRASE"
12 )
13
14 // CredentialChange is one mutation in the user-global credential store. The
15 // value is never written into config.toml; only its environment-key reference
16 // belongs in a RemoteHostEntry.
17 type CredentialChange struct {
18 Key string
19 Value string
20 Remove bool
21 }
22
23 type remoteCredentialSnapshot struct {
24 value string
25 set bool
26 }
27
28 // RemotePasswordCredentialEnvName returns the Reasonix-owned credential slot
29 // for a remote host password without exposing the user-supplied label.
30 func RemotePasswordCredentialEnvName(hostID string) string {
31 return remoteCredentialEnvName(hostID, remotePasswordCredentialKind)
32 }
33
34 // RemotePassphraseCredentialEnvName returns the Reasonix-owned credential slot
35 // for a remote host private-key passphrase.
36 func RemotePassphraseCredentialEnvName(hostID string) string {
37 return remoteCredentialEnvName(hostID, remotePassphraseCredentialKind)
38 }
39
40 func remoteCredentialEnvName(hostID, kind string) string {
41 sum := sha256.Sum256([]byte(strings.TrimSpace(hostID)))
42 return fmt.Sprintf("REASONIX_REMOTE_%X_%s", sum[:8], kind)
43 }
44
45 // IsGeneratedRemoteCredential reports whether key is a Reasonix-owned slot
46 // for this host. User-managed/shared environment variables are never deleted.
47 func IsGeneratedRemoteCredential(hostID, key string) bool {
48 key = strings.TrimSpace(key)
49 return key != "" && (key == RemotePasswordCredentialEnvName(hostID) ||
50 key == RemotePassphraseCredentialEnvName(hostID))
51 }
52
53 // UnusedGeneratedRemoteCredentialChanges returns deduplicated removals for
54 // candidates that are no longer referenced by any configured remote host.
55 func UnusedGeneratedRemoteCredentialChanges(c *Config, candidates []string) []CredentialChange {
56 if c == nil || len(candidates) == 0 {
57 return nil
58 }
59 used := make(map[string]bool, len(c.Remote.Hosts)*2)
60 for _, host := range c.Remote.Hosts {
61 used[strings.TrimSpace(host.PasswordEnv)] = true
62 used[strings.TrimSpace(host.PassphraseEnv)] = true
63 }
64 seen := map[string]bool{}
65 changes := make([]CredentialChange, 0, len(candidates))
66 for _, key := range candidates {
67 key = strings.TrimSpace(key)
68 if key == "" || used[key] || seen[key] {
69 continue
70 }
71 seen[key] = true
72 changes = append(changes, CredentialChange{Key: key, Remove: true})
73 }
74 return changes
75 }
76
77 // EditUserConfigWithCredentials updates config and its Reasonix-owned secret
78 // slots as one recoverable operation. Credential writes happen before SaveTo;
79 // any later failure restores every touched slot, keeping plaintext out of TOML.
80 func EditUserConfigWithCredentials(mutate func(*Config) ([]CredentialChange, error)) error {
81 unlock := LockUserConfigEdits()
82 defer unlock()
83 path := UserConfigPath()
84 if strings.TrimSpace(path) == "" {
85 return fmt.Errorf("cannot resolve user config path")
86 }
87 cfg := LoadForEdit(path)
88 if cfg == nil {
89 cfg = Default()
90 }
91 changes, err := mutate(cfg)
92 if err != nil {
93 return err
94 }
95 snapshots := map[string]remoteCredentialSnapshot{}
96 applied := make([]string, 0, len(changes))
97 rollback := func() {
98 seen := map[string]bool{}
99 for i := len(applied) - 1; i >= 0; i-- {
100 key := applied[i]
101 if seen[key] {
102 continue
103 }
104 seen[key] = true
105 snapshot := snapshots[key]
106 if snapshot.set {
107 _, _ = SetCredential(key, snapshot.value)
108 } else {
109 _ = RemoveCredential(key)
110 }
111 }
112 }
113 for _, change := range changes {
114 change.Key = strings.TrimSpace(change.Key)
115 if change.Key == "" {
116 continue
117 }
118 if _, ok := snapshots[change.Key]; !ok {
119 resolved := ResolveCredentialForRootGlobalFirst(".", change.Key)
120 snapshots[change.Key] = remoteCredentialSnapshot{value: resolved.Value, set: resolved.Set}
121 }
122 if change.Remove {
123 err = RemoveCredential(change.Key)
124 } else {
125 _, err = SetCredential(change.Key, change.Value)
126 }
127 if err != nil {
128 rollback()
129 return fmt.Errorf("update remote credential %s: %w", change.Key, err)
130 }
131 applied = append(applied, change.Key)
132 }
133 if err := cfg.SaveTo(path); err != nil {
134 rollback()
135 return err
136 }
137 return nil
138 }
139
139 lines GO