package moves import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "fmt" "io" "sort" "strings" "golang.org/x/crypto/hkdf" ) const RegionVar = "CARLOS_REGION" // secretsKeyLen is the length of CARLOS_SECRETS_KEY's raw bytes: a P-256 // private scalar (internal/appconfig GenerateKey). const secretsKeyLen = 32 var ( // LayoutVars name a channel the flat layout always provides: present // on both sides of a flat move, values not compared (they are paths // on each box). LayoutVars = map[string]bool{"CARLOS_CONTROL_SOCKET": true} // AdvisoryVars follow a box fact (CARLOS_PURGE is set iff the box has // a response cache, ExecBackend.agentOwnedVars): a presence difference // is reported as a note, never a failure (round-4 finding 10). The // spec's T3 table puts CARLOS_PURGE in the layout class; the plan // deliberately makes it advisory, and the spec ledger records that. AdvisoryVars = map[string]bool{"CARLOS_PURGE": true} // BoxVars are the per-process variables of a box's own shell or // systemd unit, and the box facts its unit's environment carries, // never compared. The agent hands instances its own environment bar // the CARLOS_ names, so every non-CARLOS_ variable of carlos-edge's // process is here unless it is the same on every box. Read from the // agent process on dev and flagship boxes, 2026-10-20: systemd 257 // adds SYSTEMD_EXEC_PID and MEMORY_PRESSURE_*, and the units set // AWS_REGION, STATUS_BOX_REGION, PLATFORM_* and, on a console box, // CONSOLE_*. HOME, LANG, LOGNAME, PATH and USER are the same on every // box and stay pinned. The rest of systemd's per-process set is here // so a unit that gains one cannot fail every move. BoxVars = map[string]bool{ "INVOCATION_ID": true, "JOURNAL_STREAM": true, "HOSTNAME": true, "PWD": true, "OLDPWD": true, "SHLVL": true, "_": true, "SYSTEMD_EXEC_PID": true, "MEMORY_PRESSURE_WATCH": true, "MEMORY_PRESSURE_WRITE": true, "NOTIFY_SOCKET": true, "WATCHDOG_PID": true, "WATCHDOG_USEC": true, "MAINPID": true, "LISTEN_PID": true, "LISTEN_FDS": true, "LISTEN_FDNAMES": true, "RUNTIME_DIRECTORY": true, "STATE_DIRECTORY": true, "CACHE_DIRECTORY": true, "LOGS_DIRECTORY": true, "CONFIGURATION_DIRECTORY": true, "CREDENTIALS_DIRECTORY": true, "AWS_REGION": true, "STATUS_BOX_REGION": true, "PLATFORM_EDGE_IPS": true, "PLATFORM_APPS_DOMAIN": true, "CONSOLE_DENY_DOMAINS": true, "CONSOLE_HOME_REGION": true, } // AgentOwnedVars are the variables the agent sets, overwrites or // removes on every launch (activator ExecBackend.agentOwnedVars): the // control socket, the purge channel, the box's region and the host // row's name. A tenant's env file never decides them, and each box sets // its own, so they are box-owned (BoxOwned). // TestAgentOwnedVarsAreMovesBoxOwned (internal/activator) pins this set // against agentOwnedVars. AgentOwnedVars = map[string]bool{RegionVar: true, "CARLOS_CONTROL_SOCKET": true, "CARLOS_PURGE": true, "CARLOS_HOST": true} ) // BoxOwned reports a variable whose value can be the box's rather than the // instance's: an agent-owned variable, always the box's, or a box variable // of the box's own process, the box's unless the instance's own layers // supply it (Pinned). It is the one set every comparison of a move's // environment across two boxes may leave out (home part 2, Task 2b): T3 // tolerates these (Class is not "equal") unless an instance supplied // them, and the env pin covers every variable Pinned keeps. func BoxOwned(name string) bool { return AgentOwnedVars[name] || BoxVars[name] } // Pinned reports whether a variable is part of the move-portable env, the // env the move's pin covers on the source and the target's checks hash // (fix round 4, platform-1e's exclusion rule): a variable is left out only // when it is box-owned (BoxOwned) and the instance's own layers did not // supply the value that runs (instanceSupplied false). Everything else is // pinned. The composer reports as instance-supplied only the names whose // running value is the instance's (activator ExecBackend.LaunchLayers): an // agent-owned variable never is, since the agent sets or removes it // whatever the files say, and a box variable is when the instance's files // set it, since the agent never overwrites one. func Pinned(name string, instanceSupplied bool) bool { return !BoxOwned(name) || instanceSupplied } // ReportKeyFromSecrets derives the report's HMAC key from // CARLOS_SECRETS_KEY (HKDF-SHA256), so the report reveals names and // equality only and only to boxes holding the deployment's key (ยง7.3). // The input must be the base64 of exactly 32 raw bytes; anything else is // an error, and the error never carries the input. func ReportKeyFromSecrets(secretsKeyB64 string) ([]byte, error) { raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(secretsKeyB64)) if err != nil { return nil, fmt.Errorf("moves: secrets key: %w", err) } if len(raw) != secretsKeyLen { return nil, errors.New("moves: secrets key: not a 32-byte key") } key := make([]byte, 32) if _, err := io.ReadFull(hkdf.New(sha256.New, raw, nil, []byte("carlos-move-env-report-v1")), key); err != nil { return nil, err } return key, nil } type EnvReport struct { V int `json:"v"` Vars map[string]string `json:"vars"` Digest string `json:"digest"` // Instance names the box-owned variables (BoxOwned) whose running value // the instance's own layers supplied, sorted: the box-owned variables // the report pins and T3 compares. Empty means every box-owned variable // in Vars is the box's. Instance []string `json:"instance,omitempty"` } // instanceSupplied reports whether the instance's own layers supplied the // box-owned variable name in this report. func (r EnvReport) instanceSupplied(name string) bool { for _, n := range r.Instance { if n == name { return true } } return false } // ReportLaunch HMACs every value of a composed launch environment (bound to // its name). instance is the names the instance's own layers supplied // (activator ExecBackend.LaunchLayers); nil means none of the box // variables were. Vars carries every variable, for T3's comparison by // name; Instance the box variables the instance supplied; Digest the // move-portable env, every variable Pinned keeps, so the source's pin and // the target's composition of the same instance hash the same // (PortableDigest). Raw values never appear in the result. func ReportLaunch(key []byte, env map[string]string, instance map[string]bool) EnvReport { r := EnvReport{V: EnvReportVersion, Vars: make(map[string]string, len(env))} for k, v := range env { r.Vars[k] = macVar(key, k, v) if BoxOwned(k) && instance[k] { r.Instance = append(r.Instance, k) } } sort.Strings(r.Instance) r.Digest = r.portableDigest() return r } // ReportEnv is ReportLaunch for an environment no instance layer supplied // a box variable to. func ReportEnv(key []byte, env map[string]string) EnvReport { return ReportLaunch(key, env, nil) } // PortableDigest is the digest of a composed launch environment's // move-portable part: ReportLaunch's Digest, the move's env pin. func PortableDigest(key []byte, env map[string]string, instance map[string]bool) string { return ReportLaunch(key, env, instance).Digest } // LocalDigest is the digest of a whole composed environment, every // variable, box-owned or not: the digest of a comparison made on one box // against a record that box made (the recovery's inputs), where the box's // own variables are part of what runs and must not change either. func LocalDigest(key []byte, env map[string]string) string { vars := make(map[string]string, len(env)) for k, v := range env { vars[k] = macVar(key, k, v) } return "sha256:" + digestOf(vars, func(string) bool { return true }) } func macVar(key []byte, name, value string) string { mac := hmac.New(sha256.New, key) mac.Write([]byte(name)) mac.Write([]byte{0}) mac.Write([]byte(value)) return hex.EncodeToString(mac.Sum(nil)) } // portableDigest hashes the reported variables Pinned keeps, in the v2 // pin's form. func (r EnvReport) portableDigest() string { return envPinPrefix + digestOf(r.Vars, func(k string) bool { return Pinned(k, r.instanceSupplied(k)) }) } // EnvReportVersion is the env report's and the env pin's contract // (home part 2, Task 2b fix round 3). Version 2 names the instance's layer // names (Instance) and pins a box variable only when the instance supplied // it (Pinned). A version 1 report carries no layer names, so an absent // Instance cannot tell an older producer, whose box variables were never // pinned, from a newer one with none instance-set: a v2 participant // refuses a v1 report at T3 and a v1 pin at the probe (CurrentEnvReport, // CurrentEnvPin), and a v2 pin is in a form (envPinPrefix) no v1 // digest, "sha256:", can equal. This is the report's version, not // ProtocolVersion. const EnvReportVersion = 2 // envPinPrefix marks a v2 env pin. const envPinPrefix = "env2:sha256:" // CurrentEnvPin reports whether pin is a v2 env pin, the only form this // release checks a launch against. func CurrentEnvPin(pin string) bool { return strings.HasPrefix(pin, envPinPrefix) } // CurrentEnvReport reports whether r is a v2 env report: its version and // its digest's form both say so. func CurrentEnvReport(r EnvReport) bool { return r.V == EnvReportVersion && CurrentEnvPin(r.Digest) } func digestOf(vars map[string]string, keep func(string) bool) string { names := make([]string, 0, len(vars)) for k := range vars { if keep(k) { names = append(names, k) } } sort.Strings(names) h := sha256.New() for _, k := range names { fmt.Fprintf(h, "%s=%s\n", k, vars[k]) } return hex.EncodeToString(h.Sum(nil)) } // Class is T3's variable class by name. Every BoxOwned variable has a // class other than "equal", and only those do. The agent-owned variables // without a rule of their own (CARLOS_HOST, the row's name) are box // variables. CompareEnv compares a box variable an instance supplied as // "equal" (classIn), as the pin covers it. func Class(name string) string { switch { case name == RegionVar: return "region" case LayoutVars[name]: return "layout" case AdvisoryVars[name]: return "advisory" case BoxOwned(name): return "box" default: return "equal" } } type EnvDiff struct { Name, Kind string } // classIn is Class in a comparison of two reports: a box-owned variable // either side's instance supplied is the instance's, compared as "equal", // as the pin covers it (Pinned). func classIn(name string, source, target EnvReport) string { if !BoxOwned(name) || !(source.instanceSupplied(name) || target.instanceSupplied(name)) { return Class(name) } return "equal" } // Note reports a diff T3 records without failing. func (d EnvDiff) Note() bool { return d.Kind == "advisory-differs" } // CompareEnv is T3: region must differ, layout variables must be present // iff the flat layout provides them (value not compared), advisory // variables report a presence difference as a note (EnvDiff.Note), box // variables are not compared, everything else must be equal by HMAC. T3 // fails on any diff that is not a note. // // targetHome is a move into the home placement (the record's move names // the home's code, ""). A home box sets no CARLOS_REGION (the agent adds it // only when the box has a region), so for a home target the rule turns // round: the target must not carry it, whatever the source carries, and a // target that does is not the home box (region-present). For a named // target the rule is unchanged: it must set its own, different from the // source's. func CompareEnv(source, target EnvReport, flatLayout, targetHome bool) []EnvDiff { var diffs []EnvDiff seen := map[string]bool{} for name, s := range source.Vars { seen[name] = true tv, ok := target.Vars[name] switch classIn(name, source, target) { case "region": if targetHome { if ok { diffs = append(diffs, EnvDiff{name, "region-present"}) // a home box sets no region } } else if !ok { diffs = append(diffs, EnvDiff{name, "region-missing"}) // the target must set its own region } else if tv == s { diffs = append(diffs, EnvDiff{name, "region-same"}) } case "layout": if flatLayout && !ok { diffs = append(diffs, EnvDiff{name, "layout-missing"}) } if !flatLayout && ok { diffs = append(diffs, EnvDiff{name, "layout-present"}) } case "advisory": if !ok { diffs = append(diffs, EnvDiff{name, "advisory-differs"}) } case "box": default: if !ok { diffs = append(diffs, EnvDiff{name, "missing"}) } else if tv != s { diffs = append(diffs, EnvDiff{name, "differs"}) } } } for name := range target.Vars { if seen[name] { continue } switch classIn(name, source, target) { case "region": // a target-only region is a named target's own (a move out of // home); on a home target it is a box that is not the home box if targetHome { diffs = append(diffs, EnvDiff{name, "region-present"}) } case "equal": diffs = append(diffs, EnvDiff{name, "extra"}) case "advisory": diffs = append(diffs, EnvDiff{name, "advisory-differs"}) case "layout": // present iff the flat layout provides it: a target-only // layout variable is wrong on a non-flat landing if !flatLayout { diffs = append(diffs, EnvDiff{name, "layout-present"}) } } } // layout names absent from BOTH reports are still checked: a flat // landing must provide every layout variable the flat layout provides for name := range LayoutVars { if _, inSrc := source.Vars[name]; inSrc { continue // handled above } if _, inTgt := target.Vars[name]; flatLayout && !inTgt { diffs = append(diffs, EnvDiff{name, "layout-missing"}) } } sort.Slice(diffs, func(i, j int) bool { return diffs[i].Name < diffs[j].Name }) return diffs }