// The sidecar pause: an operator allowlist in front of the two doors that // bring a native sidecar into being. // // Why it exists: a native sidecar runs on the flat tier // (activator.ExecBackend.StartWorkerStaged) as the box's carlos uid with no // mount namespace, so on its box it can read and write every nested // tenant's /data/instances/. The fix is nested sidecars (amadan // discussion #370). Until that lands, an operator lists the accounts that // may still enable or create one (CARLOS_SIDECAR_ACCOUNTS) and every other // account is refused. // // The doors, and why there are only two. A sidecar runs only from a record // at apps///sidecars/.json, which the box's sidecar // reconcile pass (or, under host claim, the root claim path) turns into a // Kind "sidecar" route, and it takes its argv and ceilings from // apps///sidecars.json. The console writes the record in exactly // one place (handleAPISidecarCreate, both the CreateSidecarRecord and the // host-claim beginClaimCreate arms) and the manifest in exactly one place // (handleAPISidecarEnable). Both call refuseSidecarsPaused. Ship and promote // publish releases and channel pointers, never a sidecar manifest; restore // clears an app's trash marker and brings back only sidecars that already // existed; carlos run needs a nested instance. Store mode (`carlos sidecars // enable` against the bucket) is the operator's own bucket credential and // is not a console door. // // What it leaves alone, on purpose: list, get, wake and delete. Existing // sidecars keep running, and an account taken off the list must still be // able to retire its own. package console import ( "log" "net/http" "slices" ) // sidecarsPausedMsg is the one sentence a refused account reads. // copy: approved by Paul 2026-10-24 (copy review). const sidecarsPausedMsg = "sidecars are paused for this account. existing sidecars keep running, and you can still delete them" // sidecarsPausedCode is the refusal's machine-readable code, beside the // sentence the way plan caps carry "cap". const sidecarsPausedCode = "sidecars-paused" // sidecarsPaused reports whether acct is refused new sidecars: the // allowlist is set and does not name the account's sqid. func (s *Server) sidecarsPaused(acct *Account) bool { return len(s.cfg.SidecarAccounts) > 0 && !slices.Contains(s.cfg.SidecarAccounts, acct.Sqid) } // refuseSidecarsPaused answers 403 sidecars-paused and reports true when // acct may not enable or create a sidecar. Call it AFTER the door's // authentication, membership and validation checks, so a non-member still // gets the byte-identical 404 and the gate never confirms that an account // exists, and BEFORE any bucket write. Request-path refusals are not // audited in this console (the plan cap's is not either), so it logs. func (s *Server) refuseSidecarsPaused(w http.ResponseWriter, acct *Account, app, verb string) bool { if !s.sidecarsPaused(acct) { return false } log.Printf("console: api sidecars %s %s/%s: refused, account not in CARLOS_SIDECAR_ACCOUNTS", verb, acct.Sqid, app) writeJSON(w, http.StatusForbidden, map[string]any{"error": sidecarsPausedMsg, "code": sidecarsPausedCode}) return true }