carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3// The instance secret. CARLOS_ADMIN_TOKEN is what the agent presents to an
4// instance for the things only the platform may ask (GET /api/next-due, and
5// now the scheduled-work tick — spec 2026-08-23-scheduled-work-design.md
6// §6.4) and what the instance presents back on its control socket, where it
7// is the bearer on the schedule verbs. That bearer keeps out other UNIX users
8// on the box and any unauthenticated dialer; it does NOT close the same-uid
9// sibling gap internal/edge/purge.go documents, because 0600 on this file is
10// owner-only and every exec child shares the agent's uid — a sibling can read
11// this file and present the token. Read purge.go for why that residual is
12// bounded (no privilege a sibling lacked already) and why per-instance uids
13// are the actual fix. Until this file nothing on the platform minted it: Eleven's own
14// tooling wrote it by hand, console-created instances never had one at all,
15// and the next-due probe failed silently for every one of them. Minted here,
16// once, by whichever comes first: the first spawn, the first seed, or the
17// schedule runner's first tick for the host (internal/host/schedule_wiring.go
18// — an instance asleep since before the roll would otherwise wait for a
19// visitor to mint the token its own tick needs). Never rotated by the
20// platform, because the running instance already holds the value and a
21// rotation behind its back would lock the agent out of its own instance.
22
23import (
24 "crypto/rand"
25 "encoding/hex"
26 "errors"
27 "fmt"
28 "log"
29 "os"
30 "path/filepath"
31 "sync"
32 "time"
33
34 "github.com/carlosframework/platform/internal/instancefs"
35 "github.com/carlosframework/platform/internal/instancekey"
36)
37
38// adminTokenMu serializes minting across the process. Two callers can find
39// the token missing at once — the runner's tick and a visitor-triggered
40// Start for the same host — and without this both would append a line,
41// leaving a file with two tokens of which readers see the last and the
42// first spawn's child holds the first.
43var adminTokenMu sync.Mutex
44
45// adminTokenLogged remembers when each (condition, file) pair last had its
46// complaint logged, so the two conditions below are diagnoses rather than a
47// log flood. Both are STICKY — a file the agent cannot tighten, a token the
48// wrong shape — and every one of the callers repeats: the runner ensures the
49// token on every tick for the host (every 15s while a timer is due), so an
50// un-throttled line would be thousands a day saying the same thing. Read and
51// written only under adminTokenMu.
52//
53// A TIME and not a bool, because the conditions can also RECUR: the chmod is
54// retried every tick, so an operator can fix a 0644 file and something can
55// re-widen it weeks later — and a once-per-process bool would keep the agent
56// silent about a world-readable instance secret until the next binary roll,
57// which is weeks away. An hour is quiet enough for the 15s caller and loud
58// enough that a condition which comes back is heard again.
59var adminTokenLogged = map[string]time.Time{}
60
61// adminTokenLogMu guards adminTokenLogged: logPerFile is called under
62// adminTokenMu by the mints and outside it by the replica gate.
63var adminTokenLogMu sync.Mutex
64
65// adminTokenLogRepeat is how long one (condition, file) complaint stays
66// deduplicated before it is said again.
67const adminTokenLogRepeat = time.Hour
68
69// ensureAdminTokenAt is EnsureAdminToken on p's layout: through instancefs
70// for a nested row's tenant-writable directory, by path otherwise. The
71// legacy launch bodies each know their layout and call one or the other;
72// the claim path, which serves both, calls this.
73func ensureAdminTokenAt(p instancePaths, name string) error {
74 if p.layout == LayoutNested {
75 return EnsureAdminTokenBeneath(p.dataDir, name)
76 }
77 return EnsureAdminToken(filepath.Join(p.dataDir, name))
78}
79
80// logPerFile emits msg unless this (kind, path) pair has already said it
81// within adminTokenLogRepeat. Keyed by kind as well as path so a file that
82// is both un-tightenable AND holding a malformed token still reports both —
83// one condition must not swallow the other's only line. The replica gate
84// keys it by host instead of path, to the same end.
85func logPerFile(kind, path, msg string) {
86 key := kind + "\x00" + path
87 adminTokenLogMu.Lock()
88 defer adminTokenLogMu.Unlock()
89 if last, seen := adminTokenLogged[key]; seen && time.Since(last) < adminTokenLogRepeat {
90 return
91 }
92 adminTokenLogged[key] = time.Now()
93 log.Print(msg)
94}
95
96// wellFormedAdminToken reports whether tok has the exact shape this file
97// mints: 64 lowercase hex characters (32 random bytes, hex-encoded). A
98// torn write — an append interrupted mid-line, a file truncated by a full
99// disk — can leave a short or garbled value that authenticates nothing,
100// and the failure it produces afterwards ("401 on every tick") points at
101// the wrong thing entirely.
102//
103// A token that fails this is not necessarily broken: before the platform
104// minted these, Eleven's tooling wrote them by hand, and a hand-written
105// value of another shape works perfectly well as long as the instance and
106// the file agree. That is exactly why a failure here is a WARNING and never
107// a rotation — see EnsureAdminToken.
108func wellFormedAdminToken(tok string) bool {
109 if len(tok) != 64 {
110 return false
111 }
112 for i := 0; i < len(tok); i++ {
113 c := tok[i]
114 if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
115 return false
116 }
117 }
118 return true
119}
120
121// EnsureAdminToken makes sure path (an instance's own <host>.env) carries a
122// CARLOS_ADMIN_TOKEN, minting one if it does not. The file is the instance's,
123// not the agent's: every other line in it is preserved untouched and an
124// existing token is never rewritten. The file ends up 0600 whether it was
125// created here, already existed in a looser mode, or already held a token —
126// that is the same mode the env file carries everywhere else, and the only
127// thing standing between another UNIX user on the box and the instance's
128// secret — the one exception being a path that turns out to be a symlink,
129// which is reported and never chmodded. An existing token is also
130// shape-checked, and a value that isn't the shape this mints is reported and
131// left exactly as it is.
132func EnsureAdminToken(path string) error {
133 return ensureAdminToken(filepath.Dir(path), filepath.Base(path), false)
134}
135
136// EnsureAdminTokenBeneath is EnsureAdminToken for a nested row's own env
137// file (design §3.2): the file lives on tenant-writable ground, so the read
138// and the append both go through instancefs.OpenBeneath instead of a plain
139// path-based open, refusing anything a tenant could plant there that is not
140// the regular file this contract expects (a FIFO the plain calls would
141// otherwise block on forever).
142//
143// EnsureAdminToken is expressed as a call into the same body (below) rather
144// than as a second copy of the mint-and-parse logic, because its own
145// os.ReadFile/os.OpenFile calls are byte-identical in behavior to going
146// through instancefs for a path no tenant can ever reach: nothing on the
147// flat path is ever a symlink or a special file in practice, and
148// instancefs adds only refusals, never a different outcome, for an
149// ordinary regular file. Splitting path into dir/name here, once, is what
150// lets both callers share every line after the read.
151func EnsureAdminTokenBeneath(dir, name string) error {
152 return ensureAdminToken(dir, name, true)
153}
154
155// ensureAdminToken is EnsureAdminToken's and EnsureAdminTokenBeneath's
156// shared body. beneath selects which pair of I/O primitives the read and
157// the append use; everything else, the parse, the mode check, the shape
158// check, the mint, is one piece of logic so the two callers cannot drift.
159func ensureAdminToken(dir, name string, beneath bool) error {
160 path := filepath.Join(dir, name)
161 adminTokenMu.Lock()
162 defer adminTokenMu.Unlock()
163 // Read the raw bytes once: they answer both questions this needs — is a
164 // token already there, and does the file end on a line boundary.
165 var raw []byte
166 var err error
167 if beneath {
168 raw, err = instancefs.ReadBeneathLimit(dir, name, 1<<20)
169 } else {
170 raw, err = os.ReadFile(path)
171 }
172 if err != nil && !errors.Is(err, os.ErrNotExist) {
173 return fmt.Errorf("read %s: %w", path, err)
174 }
175 if tok := parseEnvBytes(raw)["CARLOS_ADMIN_TOKEN"]; tok != "" {
176 // The early return is NOT a no-op. Two things still have to be true
177 // of a file that already holds the secret.
178 //
179 // First, the mode. The chmod below used to run only on the minting
180 // path, so an env file that was already 0644 when the platform found
181 // a token in it stayed 0644 — holding the very secret the mode is
182 // there to protect. Failing to tighten it does NOT fail the ensure,
183 // because the token still works and refusing the wake over a mode
184 // would trade a hardening for an outage.
185 //
186 // Lstat first, and never chmod a symlink: os.Chmod is path-based
187 // chmod(2), which FOLLOWS one. A same-uid sibling — the residual the
188 // spec's §7.2 documents and accepts — that replaced <host>.env with a
189 // link would otherwise aim the agent's chmod at a file of its
190 // choosing, now once every 15s rather than once per instance
191 // lifetime. backend_exec.go does exactly this dance before it
192 // normalizes an instance socket ("a symlink planted at the route's
193 // path must not become a mode change somewhere else") and the hazard
194 // is the same one. The Perm check also keeps the common case — a file
195 // already at 0600 — down to the Lstat the guard costs anyway.
196 switch fi, statErr := os.Lstat(path); {
197 case statErr != nil:
198 logPerFile("chmod", path, fmt.Sprintf("activator: %s holds CARLOS_ADMIN_TOKEN but could not be stat'd to check its mode: %v — another UNIX user on this box may be able to read the instance's secret", path, statErr))
199 case fi.Mode()&os.ModeSymlink != 0:
200 logPerFile("chmod", path, fmt.Sprintf("activator: %s is a SYMLINK, not the instance's own env file — refusing to change its target's mode, and the token in it should be treated as compromised (a same-uid process on this box planted this)", path))
201 case fi.Mode().Perm() != 0o600:
202 if err := os.Chmod(path, 0o600); err != nil {
203 logPerFile("chmod", path, fmt.Sprintf("activator: %s holds CARLOS_ADMIN_TOKEN at mode %#o and could not be tightened to 0600: %v — another UNIX user on this box may be able to read the instance's secret", path, fi.Mode().Perm(), err))
204 }
205 }
206 // Second, the shape. A torn write leaves a value that will 401 every
207 // tick, and the platform must say so rather than let the operator
208 // discover it as "scheduled work stopped working".
209 //
210 // It is LEFT IN PLACE, never rotated (ruling, 2026-08-24). The
211 // running instance was handed this same value at spawn, so file and
212 // process agree and ticks authenticate; re-minting behind the
213 // instance's back would 401 a live instance instead of repairing
214 // anything. Consistency beats hygiene here, and the operator has the
215 // one safe repair — stop the instance, delete the line, let the next
216 // spawn mint it.
217 if !wellFormedAdminToken(tok) {
218 logPerFile("shape", path, fmt.Sprintf("activator: CARLOS_ADMIN_TOKEN in %s is not the shape the platform mints (64 hex characters) — left in place, because the running instance holds this same value and rotating it would lock the platform out of a live instance; if it is corrupt, stop the instance, delete the line and restart it to have one minted", path))
219 }
220 return nil
221 }
222 secret := make([]byte, 32)
223 if _, err := rand.Read(secret); err != nil {
224 return fmt.Errorf("mint admin token for %s: %w", path, err)
225 }
226 // Append rather than rewrite: the instance owns this file, and a
227 // read-modify-write would race anything else editing it.
228 var f *os.File
229 if beneath {
230 f, err = instancefs.OpenBeneath(dir, name, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
231 } else {
232 f, err = os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
233 }
234 if err != nil {
235 return fmt.Errorf("open %s: %w", path, err)
236 }
237 defer f.Close()
238 // Land on a fresh line: a file whose last line has no trailing newline
239 // would otherwise swallow our variable into it.
240 prefix := ""
241 if len(raw) > 0 && raw[len(raw)-1] != '\n' {
242 prefix = "\n"
243 }
244 if _, err := fmt.Fprintf(f, "%sCARLOS_ADMIN_TOKEN=%s\n", prefix, hex.EncodeToString(secret)); err != nil {
245 return fmt.Errorf("write %s: %w", path, err)
246 }
247 // O_CREATE's mode applies only to a file created here. A pre-existing
248 // file written 0644 by hand (or by an older tool) now holds a secret, so
249 // it is tightened after the append, not before: chmod on a file the
250 // write then failed on would be a change for nothing.
251 if err := os.Chmod(path, 0o600); err != nil {
252 return fmt.Errorf("chmod %s: %w", path, err)
253 }
254 return nil
255}
256
257// parseEnvBytes parses an EnvironmentFile already in hand: KEY=VALUE lines,
258// blank lines and # comments skipped. It is readEnvFile's whole body (that
259// one adds the read), split out so EnsureAdminToken can answer "is the token
260// set" and "does the file end in a newline" from ONE read of the file — two
261// reads could disagree, and the second one decides where the token lands.
262func parseEnvBytes(raw []byte) map[string]string {
263 return instancekey.ParseEnv(raw)
264}
265