carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bufio"
5 "bytes"
6 "os"
7 "path/filepath"
8 "slices"
9 "strings"
10
11 "github.com/carlosframework/platform/internal/hostbox"
12 "github.com/carlosframework/platform/internal/hostwrite"
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/rollout"
15)
16
17// activationLockName is the lock install-control and its siblings hold in
18// the directory they install into (activationLock).
19const activationLockName = ".claims-activation.lock"
20
21// claimGlobalSharedPaths is the empty-installation census's classification
22// of what the box's own installers put under the managed roots (host
23// lifecycle claim P6-D2: shared release directories, shared unit templates
24// and the registry database are not host-owned). Each entry is derived from
25// the installer that writes it: bootstrap's embedded unit set and arming
26// plan, the self-update applier's opt layout and the release store's
27// version layout. Nothing here is a wildcard. Per-host live links, unit
28// instances, drop-ins and env files stay unclassified, so the census
29// refuses them.
30func claimGlobalSharedPaths(r ClaimRuntimeResources) map[string][]string {
31 return map[string][]string{
32 "data": claimDataCensus(r.DataDir),
33 "opt": claimOptCensus(r.OptDir),
34 "units": claimUnitCensus(r.UnitsDir),
35 }
36}
37
38// claimCensusRel names path below root, or "" (which the census refuses as
39// a malformed classification) when it is not strictly inside it.
40func claimCensusRel(root, path string) string {
41 rel, err := filepath.Rel(root, path)
42 if err != nil || !filepath.IsLocal(rel) {
43 return ""
44 }
45 return filepath.ToSlash(rel)
46}
47
48func claimDataCensus(dataDir string) []string {
49 // The claim journals and guard records verify their own contents; the
50 // census reads neither. The registry is bootstrap's file, shared by
51 // every host, plus SQLite's sidecars once something has opened it.
52 reg := claimCensusRel(dataDir, filepath.Join(dataDir, "registry.db"))
53 // Region HA's relocation journals (N9) verify their own contents, like
54 // the claim's: install and attempt journals, release positions and the
55 // build's scratch, every one keyed by op id under relocate/ and swept
56 // once its operation is over. No installation artifact lives there.
57 // Moves' held-stage relay (N11, until moves ask M-K25 classifies it):
58 // requests and acks are keyed by host, validated field by field by
59 // root against the claim before any effect, and never an installation
60 // artifact. A relocation target whose stage is blocked leaves its
61 // request there (M3).
62 stage := claimCensusRel(dataDir, release.HeldStageDir(dataDir))
63 staged := claimCensusRel(dataDir, release.HeldStagedDir(dataDir))
64 // And its held-stage lock (Task 8c fix round 1, I1): every held
65 // publication, root's relay and the relocation install's own, holds
66 // .moves/held-<host>.lock (withHeldStageLock). Moves removes it when the
67 // stage finishes (M-K25 on #236), but a holder killed mid-stage leaves
68 // it, so the fallback stays, as moves asked: .moves is a shared tree,
69 // not opaque: regular files and directories only, and anything naming
70 // the host being censused still refuses.
71 entries := []string{".hostclaims", ".hostexec", "hostguard", "relocate", stage, staged, hostbox.CensusTree(movesDirName), hostbox.CensusFile(reg), hostbox.CensusFile(reg + "-wal"), hostbox.CensusFile(reg + "-shm")}
72 return append(entries, claimBoxDataCensus(dataDir)...)
73}
74
75// claimBoxDataCensus is the box's own state under the data root, found on a
76// serving box (leo, 2026-10-19) and owned by no installation (step 8
77// readiness, R2 (b) and (c)). Each entry is the exact path its writer
78// uses, a regular file only, except the edge's certificate cache, which is a
79// shared tree of directories and regular files. Nothing here names a host,
80// and the per-host census still refuses any path that names the host it
81// censuses, so a certificate for that host still refuses its install.
82func claimBoxDataCensus(dataDir string) []string {
83 rel := func(path string) string { return claimCensusRel(dataDir, path) }
84 files := []string{
85 // The edge's autocert cache: certificates and keys, the ACME account
86 // key (edge.go's -acme-cache default; bootstrap makes the directory).
87 hostbox.CensusTree(rel(filepath.Join(dataDir, "acme"))),
88 // The agent's and edge's pointer-digest ratchet (#253): one mark per
89 // channel, <acct>/<app>/<channel>.digests. Box anti-rollback state,
90 // never cleared and never an installation artifact (#324).
91 hostbox.CensusTree(rel(release.EdgeRatchetDir(dataDir))),
92 // The agent's system update state: its request (requested.json, also
93 // a file below) and its cache of the router build it fetched,
94 // <version>/carlos (systemCachePath). Root walks the cache beneath
95 // the data dir and checks it against the release's signed sums before
96 // it uses it; a normal update leaves it until the agent's next hourly
97 // observe forgets it, so a migration window can meet it (harness,
98 // 2026-10-20). Box state, never an installation artifact.
99 hostbox.CensusTree(rel(systemDataDir(dataDir))),
100 }
101 for _, path := range []string{
102 filepath.Join(dataDir, stageRequestMarker), // the agent's root stage request
103 filepath.Join(dataDir, systemRequestMarker), // the agent's self-update request
104 systemRequestPath(dataDir), // the update the agent asked for
105 restartMarksPath(dataDir), // root's restart memory
106 restartMarksPath(dataDir) + ".lock", // and its lock (lockRestartMarks)
107 adoptPassLockPath(dataDir), // every root adoption pass's lock
108 filepath.Join(dataDir, rollout.FailureFile), // the swap failure record
109 release.FirstSeenPath(dataDir), // channel first-seen observations
110 release.GenerationsPath(dataDir), // zero-downtime generations
111 release.GenerationsSeqPath(release.GenerationsPath(dataDir)),
112 // The edge's GeoIP mirror pointer (edge.go newGeoMirror's directory,
113 // geoipmirror.go's "current") and the metrics loop's alert state.
114 filepath.Join(dataDir, "carlos", "geoip", "current"),
115 filepath.Join(dataDir, "monitor", "alerts.json"),
116 } {
117 files = append(files, hostbox.CensusFile(rel(path)))
118 }
119 return files
120}
121
122func claimOptCensus(optDir string) []string {
123 entries := []string{
124 // The staged platform binary and the self-update applier's fallback
125 // with its sidecars.
126 hostbox.CensusFile(claimCensusRel(optDir, systemBinaryPath(optDir))),
127 hostbox.CensusFile(claimCensusRel(optDir, systemPrevPath(optDir))),
128 hostbox.CensusFile(claimCensusRel(optDir, prevVersionPath(optDir))),
129 hostbox.CensusFile(claimCensusRel(optDir, prevAllowedPath(optDir))),
130 // The seeds floor's verdict on carlos.prev (seedsfloor.go), beside
131 // the moves verdict and written by the same root passes. Its floor
132 // files live in .moves, the shared tree above.
133 hostbox.CensusFile(claimCensusRel(optDir, seedsPrevAllowedPath(optDir))),
134 // The shared release store: <opt>/versions/<ver>/[<acct>/]<app>.
135 // Directories and regular files only; a symlink in it is refused.
136 hostbox.CensusTree(claimCensusRel(optDir, filepath.Dir(filepath.Dir(release.VersionPath(optDir, "", "v", "a"))))),
137 // Root's pointer-digest ratchet (#253), the same marks as the edge's
138 // under the data root; root reads both, so neither is moved aside.
139 hostbox.CensusTree(claimCensusRel(optDir, release.RootRatchetDir(optDir))),
140 }
141 // Root's self-update state and the host-floor guard copy. Both are box
142 // state below one parent; it is opaque to the census, as before.
143 for _, dir := range []string{systemRootDir(optDir), systemGuardDir(optDir)} {
144 if rel := claimCensusRel(optDir, filepath.Dir(dir)); !slices.Contains(entries, rel) {
145 entries = append(entries, rel)
146 }
147 }
148 // <opt>/live itself is only a directory. Every link in it belongs to a
149 // host and is unclassified.
150 return entries
151}
152
153// claimCensusEnabledUnits is every unit an installer enables: bootstrap's
154// arming plan (which a self-update apply also arms, from the target build's
155// own plan) and ensureSystemUnits' second belt.
156func claimCensusEnabledUnits() []string {
157 units := currentBootstrapPlan().Enabled
158 for _, u := range ensureSystemArmed {
159 if !slices.Contains(units, u) {
160 units = append(units, u)
161 }
162 }
163 return units
164}
165
166func claimUnitCensus(unitsDir string) []string {
167 names, err := unitNames()
168 if err != nil {
169 // Nothing of the platform's is classified, so the census refuses.
170 return nil
171 }
172 var entries []string
173 for _, name := range names {
174 // Every embedded unit, templates, slices and mounts included, is a
175 // file bootstrap and the applier install for the whole box.
176 entries = append(entries, hostbox.CensusFile(name))
177 }
178 for _, unit := range []string{"carlos-edge.service", "carlos-system-update.service", "carlos-system-recover.service"} {
179 entries = append(entries, hostbox.CensusFile(unit+".d/90-host-floor.conf"))
180 }
181 // install-control's lock (activationLock), left in the unit directory
182 // after it runs. It stays: unlinking a flocked file lets a second
183 // installer lock a fresh inode beside the first.
184 entries = append(entries, hostbox.CensusFile(activationLockName))
185 for _, unit := range claimCensusEnabledUnits() {
186 if !slices.Contains(names, unit) || strings.Contains(unit, "@") {
187 continue
188 }
189 body, err := unitsFS.ReadFile("units/" + unit)
190 if err != nil {
191 continue
192 }
193 target := filepath.Join(unitsDir, unit)
194 for _, link := range claimUnitInstallLinks(unit, body) {
195 entries = append(entries, hostbox.CensusLink(link, target))
196 }
197 }
198 if hostwrite.Enabled(os.Getenv) && slices.Contains(names, claimControlUnit) {
199 // Claim rollout uses add-wants to start the static root service at
200 // boot. Its unit has no [Install] metadata to derive this link from.
201 entries = append(entries, hostbox.CensusLink("multi-user.target.wants/"+claimControlUnit, filepath.Join(unitsDir, claimControlUnit)))
202 }
203 return entries
204}
205
206// claimUnitInstallLinks lists the symlinks `systemctl enable` creates for a
207// non-template unit from its [Install] section, relative to the unit
208// directory. A value it cannot read exactly (a specifier, an Also= list) is
209// left out, so the link it would have made stays unclassified.
210func claimUnitInstallLinks(unit string, body []byte) []string {
211 var links []string
212 section := ""
213 scan := bufio.NewScanner(bytes.NewReader(body))
214 for scan.Scan() {
215 line := strings.TrimSpace(scan.Text())
216 if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
217 continue
218 }
219 if strings.HasPrefix(line, "[") {
220 section = line
221 continue
222 }
223 key, value, ok := strings.Cut(line, "=")
224 if section != "[Install]" || !ok {
225 continue
226 }
227 suffix := map[string]string{"WantedBy": ".wants/", "RequiredBy": ".requires/", "UpheldBy": ".upholds/", "Alias": ""}
228 dir, known := suffix[strings.TrimSpace(key)]
229 if !known {
230 continue
231 }
232 for _, name := range strings.Fields(value) {
233 if strings.ContainsAny(name, "%/\\") {
234 continue
235 }
236 if dir == "" {
237 links = append(links, name)
238 continue
239 }
240 links = append(links, name+dir+unit)
241 }
242 }
243 return links
244}
245