package activator // The instance secret. CARLOS_ADMIN_TOKEN is what the agent presents to an // instance for the things only the platform may ask (GET /api/next-due, and // now the scheduled-work tick — spec 2026-08-23-scheduled-work-design.md // §6.4) and what the instance presents back on its control socket, where it // is the bearer on the schedule verbs. That bearer keeps out other UNIX users // on the box and any unauthenticated dialer; it does NOT close the same-uid // sibling gap internal/edge/purge.go documents, because 0600 on this file is // owner-only and every exec child shares the agent's uid — a sibling can read // this file and present the token. Read purge.go for why that residual is // bounded (no privilege a sibling lacked already) and why per-instance uids // are the actual fix. Until this file nothing on the platform minted it: Eleven's own // tooling wrote it by hand, console-created instances never had one at all, // and the next-due probe failed silently for every one of them. Minted here, // once, by whichever comes first: the first spawn, the first seed, or the // schedule runner's first tick for the host (internal/host/schedule_wiring.go // — an instance asleep since before the roll would otherwise wait for a // visitor to mint the token its own tick needs). Never rotated by the // platform, because the running instance already holds the value and a // rotation behind its back would lock the agent out of its own instance. import ( "crypto/rand" "encoding/hex" "errors" "fmt" "log" "os" "path/filepath" "sync" "time" "github.com/carlosframework/platform/internal/instancefs" "github.com/carlosframework/platform/internal/instancekey" ) // adminTokenMu serializes minting across the process. Two callers can find // the token missing at once — the runner's tick and a visitor-triggered // Start for the same host — and without this both would append a line, // leaving a file with two tokens of which readers see the last and the // first spawn's child holds the first. var adminTokenMu sync.Mutex // adminTokenLogged remembers when each (condition, file) pair last had its // complaint logged, so the two conditions below are diagnoses rather than a // log flood. Both are STICKY — a file the agent cannot tighten, a token the // wrong shape — and every one of the callers repeats: the runner ensures the // token on every tick for the host (every 15s while a timer is due), so an // un-throttled line would be thousands a day saying the same thing. Read and // written only under adminTokenMu. // // A TIME and not a bool, because the conditions can also RECUR: the chmod is // retried every tick, so an operator can fix a 0644 file and something can // re-widen it weeks later — and a once-per-process bool would keep the agent // silent about a world-readable instance secret until the next binary roll, // which is weeks away. An hour is quiet enough for the 15s caller and loud // enough that a condition which comes back is heard again. var adminTokenLogged = map[string]time.Time{} // adminTokenLogMu guards adminTokenLogged: logPerFile is called under // adminTokenMu by the mints and outside it by the replica gate. var adminTokenLogMu sync.Mutex // adminTokenLogRepeat is how long one (condition, file) complaint stays // deduplicated before it is said again. const adminTokenLogRepeat = time.Hour // ensureAdminTokenAt is EnsureAdminToken on p's layout: through instancefs // for a nested row's tenant-writable directory, by path otherwise. The // legacy launch bodies each know their layout and call one or the other; // the claim path, which serves both, calls this. func ensureAdminTokenAt(p instancePaths, name string) error { if p.layout == LayoutNested { return EnsureAdminTokenBeneath(p.dataDir, name) } return EnsureAdminToken(filepath.Join(p.dataDir, name)) } // logPerFile emits msg unless this (kind, path) pair has already said it // within adminTokenLogRepeat. Keyed by kind as well as path so a file that // is both un-tightenable AND holding a malformed token still reports both — // one condition must not swallow the other's only line. The replica gate // keys it by host instead of path, to the same end. func logPerFile(kind, path, msg string) { key := kind + "\x00" + path adminTokenLogMu.Lock() defer adminTokenLogMu.Unlock() if last, seen := adminTokenLogged[key]; seen && time.Since(last) < adminTokenLogRepeat { return } adminTokenLogged[key] = time.Now() log.Print(msg) } // wellFormedAdminToken reports whether tok has the exact shape this file // mints: 64 lowercase hex characters (32 random bytes, hex-encoded). A // torn write — an append interrupted mid-line, a file truncated by a full // disk — can leave a short or garbled value that authenticates nothing, // and the failure it produces afterwards ("401 on every tick") points at // the wrong thing entirely. // // A token that fails this is not necessarily broken: before the platform // minted these, Eleven's tooling wrote them by hand, and a hand-written // value of another shape works perfectly well as long as the instance and // the file agree. That is exactly why a failure here is a WARNING and never // a rotation — see EnsureAdminToken. func wellFormedAdminToken(tok string) bool { if len(tok) != 64 { return false } for i := 0; i < len(tok); i++ { c := tok[i] if (c < '0' || c > '9') && (c < 'a' || c > 'f') { return false } } return true } // EnsureAdminToken makes sure path (an instance's own .env) carries a // CARLOS_ADMIN_TOKEN, minting one if it does not. The file is the instance's, // not the agent's: every other line in it is preserved untouched and an // existing token is never rewritten. The file ends up 0600 whether it was // created here, already existed in a looser mode, or already held a token — // that is the same mode the env file carries everywhere else, and the only // thing standing between another UNIX user on the box and the instance's // secret — the one exception being a path that turns out to be a symlink, // which is reported and never chmodded. An existing token is also // shape-checked, and a value that isn't the shape this mints is reported and // left exactly as it is. func EnsureAdminToken(path string) error { return ensureAdminToken(filepath.Dir(path), filepath.Base(path), false) } // EnsureAdminTokenBeneath is EnsureAdminToken for a nested row's own env // file (design §3.2): the file lives on tenant-writable ground, so the read // and the append both go through instancefs.OpenBeneath instead of a plain // path-based open, refusing anything a tenant could plant there that is not // the regular file this contract expects (a FIFO the plain calls would // otherwise block on forever). // // EnsureAdminToken is expressed as a call into the same body (below) rather // than as a second copy of the mint-and-parse logic, because its own // os.ReadFile/os.OpenFile calls are byte-identical in behavior to going // through instancefs for a path no tenant can ever reach: nothing on the // flat path is ever a symlink or a special file in practice, and // instancefs adds only refusals, never a different outcome, for an // ordinary regular file. Splitting path into dir/name here, once, is what // lets both callers share every line after the read. func EnsureAdminTokenBeneath(dir, name string) error { return ensureAdminToken(dir, name, true) } // ensureAdminToken is EnsureAdminToken's and EnsureAdminTokenBeneath's // shared body. beneath selects which pair of I/O primitives the read and // the append use; everything else, the parse, the mode check, the shape // check, the mint, is one piece of logic so the two callers cannot drift. func ensureAdminToken(dir, name string, beneath bool) error { path := filepath.Join(dir, name) adminTokenMu.Lock() defer adminTokenMu.Unlock() // Read the raw bytes once: they answer both questions this needs — is a // token already there, and does the file end on a line boundary. var raw []byte var err error if beneath { raw, err = instancefs.ReadBeneathLimit(dir, name, 1<<20) } else { raw, err = os.ReadFile(path) } if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("read %s: %w", path, err) } if tok := parseEnvBytes(raw)["CARLOS_ADMIN_TOKEN"]; tok != "" { // The early return is NOT a no-op. Two things still have to be true // of a file that already holds the secret. // // First, the mode. The chmod below used to run only on the minting // path, so an env file that was already 0644 when the platform found // a token in it stayed 0644 — holding the very secret the mode is // there to protect. Failing to tighten it does NOT fail the ensure, // because the token still works and refusing the wake over a mode // would trade a hardening for an outage. // // Lstat first, and never chmod a symlink: os.Chmod is path-based // chmod(2), which FOLLOWS one. A same-uid sibling — the residual the // spec's §7.2 documents and accepts — that replaced .env with a // link would otherwise aim the agent's chmod at a file of its // choosing, now once every 15s rather than once per instance // lifetime. backend_exec.go does exactly this dance before it // normalizes an instance socket ("a symlink planted at the route's // path must not become a mode change somewhere else") and the hazard // is the same one. The Perm check also keeps the common case — a file // already at 0600 — down to the Lstat the guard costs anyway. switch fi, statErr := os.Lstat(path); { case statErr != nil: 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)) case fi.Mode()&os.ModeSymlink != 0: 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)) case fi.Mode().Perm() != 0o600: if err := os.Chmod(path, 0o600); err != nil { 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)) } } // Second, the shape. A torn write leaves a value that will 401 every // tick, and the platform must say so rather than let the operator // discover it as "scheduled work stopped working". // // It is LEFT IN PLACE, never rotated (ruling, 2026-08-24). The // running instance was handed this same value at spawn, so file and // process agree and ticks authenticate; re-minting behind the // instance's back would 401 a live instance instead of repairing // anything. Consistency beats hygiene here, and the operator has the // one safe repair — stop the instance, delete the line, let the next // spawn mint it. if !wellFormedAdminToken(tok) { 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)) } return nil } secret := make([]byte, 32) if _, err := rand.Read(secret); err != nil { return fmt.Errorf("mint admin token for %s: %w", path, err) } // Append rather than rewrite: the instance owns this file, and a // read-modify-write would race anything else editing it. var f *os.File if beneath { f, err = instancefs.OpenBeneath(dir, name, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) } else { f, err = os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) } if err != nil { return fmt.Errorf("open %s: %w", path, err) } defer f.Close() // Land on a fresh line: a file whose last line has no trailing newline // would otherwise swallow our variable into it. prefix := "" if len(raw) > 0 && raw[len(raw)-1] != '\n' { prefix = "\n" } if _, err := fmt.Fprintf(f, "%sCARLOS_ADMIN_TOKEN=%s\n", prefix, hex.EncodeToString(secret)); err != nil { return fmt.Errorf("write %s: %w", path, err) } // O_CREATE's mode applies only to a file created here. A pre-existing // file written 0644 by hand (or by an older tool) now holds a secret, so // it is tightened after the append, not before: chmod on a file the // write then failed on would be a change for nothing. if err := os.Chmod(path, 0o600); err != nil { return fmt.Errorf("chmod %s: %w", path, err) } return nil } // parseEnvBytes parses an EnvironmentFile already in hand: KEY=VALUE lines, // blank lines and # comments skipped. It is readEnvFile's whole body (that // one adds the read), split out so EnsureAdminToken can answer "is the token // set" and "does the file end in a newline" from ONE read of the file — two // reads could disagree, and the second one decides where the token lands. func parseEnvBytes(raw []byte) map[string]string { return instancekey.ParseEnv(raw) }