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