| 1 | package appconfig |
| 2 | |
| 3 | import "strings" |
| 4 | |
| 5 | // Agent-owned vars (parent spec §7.3): CARLOS_PURGE is set by the platform |
| 6 | // when the box has a cache and REMOVED when it does not — the removal half |
| 7 | // is load-bearing, since the env layers are tenant-supplied and a tenant |
| 8 | // advertising a purge channel that does not exist would have apps skip |
| 9 | // their own invalidation and serve stale content indefinitely. |
| 10 | // CARLOS_CONTROL_SOCKET is stripped for the stronger reason: a |
| 11 | // tenant-chosen socket path is a name the agent never vouched for. This |
| 12 | // mirrors the exec backend's withoutVars (internal/activator/backend_exec.go); |
| 13 | // the two lists must not drift. |
| 14 | // |
| 15 | // The sync appends CARLOS_PURGE=header UNCONDITIONALLY because cache |
| 16 | // construction is unconditional in carlos agent (internal/host/agent.go, |
| 17 | // edge.NewCache at startup) — every box this sync runs on serves through |
| 18 | // that cache. The day the cache grows an off switch, this must learn to |
| 19 | // read it. |
| 20 | func agentOwned(name string) bool { |
| 21 | return name == "CARLOS_PURGE" || name == "CARLOS_CONTROL_SOCKET" |
| 22 | } |
| 23 | |
| 24 | // applyAgentVars rewrites one rendered env file: tenant-supplied |
| 25 | // agent-owned vars are dropped and the agent's own CARLOS_PURGE=header is |
| 26 | // appended last (last assignment wins under systemd EnvironmentFile, so |
| 27 | // even a hand-staged earlier line could not override it — but we strip |
| 28 | // those anyway). Deterministic and idempotent, so syncHost's bytes.Equal |
| 29 | // convergence check still short-circuits the steady state. |
| 30 | // |
| 31 | // RenderEnv (materialize.go) emits exactly one `KEY=value\n` line per var, |
| 32 | // keys sorted, and nothing else — no comments, no blank lines — so dropping |
| 33 | // blank lines here is a no-op against its actual output; it's kept because a |
| 34 | // transformation over line-oriented KEY=value content should not depend on |
| 35 | // its input never containing one. |
| 36 | func applyAgentVars(rendered []byte) []byte { |
| 37 | var out strings.Builder |
| 38 | for _, line := range strings.Split(string(rendered), "\n") { |
| 39 | s := strings.TrimSpace(line) |
| 40 | if s == "" { |
| 41 | continue |
| 42 | } |
| 43 | if name, _, ok := strings.Cut(s, "="); ok && agentOwned(strings.TrimSpace(name)) { |
| 44 | continue |
| 45 | } |
| 46 | out.WriteString(line) |
| 47 | out.WriteByte('\n') |
| 48 | } |
| 49 | out.WriteString("CARLOS_PURGE=header\n") |
| 50 | return []byte(out.String()) |
| 51 | } |
| 52 | |