carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "crypto/hmac"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "io"
11 "sort"
12 "strings"
13
14 "golang.org/x/crypto/hkdf"
15)
16
17const RegionVar = "CARLOS_REGION"
18
19// secretsKeyLen is the length of CARLOS_SECRETS_KEY's raw bytes: a P-256
20// private scalar (internal/appconfig GenerateKey).
21const secretsKeyLen = 32
22
23var (
24 // LayoutVars name a channel the flat layout always provides: present
25 // on both sides of a flat move, values not compared (they are paths
26 // on each box).
27 LayoutVars = map[string]bool{"CARLOS_CONTROL_SOCKET": true}
28 // AdvisoryVars follow a box fact (CARLOS_PURGE is set iff the box has
29 // a response cache, ExecBackend.agentOwnedVars): a presence difference
30 // is reported as a note, never a failure (round-4 finding 10). The
31 // spec's T3 table puts CARLOS_PURGE in the layout class; the plan
32 // deliberately makes it advisory, and the spec ledger records that.
33 AdvisoryVars = map[string]bool{"CARLOS_PURGE": true}
34 // BoxVars are the per-process variables of a box's own shell or
35 // systemd unit, and the box facts its unit's environment carries,
36 // never compared. The agent hands instances its own environment bar
37 // the CARLOS_ names, so every non-CARLOS_ variable of carlos-edge's
38 // process is here unless it is the same on every box. Read from the
39 // agent process on dev and flagship boxes, 2026-10-20: systemd 257
40 // adds SYSTEMD_EXEC_PID and MEMORY_PRESSURE_*, and the units set
41 // AWS_REGION, STATUS_BOX_REGION, PLATFORM_* and, on a console box,
42 // CONSOLE_*. HOME, LANG, LOGNAME, PATH and USER are the same on every
43 // box and stay pinned. The rest of systemd's per-process set is here
44 // so a unit that gains one cannot fail every move.
45 BoxVars = map[string]bool{
46 "INVOCATION_ID": true, "JOURNAL_STREAM": true, "HOSTNAME": true, "PWD": true, "OLDPWD": true, "SHLVL": true, "_": true,
47 "SYSTEMD_EXEC_PID": true, "MEMORY_PRESSURE_WATCH": true, "MEMORY_PRESSURE_WRITE": true,
48 "NOTIFY_SOCKET": true, "WATCHDOG_PID": true, "WATCHDOG_USEC": true, "MAINPID": true,
49 "LISTEN_PID": true, "LISTEN_FDS": true, "LISTEN_FDNAMES": true,
50 "RUNTIME_DIRECTORY": true, "STATE_DIRECTORY": true, "CACHE_DIRECTORY": true, "LOGS_DIRECTORY": true,
51 "CONFIGURATION_DIRECTORY": true, "CREDENTIALS_DIRECTORY": true,
52 "AWS_REGION": true, "STATUS_BOX_REGION": true, "PLATFORM_EDGE_IPS": true, "PLATFORM_APPS_DOMAIN": true,
53 "CONSOLE_DENY_DOMAINS": true, "CONSOLE_HOME_REGION": true,
54 }
55 // AgentOwnedVars are the variables the agent sets, overwrites or
56 // removes on every launch (activator ExecBackend.agentOwnedVars): the
57 // control socket, the purge channel, the box's region and the host
58 // row's name. A tenant's env file never decides them, and each box sets
59 // its own, so they are box-owned (BoxOwned).
60 // TestAgentOwnedVarsAreMovesBoxOwned (internal/activator) pins this set
61 // against agentOwnedVars.
62 AgentOwnedVars = map[string]bool{RegionVar: true, "CARLOS_CONTROL_SOCKET": true, "CARLOS_PURGE": true, "CARLOS_HOST": true}
63)
64
65// BoxOwned reports a variable whose value can be the box's rather than the
66// instance's: an agent-owned variable, always the box's, or a box variable
67// of the box's own process, the box's unless the instance's own layers
68// supply it (Pinned). It is the one set every comparison of a move's
69// environment across two boxes may leave out (home part 2, Task 2b): T3
70// tolerates these (Class is not "equal") unless an instance supplied
71// them, and the env pin covers every variable Pinned keeps.
72func BoxOwned(name string) bool { return AgentOwnedVars[name] || BoxVars[name] }
73
74// Pinned reports whether a variable is part of the move-portable env, the
75// env the move's pin covers on the source and the target's checks hash
76// (fix round 4, platform-1e's exclusion rule): a variable is left out only
77// when it is box-owned (BoxOwned) and the instance's own layers did not
78// supply the value that runs (instanceSupplied false). Everything else is
79// pinned. The composer reports as instance-supplied only the names whose
80// running value is the instance's (activator ExecBackend.LaunchLayers): an
81// agent-owned variable never is, since the agent sets or removes it
82// whatever the files say, and a box variable is when the instance's files
83// set it, since the agent never overwrites one.
84func Pinned(name string, instanceSupplied bool) bool {
85 return !BoxOwned(name) || instanceSupplied
86}
87
88// ReportKeyFromSecrets derives the report's HMAC key from
89// CARLOS_SECRETS_KEY (HKDF-SHA256), so the report reveals names and
90// equality only and only to boxes holding the deployment's key (§7.3).
91// The input must be the base64 of exactly 32 raw bytes; anything else is
92// an error, and the error never carries the input.
93func ReportKeyFromSecrets(secretsKeyB64 string) ([]byte, error) {
94 raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(secretsKeyB64))
95 if err != nil {
96 return nil, fmt.Errorf("moves: secrets key: %w", err)
97 }
98 if len(raw) != secretsKeyLen {
99 return nil, errors.New("moves: secrets key: not a 32-byte key")
100 }
101 key := make([]byte, 32)
102 if _, err := io.ReadFull(hkdf.New(sha256.New, raw, nil, []byte("carlos-move-env-report-v1")), key); err != nil {
103 return nil, err
104 }
105 return key, nil
106}
107
108type EnvReport struct {
109 V int `json:"v"`
110 Vars map[string]string `json:"vars"`
111 Digest string `json:"digest"`
112 // Instance names the box-owned variables (BoxOwned) whose running value
113 // the instance's own layers supplied, sorted: the box-owned variables
114 // the report pins and T3 compares. Empty means every box-owned variable
115 // in Vars is the box's.
116 Instance []string `json:"instance,omitempty"`
117}
118
119// instanceSupplied reports whether the instance's own layers supplied the
120// box-owned variable name in this report.
121func (r EnvReport) instanceSupplied(name string) bool {
122 for _, n := range r.Instance {
123 if n == name {
124 return true
125 }
126 }
127 return false
128}
129
130// ReportLaunch HMACs every value of a composed launch environment (bound to
131// its name). instance is the names the instance's own layers supplied
132// (activator ExecBackend.LaunchLayers); nil means none of the box
133// variables were. Vars carries every variable, for T3's comparison by
134// name; Instance the box variables the instance supplied; Digest the
135// move-portable env, every variable Pinned keeps, so the source's pin and
136// the target's composition of the same instance hash the same
137// (PortableDigest). Raw values never appear in the result.
138func ReportLaunch(key []byte, env map[string]string, instance map[string]bool) EnvReport {
139 r := EnvReport{V: EnvReportVersion, Vars: make(map[string]string, len(env))}
140 for k, v := range env {
141 r.Vars[k] = macVar(key, k, v)
142 if BoxOwned(k) && instance[k] {
143 r.Instance = append(r.Instance, k)
144 }
145 }
146 sort.Strings(r.Instance)
147 r.Digest = r.portableDigest()
148 return r
149}
150
151// ReportEnv is ReportLaunch for an environment no instance layer supplied
152// a box variable to.
153func ReportEnv(key []byte, env map[string]string) EnvReport {
154 return ReportLaunch(key, env, nil)
155}
156
157// PortableDigest is the digest of a composed launch environment's
158// move-portable part: ReportLaunch's Digest, the move's env pin.
159func PortableDigest(key []byte, env map[string]string, instance map[string]bool) string {
160 return ReportLaunch(key, env, instance).Digest
161}
162
163// LocalDigest is the digest of a whole composed environment, every
164// variable, box-owned or not: the digest of a comparison made on one box
165// against a record that box made (the recovery's inputs), where the box's
166// own variables are part of what runs and must not change either.
167func LocalDigest(key []byte, env map[string]string) string {
168 vars := make(map[string]string, len(env))
169 for k, v := range env {
170 vars[k] = macVar(key, k, v)
171 }
172 return "sha256:" + digestOf(vars, func(string) bool { return true })
173}
174
175func macVar(key []byte, name, value string) string {
176 mac := hmac.New(sha256.New, key)
177 mac.Write([]byte(name))
178 mac.Write([]byte{0})
179 mac.Write([]byte(value))
180 return hex.EncodeToString(mac.Sum(nil))
181}
182
183// portableDigest hashes the reported variables Pinned keeps, in the v2
184// pin's form.
185func (r EnvReport) portableDigest() string {
186 return envPinPrefix + digestOf(r.Vars, func(k string) bool { return Pinned(k, r.instanceSupplied(k)) })
187}
188
189// EnvReportVersion is the env report's and the env pin's contract
190// (home part 2, Task 2b fix round 3). Version 2 names the instance's layer
191// names (Instance) and pins a box variable only when the instance supplied
192// it (Pinned). A version 1 report carries no layer names, so an absent
193// Instance cannot tell an older producer, whose box variables were never
194// pinned, from a newer one with none instance-set: a v2 participant
195// refuses a v1 report at T3 and a v1 pin at the probe (CurrentEnvReport,
196// CurrentEnvPin), and a v2 pin is in a form (envPinPrefix) no v1
197// digest, "sha256:<hex>", can equal. This is the report's version, not
198// ProtocolVersion.
199const EnvReportVersion = 2
200
201// envPinPrefix marks a v2 env pin.
202const envPinPrefix = "env2:sha256:"
203
204// CurrentEnvPin reports whether pin is a v2 env pin, the only form this
205// release checks a launch against.
206func CurrentEnvPin(pin string) bool { return strings.HasPrefix(pin, envPinPrefix) }
207
208// CurrentEnvReport reports whether r is a v2 env report: its version and
209// its digest's form both say so.
210func CurrentEnvReport(r EnvReport) bool { return r.V == EnvReportVersion && CurrentEnvPin(r.Digest) }
211
212func digestOf(vars map[string]string, keep func(string) bool) string {
213 names := make([]string, 0, len(vars))
214 for k := range vars {
215 if keep(k) {
216 names = append(names, k)
217 }
218 }
219 sort.Strings(names)
220 h := sha256.New()
221 for _, k := range names {
222 fmt.Fprintf(h, "%s=%s\n", k, vars[k])
223 }
224 return hex.EncodeToString(h.Sum(nil))
225}
226
227// Class is T3's variable class by name. Every BoxOwned variable has a
228// class other than "equal", and only those do. The agent-owned variables
229// without a rule of their own (CARLOS_HOST, the row's name) are box
230// variables. CompareEnv compares a box variable an instance supplied as
231// "equal" (classIn), as the pin covers it.
232func Class(name string) string {
233 switch {
234 case name == RegionVar:
235 return "region"
236 case LayoutVars[name]:
237 return "layout"
238 case AdvisoryVars[name]:
239 return "advisory"
240 case BoxOwned(name):
241 return "box"
242 default:
243 return "equal"
244 }
245}
246
247type EnvDiff struct {
248 Name, Kind string
249}
250
251// classIn is Class in a comparison of two reports: a box-owned variable
252// either side's instance supplied is the instance's, compared as "equal",
253// as the pin covers it (Pinned).
254func classIn(name string, source, target EnvReport) string {
255 if !BoxOwned(name) || !(source.instanceSupplied(name) || target.instanceSupplied(name)) {
256 return Class(name)
257 }
258 return "equal"
259}
260
261// Note reports a diff T3 records without failing.
262func (d EnvDiff) Note() bool { return d.Kind == "advisory-differs" }
263
264// CompareEnv is T3: region must differ, layout variables must be present
265// iff the flat layout provides them (value not compared), advisory
266// variables report a presence difference as a note (EnvDiff.Note), box
267// variables are not compared, everything else must be equal by HMAC. T3
268// fails on any diff that is not a note.
269//
270// targetHome is a move into the home placement (the record's move names
271// the home's code, ""). A home box sets no CARLOS_REGION (the agent adds it
272// only when the box has a region), so for a home target the rule turns
273// round: the target must not carry it, whatever the source carries, and a
274// target that does is not the home box (region-present). For a named
275// target the rule is unchanged: it must set its own, different from the
276// source's.
277func CompareEnv(source, target EnvReport, flatLayout, targetHome bool) []EnvDiff {
278 var diffs []EnvDiff
279 seen := map[string]bool{}
280 for name, s := range source.Vars {
281 seen[name] = true
282 tv, ok := target.Vars[name]
283 switch classIn(name, source, target) {
284 case "region":
285 if targetHome {
286 if ok {
287 diffs = append(diffs, EnvDiff{name, "region-present"}) // a home box sets no region
288 }
289 } else if !ok {
290 diffs = append(diffs, EnvDiff{name, "region-missing"}) // the target must set its own region
291 } else if tv == s {
292 diffs = append(diffs, EnvDiff{name, "region-same"})
293 }
294 case "layout":
295 if flatLayout && !ok {
296 diffs = append(diffs, EnvDiff{name, "layout-missing"})
297 }
298 if !flatLayout && ok {
299 diffs = append(diffs, EnvDiff{name, "layout-present"})
300 }
301 case "advisory":
302 if !ok {
303 diffs = append(diffs, EnvDiff{name, "advisory-differs"})
304 }
305 case "box":
306 default:
307 if !ok {
308 diffs = append(diffs, EnvDiff{name, "missing"})
309 } else if tv != s {
310 diffs = append(diffs, EnvDiff{name, "differs"})
311 }
312 }
313 }
314 for name := range target.Vars {
315 if seen[name] {
316 continue
317 }
318 switch classIn(name, source, target) {
319 case "region":
320 // a target-only region is a named target's own (a move out of
321 // home); on a home target it is a box that is not the home box
322 if targetHome {
323 diffs = append(diffs, EnvDiff{name, "region-present"})
324 }
325 case "equal":
326 diffs = append(diffs, EnvDiff{name, "extra"})
327 case "advisory":
328 diffs = append(diffs, EnvDiff{name, "advisory-differs"})
329 case "layout":
330 // present iff the flat layout provides it: a target-only
331 // layout variable is wrong on a non-flat landing
332 if !flatLayout {
333 diffs = append(diffs, EnvDiff{name, "layout-present"})
334 }
335 }
336 }
337 // layout names absent from BOTH reports are still checked: a flat
338 // landing must provide every layout variable the flat layout provides
339 for name := range LayoutVars {
340 if _, inSrc := source.Vars[name]; inSrc {
341 continue // handled above
342 }
343 if _, inTgt := target.Vars[name]; flatLayout && !inTgt {
344 diffs = append(diffs, EnvDiff{name, "layout-missing"})
345 }
346 }
347 sort.Slice(diffs, func(i, j int) bool { return diffs[i].Name < diffs[j].Name })
348 return diffs
349}
350