Repository navigation
Expand file tree
/
Copy pathenvironment.go
More file actions
181 lines (163 loc) · 7.15 KB
/
Copy pathenvironment.go
File metadata and controls
181 lines (163 loc) · 7.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package enginehost
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"os"
"path/filepath"
"sort"
"strings"
"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/env"
"github.com/Agent-Field/codeaf/internal/home"
"github.com/Agent-Field/codeaf/internal/syncsetup"
)
// environmentName stays beside the lock and socket because this answer belongs
// to the host, never to a conversation or a credential sent across its wire.
const environmentName = "environment"
// Sync has a separate fingerprint because attaching with a different relay
// must be refused even when a provider environment change can join busy work.
const syncEnvironmentName = "sync-environment"
// syncUnnamedName is the relay the host would have resolved with no
// CODEAF_SYNC_URL of its own. A launch that names no relay is compared against
// THIS: `codeaf chat --host` runs `codeaf engine` under a non-interactive ssh
// shell that rarely carries the variable, and comparing its silence with the
// variable of the terminal that started the host retired that host, and the
// terminal's next launch retired the ssh one's, back and forth.
const syncUnnamedName = "sync-unnamed"
func syncEnvironment() string { return relayWord(syncsetup.Resolve(home.Dir())) }
func syncUnnamed() string { return relayWord(syncsetup.ResolveUnnamed(home.Dir())) }
func relayWord(relay syncsetup.Relay) string {
if relay.Off {
return "off"
}
return relay.URL
}
// canonicalEnvironment keeps only the environment codeaf actually reads for
// providers and settings. THE HOST'S ADDRESS IS EXCLUDED: two processes finding
// this socket already agreed on the state root, and shell bookkeeping must not
// make an idle host restart. Compatibility spellings collapse to the owned name.
func canonicalEnvironment(workspace string) string {
profileDir := config.ProfilePath(config.ProfileDir(), "")
// PROFILE REFERENCES RESOLVE WHERE THE HOST RUNS. The launcher can stand
// elsewhere, but a daemon chdir must not turn an equal environment into a
// different custom-key list on every launch.
if profileDir != "" && !filepath.IsAbs(profileDir) {
profileDir = filepath.Join(workspace, profileDir)
}
// The variable list is read from the profile at each side's moment.
names := config.ProviderKeyEnvNamesAt(profileDir)
names = append(names, "CODEAF_BASE_URL")
for _, row := range config.NewSettings(config.SettingsOptions{ProfileDir: profileDir}).Rows() {
if row.Env != "" {
names = append(names, row.Env)
}
}
seen := make(map[string]bool, len(names))
var lines []string
for _, name := range names {
if strings.HasPrefix(name, env.Legacy("CODEAF_")) {
name = "CODEAF_" + strings.TrimPrefix(name, env.Legacy("CODEAF_"))
}
if name == "CODEAF_HOME" || name == config.ProfileDirEnv || seen[name] {
continue
}
seen[name] = true
// Value is the dynamic env door: provider names are foreign, while
// owned settings read with exactly Get's compatibility fallback.
if value := env.Value(name); value != "" {
lines = append(lines, name+"="+value)
}
}
sort.Strings(lines)
return strings.Join(lines, "\n")
}
// EnvironmentMAC fingerprints providers, settings and the resolved relay
// under a caller's salt: no value is ever in it, and
// two processes with the same salt and the same keys answer the same bytes.
// The automations clock compares windows with it (cmd/codeaf's chatv3_clock.go).
func EnvironmentMAC(salt []byte, workspace string) []byte { return environmentMAC(salt, workspace) }
func environmentMAC(salt []byte, workspace string) []byte {
return fingerprintMAC(salt, resolvedEnvironment(workspace))
}
// The relay is resolved configuration, separate from the provider/settings
// registry that canonicalEnvironment describes. Both belong to compatibility.
func resolvedEnvironment(workspace string) string {
return canonicalEnvironment(workspace) + "\nCODEAF_SYNC_URL=" + syncEnvironment()
}
func fingerprintMAC(salt []byte, value string) []byte {
mac := hmac.New(sha256.New, salt)
_, _ = mac.Write([]byte(value))
return mac.Sum(nil)
}
// writeEnvironment writes a fresh salt and digest atomically, so a launcher
// never compares a partly written answer. No environment value is saved, even
// when the environment contains a provider key with an unfamiliar shape.
//
// THE PROVIDER FINGERPRINT LEAVES THE RELAY OUT. The relay has its own two
// answers and its own rule ([SyncDiffers]), and folding it in here too would
// let a launch that named no relay restart an idle host through the back door.
func writeEnvironment(dir, workspace string) error {
if err := writeFingerprint(dir, syncEnvironmentName, syncEnvironment()); err != nil {
return err
}
if err := writeFingerprint(dir, syncUnnamedName, syncUnnamed()); err != nil {
return err
}
return writeFingerprint(dir, environmentName, canonicalEnvironment(workspace))
}
func writeFingerprint(dir, name, value string) error {
salt := make([]byte, sha256.Size)
if _, err := rand.Read(salt); err != nil {
return err
}
file, err := os.CreateTemp(dir, ".environment-")
if err != nil {
return err
}
defer os.Remove(file.Name())
if _, err := file.Write(append(salt, fingerprintMAC(salt, value)...)); err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
return os.Rename(file.Name(), filepath.Join(dir, name))
}
// EnvironmentDiffers answers only a complete fingerprint. Missing, unreadable
// or malformed files mean the host has not said, which must never be a reason
// to restart it. Comparison stays local and takes constant time for the digest.
func EnvironmentDiffers(workspace string) bool {
return fingerprintDiffers(workspace, environmentName, canonicalEnvironment(workspace), false) || SyncDiffers(workspace)
}
// SyncDiffers compares the relay the host resolved at startup with this
// terminal's choice, including a pairing's saved address and the off switch.
//
// ONLY A RELAY THIS PROCESS NAMES IS A CHOICE. With CODEAF_SYNC_URL set, the
// whole resolved answer is compared, and an address or off that differs from
// the host's must not be joined. With it unset, this process said nothing
// about the relay, so only what both sides read from the same home — a
// pairing's saved address, else the hosted default — is compared; the
// variable the host's own terminal carried is that terminal's word and stands.
func SyncDiffers(workspace string) bool {
if env.Get(syncsetup.URLVar) != "" {
// A host from before this fingerprint cannot confirm an explicit address
// or off switch. That choice must not fall through to busy-host reuse.
return fingerprintDiffers(workspace, syncEnvironmentName, syncEnvironment(), true)
}
// A host from before the unnamed fingerprint cannot say, and a process that
// named nothing has no choice for it to be ignoring: join it.
return fingerprintDiffers(workspace, syncUnnamedName, syncUnnamed(), false)
}
func fingerprintDiffers(workspace, name, value string, unknown bool) bool {
dir, err := where(workspace)
if err != nil {
return false
}
body, err := os.ReadFile(filepath.Join(dir, name))
if err != nil || len(body) != 2*sha256.Size {
return unknown
}
return !hmac.Equal(body[sha256.Size:], fingerprintMAC(body[:sha256.Size], value))
}