| 1 | # Sidecar members: a serving class that nothing can dial |
| 2 | |
| 3 | **Date:** 2026-08-17 · **Status:** approved design (Paul, via brainstorming |
| 4 | session, 2026-08-17) · **Requested by:** Paul, for Eleven. |
| 5 | |
| 6 | **Closes:** the gap between the platform spec's `sidecar` serving class |
| 7 | (`2026-08-01-carlos-platform-design.md` §5.4, §4 manifest) and what the |
| 8 | platform can actually be asked to run. The runtime half has existed since |
| 9 | Plan A; the declaration half has never existed. |
| 10 | |
| 11 | **Extends:** instance provisioning (`2026-08-04-instance-provisioning-design.md` |
| 12 | — the record/manifest/reconciler/API pattern this mirrors), exec config |
| 13 | delivery (`2026-08-02-exec-config-delivery-design.md` §"Per-sidecar config … |
| 14 | deferred", which this closes), customer fleets |
| 15 | (`2026-08-05-customer-fleets-design.md` — outbound-only boxes), |
| 16 | `carlos restart` (#113 — the pull-based generation-object idiom the wake |
| 17 | verb copies). |
| 18 | |
| 19 | **Deliberately does not depend on** the unmerged trust-classes and |
| 20 | private-instances specs (PR #183). Those name the *vocabulary* this class |
| 21 | will eventually be placed by (an A-class sidecar holding a member key is |
| 22 | exactly their subject); nothing here waits on them. |
| 23 | |
| 24 | ## 1. Why |
| 25 | |
| 26 | A sidecar today is a file on a box. `HasSidecar` is `os.Stat` of |
| 27 | `/data/<host>-sidecar.env` (`internal/activator/backend_exec.go:732`), and |
| 28 | nothing in the platform writes that file — config delivery explicitly |
| 29 | disowns it (`internal/appconfig/syncexec.go:617`). So the only way to run |
| 30 | one is to get root on a box and place a file, which is precisely the |
| 31 | product gap CLAUDE.md names: *every `carlos` command is designed for a user |
| 32 | with no access to the underlying infra.* |
| 33 | |
| 34 | The runtime that exists is also the wrong shape for what Eleven needs. It |
| 35 | is a **co-spawn**: one sidecar bound to one routed instance, alive exactly |
| 36 | while that instance is awake (`internal/activator/activator.go:17-24`). A |
| 37 | bot that must run on a box with no routed app on it — eventually a box in a |
| 38 | private subnet with no inbound path from the internet at all — cannot be |
| 39 | expressed that way, because there is no parent instance to hang it from. |
| 40 | |
| 41 | ## 2. Rulings (Paul, 2026-08-17) |
| 42 | |
| 43 | 1. **Standalone members.** A sidecar gets its own record, its own registry |
| 44 | row, its own identity and its own lifecycle. It can be the only thing on |
| 45 | a box. The existing co-spawn path keeps working, untouched, for Eleven's |
| 46 | current shape; provisioned sidecars never use it (§10). |
| 47 | 2. **Hibernating**, with schedule and operator-command wakes. Delivery-wake |
| 48 | (the edge signalling that ciphertext is queued for a sidecar's identity) |
| 49 | is designed here but **not built** — it needs edge work this does not |
| 50 | include (§7.5). |
| 51 | 3. **Synthetic internal host** as the identity: `<name>.<app>.<sqid>.sidecar.internal`, |
| 52 | never resolvable, refused by the edge, so every host-keyed subsystem |
| 53 | works unchanged (§3). |
| 54 | 4. **Placement axis in the platform now; infra spec'd, not applied.** Boxes |
| 55 | declare that they accept no-inbound work; the reconciler honours it; the |
| 56 | tofu for a real private-subnet box is a runbook for Paul's applies (§8). |
| 57 | |
| 58 | ## 3. Identity: a host that cannot be a host |
| 59 | |
| 60 | A sidecar is keyed by a **synthetic host**: |
| 61 | |
| 62 | <name>.<app>.<sqid>.sidecar.internal |
| 63 | bot-acme.eleven.bdm.sidecar.internal |
| 64 | |
| 65 | `name` is one DNS label, validated by the same regex instance hosts use |
| 66 | (`provision.labelRe`) and for the same stated reason: the name becomes a |
| 67 | bucket key segment and a filesystem path segment (`<host>.db`, the cgroup |
| 68 | directory, the log sink file), and the label regex is what keeps a hostile |
| 69 | name an opaque, safe path component. |
| 70 | |
| 71 | **Why a synthetic host and not a new identity axis.** The registry, config |
| 72 | delivery, cgroups, the S3 lease, the DB path, the log sink, metrics |
| 73 | attribution, `carlos logs`, `carlos restart` and the release ladder are all |
| 74 | keyed by host. A second identity axis would mean touching every consumer |
| 75 | that assumes host is the key — the wire-shape rule this repo has paid for |
| 76 | three times. A synthetic host costs one reserved suffix and buys every one |
| 77 | of those subsystems working on day one. |
| 78 | |
| 79 | **`.sidecar.internal` is reserved, and reservation is enforced, not |
| 80 | documented.** The suffix is refused at every door where a name becomes |
| 81 | routable: |
| 82 | |
| 83 | - the edge never binds a `Kind == "sidecar"` row at all (§6.1), which is |
| 84 | the structural guarantee; the suffix refusals below are defence in depth |
| 85 | against a *different* row type wearing the name. |
| 86 | - `carlos add` / the route door / domain attach refuse a host ending in |
| 87 | `.sidecar.internal`. |
| 88 | - the domains reconciler refuses to mint an alias for one. |
| 89 | - a custom domain may not be attached to a sidecar. |
| 90 | |
| 91 | The CLI speaks the short name (`carlos sidecars ls` prints `bot-acme`); the |
| 92 | synthetic host is the internal key, shown only by `-long`. |
| 93 | |
| 94 | ## 4. Bucket shapes (all additive) |
| 95 | |
| 96 | Mirrors instance provisioning exactly, one directory over. |
| 97 | |
| 98 | ### 4.1 `apps/<sqid>/<app>/sidecars.json` — the app's opt-in and policy |
| 99 | |
| 100 | ```json |
| 101 | { |
| 102 | "schema": 1, |
| 103 | "app": "eleven", |
| 104 | "pool": "eleven", |
| 105 | "regions": ["fleet/eleven"], |
| 106 | "channel": "stable", |
| 107 | "args": ["sidecar", "run"], |
| 108 | "placement": "no-inbound", |
| 109 | "hibernate": true, |
| 110 | "idle": "5m", |
| 111 | "max_run": "1h" |
| 112 | } |
| 113 | ``` |
| 114 | |
| 115 | - **Absence means the feature does not exist for that app** — the |
| 116 | litestream/instances convention. The API refuses creates with a message |
| 117 | naming `carlos sidecars enable`, only after membership is proven. |
| 118 | - `args` (required): the argv the platform execs after the release binary. |
| 119 | App-level, not per-record — like `seed`, it is a property of the app's |
| 120 | current release contract. `["sidecar", "run"]` is the shape the existing |
| 121 | co-spawn hard-codes; making it declared is what lets an app ship more |
| 122 | than one kind of worker later. |
| 123 | |
| 124 | **One binary, many sidecars.** Args being app-level is what makes |
| 125 | Eleven's shape — a bot per enrolled member — work: the same release runs |
| 126 | every sidecar, and each child learns *which* one it is from its |
| 127 | environment, not its argv: |
| 128 | |
| 129 | CARLOS_SIDECAR_NAME=bot-acme |
| 130 | CARLOS_SIDECAR_HOST=bot-acme.eleven.bdm.sidecar.internal |
| 131 | |
| 132 | Per-sidecar configuration beyond that is the config plane's job, not the |
| 133 | manifest's: a record may carry `environment`, and route-scoped config |
| 134 | delivery (merged, `#206`) then gives that sidecar its own variables and |
| 135 | sealed secrets. There is deliberately no per-record `args`. |
| 136 | - `placement`: `"any"` (default) or `"no-inbound"` (§8). |
| 137 | - `hibernate` (default true), `idle` (default 5m), `max_run` (default 1h) — |
| 138 | the lifecycle knobs (§7). |
| 139 | - `regions`, `pool`, `channel`: as instances. `regions: ["fleet/eleven"]` |
| 140 | is how Eleven's sidecars land only on Eleven's own boxes. |
| 141 | |
| 142 | Written by the operator or the app owner — `carlos sidecars enable`, |
| 143 | API and direct/store mode both, the `carlos instances enable` pattern. |
| 144 | |
| 145 | ### 4.2 `apps/<sqid>/<app>/sidecars/<name>.json` — the record |
| 146 | |
| 147 | ```json |
| 148 | { |
| 149 | "schema": 1, |
| 150 | "name": "bot-acme", |
| 151 | "host": "bot-acme.eleven.bdm.sidecar.internal", |
| 152 | "app": "eleven", |
| 153 | "account": "bdm", |
| 154 | "pool": "eleven", |
| 155 | "region": "fleet/eleven", |
| 156 | "channel": "stable", |
| 157 | "placement": "no-inbound", |
| 158 | "created_at": "2026-08-17T23:00:00Z", |
| 159 | "created_by": "app:bdm/eleven" |
| 160 | } |
| 161 | ``` |
| 162 | |
| 163 | Create-only (`If-None-Match: *`) — a lost race is a 409, never a silent |
| 164 | overwrite. The body must agree with its own key path (name/app/account vs |
| 165 | the key's segments) or the reconciler skips it loudly. No tombstone: |
| 166 | deletion deletes the object; the parked DB and WAL stream under the |
| 167 | regional bucket are deliberately untouched, the same data-safety contract |
| 168 | `registry.Delete` and `carlos remove` hold. |
| 169 | |
| 170 | `ParseRecordKey`'s discipline is inherited whole: exactly one non-empty |
| 171 | segment after `sidecars/`, so `sidecars.json` (the manifest) and any nested |
| 172 | path both refuse to parse as a record. |
| 173 | |
| 174 | ### 4.3 `apps/<sqid>/<app>/sidecar-wake/<name>.json` — the wake generation |
| 175 | |
| 176 | The operator/app wake source (§7.4), copied from `restart-gen.json` |
| 177 | (`internal/release/restartgen.go`): a small CAS-guarded generation object |
| 178 | whose **etag moving** is the whole signal. A sibling prefix rather than a |
| 179 | key under `sidecars/` so that §4.2's one-segment rule stays exactly as |
| 180 | strict as it reads. |
| 181 | |
| 182 | ```json |
| 183 | {"schema": 1, "generation": 3, "at": "2026-08-17T23:05:00Z", |
| 184 | "by": "member:bdm", "reason": "operator wake"} |
| 185 | ``` |
| 186 | |
| 187 | ## 5. Registry: `Kind = "sidecar"` |
| 188 | |
| 189 | A sidecar row is a route row that is not a route: `Kind "sidecar"`, |
| 190 | **`Socket ""`, `Addr ""`** — the two columns that make a row servable are |
| 191 | empty by construction, and §6.1 makes that structural rather than |
| 192 | incidental. |
| 193 | |
| 194 | Set: `App`, `Account`, `Pool`, `Region`, `Channel`, `Slug` (the short |
| 195 | name), `DBPath` (`<DataDir>/<host>.db`), `Hibernate`, `Provisioned true`, |
| 196 | `NextDue`, and one additive column: |
| 197 | |
| 198 | - **`args`** (new, additive TEXT, JSON argv) — materialized from the |
| 199 | manifest on every reconcile pass, exactly as `seed` is. Truth stays in |
| 200 | the bucket; the column is the box's local copy. |
| 201 | |
| 202 | `Provisioned` carries removal authority as it does for instances: the flag, |
| 203 | inside the agent's own region/pool scope, with the record gone, is what |
| 204 | authorizes the one sanctioned destructive move. |
| 205 | |
| 206 | **Additive-only, per the wire-shape rule:** every route written before this |
| 207 | change parses unchanged (`Kind` defaults to `"instance"` at |
| 208 | `internal/registry/registry.go:704`; an absent `args` is the empty argv). |
| 209 | |
| 210 | **`DueInstances` must widen — this is load-bearing.** |
| 211 | `internal/registry/registry.go:1341-1343` reads |
| 212 | |
| 213 | ```sql |
| 214 | SELECT ... FROM routes WHERE kind = 'instance' AND next_due > 0 AND next_due <= ? |
| 215 | ``` |
| 216 | |
| 217 | The sweeper's timer wake is driven entirely by that query. Left alone, a |
| 218 | sidecar's `next_due` would be written, stored, and **never once acted on** — |
| 219 | the schedule wake source would silently not exist, with no error anywhere. |
| 220 | It widens to `kind IN ('instance','sidecar')`, and the test asserts a due |
| 221 | sidecar row comes back. |
| 222 | |
| 223 | This is the shape of bug the wire-shape rule exists to catch: the consumer |
| 224 | that breaks is a `WHERE` clause, not a caller. |
| 225 | |
| 226 | ## 6. What makes a sidecar unreachable |
| 227 | |
| 228 | Three independent mechanisms, in order of how much they are relied on: |
| 229 | |
| 230 | ### 6.1 The edge cannot bind it (structural) |
| 231 | |
| 232 | The edge's route→servable path refuses `Kind == "sidecar"` at the point a |
| 233 | host is resolved to a route, before any upstream selection. Not "has no |
| 234 | socket so nothing happens" — an explicit refusal with a test that asserts a |
| 235 | sidecar row is never servable even if a `Socket` value is somehow present. |
| 236 | The test is the point: it is the assertion that survives someone later |
| 237 | adding a socket column to a sidecar row for an unrelated reason. |
| 238 | |
| 239 | Likewise the sidecar row is excluded from: upstream selection |
| 240 | (`firstReadyUpstream`), alias minting (`candidateApps`), the apex/canary |
| 241 | domain reconciler, custom domain attach, and the status page's public |
| 242 | surface (a sidecar's existence is not public information). |
| 243 | |
| 244 | ### 6.2 It has no listener (contract) |
| 245 | |
| 246 | A sidecar binds nothing. The platform never dials it — there is no health |
| 247 | probe, no admin socket, no next-due probe. Everything the platform learns |
| 248 | from a sidecar, the sidecar tells it by dialing **out** (§7.1). This is the |
| 249 | property that lets the identical artifact run off-platform behind NAT, |
| 250 | which the platform spec §5.4 names as the whole point of the class. |
| 251 | |
| 252 | ### 6.2a The fleet-binding chain must be closed — this one is not theoretical |
| 253 | |
| 254 | A Kind-blind chain exists today from a resident route to a servable edge |
| 255 | upstream, and **Eleven's sidecars sit exactly in it**, because they are |
| 256 | fleet-placed (`region: "fleet/eleven"`): |
| 257 | |
| 258 | 1. `publishResidentFleetBindings` (`internal/host/fleetbind.go:301-316`) |
| 259 | iterates **every** route with no Kind filter and publishes a fleet |
| 260 | binding claiming this box serves that host. |
| 261 | 2. `ReconcileFleetBindings` (`fleetbind.go:210-255`) reads it back, checks |
| 262 | `registry.FleetPlacement(rt.Region)` — again no Kind filter — and stamps |
| 263 | `SetFleetBox`. |
| 264 | 3. The edge's dispatch switch has a `case isFleetPlaced && e.tunnels != nil` |
| 265 | arm (`internal/edge/edge.go:379-383`) with **no Kind check**, which |
| 266 | builds a live tunnel proxy. |
| 267 | |
| 268 | A sidecar row is unroutable today only because it has no Socket, no Addr |
| 269 | and no Tunnel — it falls to `default: continue` (`edge.go:445`) and is |
| 270 | never inserted into the routing map. That is omission, not refusal. Put a |
| 271 | fleet placement on it, which is precisely what Eleven's records carry, and |
| 272 | step 3's arm is reached on shape alone. |
| 273 | |
| 274 | So the guard is added at **both** ends, and both are tested: |
| 275 | |
| 276 | - `publishResidentFleetBindings` skips `Kind == "sidecar"` — a sidecar is |
| 277 | never advertised as served by any box. |
| 278 | - the edge refuses `Kind == "sidecar"` **before** the dispatch switch, so |
| 279 | no present or future arm of it can bind one. |
| 280 | |
| 281 | The edge-side refusal is the one that must hold on its own: it is the |
| 282 | assertion that survives someone later adding a column, an arm, or a |
| 283 | placement that reaches a sidecar row by shape. |
| 284 | |
| 285 | ### 6.3 The box need not be reachable at all (placement) |
| 286 | |
| 287 | §8. A box that accepts only sidecar work needs no certs, no inbound ports, |
| 288 | no tunnel, and can sit in a private subnet with egress-only networking. |
| 289 | |
| 290 | ## 7. Lifecycle |
| 291 | |
| 292 | A sidecar is an activator-managed child, like a hibernating instance, with |
| 293 | the probe-shaped parts replaced by an outbound protocol. |
| 294 | |
| 295 | ### 7.1 The agent socket — the only channel, and it points outward |
| 296 | |
| 297 | The agent listens on a unix socket; children dial it. **Nothing ever dials |
| 298 | a sidecar.** |
| 299 | |
| 300 | /run/carlos/agent.sock 0660 carlos:carlos |
| 301 | |
| 302 | Exec children all run as the agent's user, so the socket's file mode is not |
| 303 | an identity boundary. Identity is a **per-wake token** the agent mints and |
| 304 | passes in the child's environment — the `CARLOS_ADMIN_TOKEN` idiom |
| 305 | inverted: |
| 306 | |
| 307 | CARLOS_AGENT_SOCKET=/run/carlos/agent.sock |
| 308 | CARLOS_SIDECAR_TOKEN=<per-wake, per-sidecar, never persisted> |
| 309 | |
| 310 | The token is scoped to one sidecar for one wake: it authorizes calls about |
| 311 | that host only, and it dies when the child is stopped. A stale token from a |
| 312 | previous wake is refused. |
| 313 | |
| 314 | Verbs (all `POST`, all JSON, all tiny): |
| 315 | |
| 316 | | Verb | Meaning | |
| 317 | |---|---| |
| 318 | | `/v1/ready` | "I am up." Ends the settle window; carries the running version (§9.2). | |
| 319 | | `/v1/next-due` `{"at": <unix>}` | "Wake me at T." Recorded in `routes.next_due`. | |
| 320 | | `/v1/idle` | "I have nothing to do." The agent stops me. | |
| 321 | | `/v1/heartbeat` | Optional liveness for a long-running sidecar. | |
| 322 | |
| 323 | An app that implements none of them still works: it runs until `max_run` |
| 324 | or until the record is deleted. `idle` is what makes hibernation actually |
| 325 | cheap, and `next-due` is what makes a schedule survive sleep. |
| 326 | |
| 327 | ### 7.2 Wake, run, sleep |
| 328 | |
| 329 | **Wake** (any source): claim the per-host S3 lease → restore the DB from |
| 330 | the bucket (`fresh` computed as today) → seed if fresh and a seed is |
| 331 | declared → mint the wake token → spawn `live/<host> <args...>` in the |
| 332 | host's cgroup, output teed to the log sink → **settle window** instead of a |
| 333 | health probe: the child is considered up when it either calls `/v1/ready` |
| 334 | or survives `sidecarSettle` (10s) without exiting. A child that exits |
| 335 | non-zero inside the settle window is a failed wake, released and backed off |
| 336 | like a failed instance start. |
| 337 | |
| 338 | **The seam this needs.** `doWake`'s step 4 calls `Backend.Start`, and |
| 339 | `ExecBackend.Start` ends with an **unconditional** |
| 340 | `waitHealthyCtx(ctx, rt.Socket, spawnHealthTimeout)` |
| 341 | (`internal/activator/backend_exec.go:601`) — there is no branch for a route |
| 342 | with no socket. Handed `Socket ""` it would dial `unix://""` every 150ms for |
| 343 | the full 30s and then fail the wake. So the `Backend` seam gains one method |
| 344 | rather than the existing one growing a mode flag: |
| 345 | |
| 346 | ```go |
| 347 | // StartWorker spawns a child that binds nothing. No health probe is |
| 348 | // possible or wanted: readiness is the settle window, or the child's own |
| 349 | // call to /v1/ready over the agent socket. |
| 350 | StartWorker(ctx context.Context, rt registry.Route, tok string) (Proc, string, error) |
| 351 | ``` |
| 352 | |
| 353 | `ExecBackend.StartWorker` is `Start`'s body with the health wait and the |
| 354 | socket-mode normalization removed and `args` in place of |
| 355 | `--socket/--db` — which is very nearly what `StartSidecar` already is |
| 356 | (`backend_exec.go:746-773`), the one spawn path in the package that |
| 357 | deliberately has no health wait. `UnitBackend.StartWorker` refuses, exactly |
| 358 | as it refuses `StartSidecar` today. |
| 359 | |
| 360 | **Run**: supervised. A crash restarts with the existing exponential |
| 361 | backoff (2s → 1m, reset after a healthy minute) — the constants the |
| 362 | co-spawn path already uses, now applied to a standalone member. |
| 363 | |
| 364 | A hibernating worker's exit is read by **exit status**, the only thing that |
| 365 | separates the two meanings it can carry (final review, 2026-08-17): a clean |
| 366 | exit is "my work is done" and parks; a non-zero exit is a crash and restarts |
| 367 | with that same backoff, exactly as an always-on worker's does. Without the |
| 368 | distinction, a bot that fell over before setting a next-due slept forever — |
| 369 | nothing can dial one. |
| 370 | |
| 371 | A **fired** due time is consumed by the wake it causes, never by the sleep |
| 372 | that follows (final review, 2026-08-17): a worker's stop writes no |
| 373 | `next_due` at all, so otherwise the passed value is re-served on every sweep |
| 374 | and the sidecar wakes, idles, parks and re-wakes forever. The consumption is |
| 375 | conditional on the row still holding the value that fired, so a next-due the |
| 376 | child posted during its own wake survives — and a worker that sleeps BEFORE |
| 377 | its timer fires keeps its schedule untouched. |
| 378 | |
| 379 | **Sleep**, triggered by `/v1/idle`, by `max_run`, or by the idle timer |
| 380 | (no `/v1/heartbeat` and no agent-socket call within `idle`): |
| 381 | SIGTERM (5s grace, then SIGKILL) → record `next_due` from whatever the |
| 382 | sidecar last declared → park the DB to the bucket → release the lease. |
| 383 | The ordering is the instance stop path's, minus the next-due probe: a |
| 384 | sidecar's next-due arrived over the socket while it was alive, which is |
| 385 | strictly better than asking a process we are about to kill. |
| 386 | |
| 387 | **Always-on** is `hibernate: false`: the record exists ⇒ the process runs, |
| 388 | same supervision, no idle clock, no lease churn. Two consequences of that |
| 389 | rule, both from the final review (2026-08-17): the sweep starts an always-on |
| 390 | worker that has no live entry — otherwise one created while the agent was |
| 391 | running never ran at all, since the boot pass runs once, at arming — and |
| 392 | `max_run` applies to it too (it is the wedged-reconnect-loop guard, and a |
| 393 | long-lived worker is exactly that shape), but is spent there as a RECYCLE of |
| 394 | its child on the same entry, keeping the lease, because stopping an |
| 395 | always-on worker leaves nothing to start it again. |
| 396 | |
| 397 | ### 7.3 Where this lives: inside the activator, off its request path |
| 398 | |
| 399 | A sidecar's lifecycle is ~80% of `doWake`/`stop` already: acquire the lease, |
| 400 | restore, seed, spawn, replicate; then stop, park, release. Reimplementing |
| 401 | that in a sibling package would duplicate exactly the code where bugs are |
| 402 | expensive — the error paths that must release a lease. So sidecars reuse |
| 403 | `activator.Service` and its `actEntry`, with the request path left alone: |
| 404 | |
| 405 | - **Nothing routes to a sidecar, so `beginRequest`, `BeginSidecarRequest` |
| 406 | and `ServeHTTP` never see one.** The edge refuses the row first (§6.1), |
| 407 | and `ServeHTTP` already gates on `rt.Kind == "instance" && rt.Hibernate` |
| 408 | (`activator.go:1128`), so it is unreachable there twice over. A new |
| 409 | `WakeWorker(host)` entry point creates the entry and runs the same |
| 410 | `wake`/`doWake` machinery the sweeper's request-less due-time wake uses. |
| 411 | - **The idle clock is reused verbatim, with a different input.** For a |
| 412 | sidecar `inflight` is always 0 and `lastActive` is stamped by **agent |
| 413 | socket contact** instead of by a released HTTP request. The existing |
| 414 | predicate — `inflight == 0 && now-lastActive >= idle` |
| 415 | (`activator.go:927`) — then means exactly the right thing with no new |
| 416 | branch, and `/v1/idle` is a stop requested immediately rather than a |
| 417 | timestamp nudged backwards. |
| 418 | - **`stop` skips only the next-due probe** (`activator.go:820-845`): a |
| 419 | sidecar's next-due arrived over the socket while it was alive, which is |
| 420 | strictly better than asking a process we are about to kill. Everything |
| 421 | after it — stop, final checkpoint, lease release — is shared unchanged. |
| 422 | - **`max_run`** is one more sweep predicate beside the idle one. |
| 423 | |
| 424 | The co-spawn fields on `actEntry` (`sidecar`, `sidecarWanted`, …) are |
| 425 | untouched: a standalone sidecar is the entry's own `proc`, not a passenger |
| 426 | on someone else's. |
| 427 | |
| 428 | ### 7.4 Wake sources built here |
| 429 | |
| 430 | 1. **Timer** — the sweeper's existing due-time wake, unchanged: a sleeping |
| 431 | host whose `next_due` has arrived is woken with no client request. The |
| 432 | machinery already exists for instances; a sidecar is simply a host that |
| 433 | only ever wakes this way. |
| 434 | 2. **Operator/app command** — `carlos sidecars wake`, pull-based: the |
| 435 | console bumps the wake generation (§4.3) and the serial; every box |
| 436 | serving that app notices within seconds (the `carlos restart` pattern |
| 437 | verbatim) and wakes the named sidecar. Nothing is pushed at a box — |
| 438 | which is what lets the box live in a private subnet. |
| 439 | |
| 440 | The box's etag memory is seeded once, at arming, and that seeding is |
| 441 | what keeps an agent restart from replaying every wake in history. It is |
| 442 | scoped to arming precisely (final review, 2026-08-17): a sidecar route |
| 443 | seen for the first time AFTER that baseline has just been minted, and a |
| 444 | wake object already standing at it is a wake somebody asked for and the |
| 445 | CLI already reported as accepted — the create-then-wake flow, which is |
| 446 | the feature's primary one, since an operator wake is a hibernating |
| 447 | sidecar's only first-run mechanism. A spurious extra wake is idempotent |
| 448 | and cheap; a lost one is a bot that never runs. |
| 449 | 3. **Boot** — a non-hibernating sidecar starts with the agent. |
| 450 | |
| 451 | ### 7.5 Delivery wake — designed, not built |
| 452 | |
| 453 | The spec's third wake source is the edge observing (metadata only) that |
| 454 | ciphertext is queued for a sidecar's identity and signalling the host |
| 455 | agent. It is out of scope here and needs: an identity→sidecar mapping the |
| 456 | edge can consult without learning content, a signalling path to a box that |
| 457 | may have no inbound port (so: the same pull-based generation object as §7.4, |
| 458 | written by the edge instead of the console), and a rate discipline so a |
| 459 | chatty correspondent cannot bill a member for continuous wakes. Recorded so |
| 460 | the shape is not invented differently later. |
| 461 | |
| 462 | ## 8. Placement: boxes that accept no-inbound work |
| 463 | |
| 464 | Two halves that must agree. |
| 465 | |
| 466 | **The box declares.** A new env var, read where `CARLOS_REGION`/ |
| 467 | `CARLOS_POOLS` are read (`internal/host/agent.go:1317`): |
| 468 | |
| 469 | CARLOS_INBOUND=none # this box serves sidecar work only |
| 470 | |
| 471 | Unset/`any` is today's behaviour exactly — an inert default, the |
| 472 | apps-domain-style gate this repo already uses. On a `none` box: |
| 473 | |
| 474 | - the provisioning reconciler **refuses to mint routed instance rows**, |
| 475 | loudly. A box that cannot serve must never be handed something to serve; |
| 476 | this is the guard that turns a misfiled record into a log line instead of |
| 477 | a silent outage. |
| 478 | - the edge and cert machinery are not required to be running at all. |
| 479 | |
| 480 | **The record asks.** `placement: "no-inbound"` on the manifest/record makes |
| 481 | the sidecar reconciler mint **only** on boxes declaring `CARLOS_INBOUND=none`. |
| 482 | `placement: "any"` (default) places by pool/region alone. Where the two |
| 483 | disagree, the **most restrictive wins** (final review, 2026-08-17): a record |
| 484 | may narrow an app that asks for nothing, but a record stating `"any"` under a |
| 485 | `no-inbound` manifest does not widen it — that is the direction the console |
| 486 | door already refuses, and a direct bucket write must not reach around it. |
| 487 | |
| 488 | The two are independent on purpose: `none` is a property of the box's |
| 489 | network, `no-inbound` is a requirement of the workload. A member who wants |
| 490 | "this bot must only ever run somewhere with no inbound path" gets a |
| 491 | mechanical guarantee rather than an operator's promise. |
| 492 | |
| 493 | **Infra is written, not applied** (Paul's ruling): a runbook for a |
| 494 | private-subnet box — no IGW route, egress via NAT, no inbound SG rules, no |
| 495 | SSM — lands beside `infra/eleven-fleet.md` for Paul's own applies. Note |
| 496 | that fleet boxes are *already* outbound-only dial (`infra/eleven-fleet.md`), |
| 497 | so Eleven's first sidecar box needs no new network shape at all: the |
| 498 | private-subnet variant is the harder posture, available when wanted. |
| 499 | |
| 500 | ## 9. The rest of the platform |
| 501 | |
| 502 | ### 9.1 Config delivery |
| 503 | |
| 504 | Sidecars get their env files the way instances do — `execRouteBase` |
| 505 | (`internal/appconfig/syncexec.go:404`) widens to include `Kind == "sidecar"`. |
| 506 | This is the direct replacement for the hand-staged `<host>-sidecar.env`: |
| 507 | `carlos env set` reaches a sidecar, secrets are sealed and delivered the |
| 508 | same way, and the 2s serial-driven convergence applies unchanged. |
| 509 | |
| 510 | ### 9.2 Version visibility without a header |
| 511 | |
| 512 | `carlos deploy` verifies a roll by reading `X-Carlos-Version` off the URL. |
| 513 | A sidecar has no URL. The substitute is the `/v1/ready` call: the sidecar |
| 514 | reports the version it is running, the agent records it on the row, and |
| 515 | `carlos sidecars ls` shows *running* version against *desired* channel |
| 516 | version. A sidecar that never calls `/v1/ready` shows its version as |
| 517 | unknown rather than as current — the honesty rule this repo has paid for: |
| 518 | a stamp that overclaims is worse than an absent one. |
| 519 | |
| 520 | ### 9.3 Logs, metrics, restart |
| 521 | |
| 522 | **Correct for free**, because they key on host or on the cgroup name rather |
| 523 | than on Kind: the log sink names its file by host; `internal/cgroup`'s |
| 524 | `carlos-exec@<host>` convention means `listExecCgroups` |
| 525 | (`internal/host/metrics_probes.go:240-261`) samples a sidecar's CPU and |
| 526 | memory the moment it spawns into that cgroup, with no code change; and |
| 527 | `ops.FleetDBs`'s `claimed` map (`internal/ops/litestream.go:371-379`) is |
| 528 | Kind-agnostic, so a sidecar's `DBPath` is replicated like any other. |
| 529 | |
| 530 | **Needs widening**: `carlos restart -app X` is gated on |
| 531 | `release.Adoptable` (`internal/release/adopt.go:518-521`, via |
| 532 | `internal/host/restart.go:77,192,545`), which requires `Kind == "instance"`. |
| 533 | Sidecars are cycled by the restart pass too, so `restart.go` widens. |
| 534 | |
| 535 | **`release.Adoptable` itself does not change.** It is the most reused |
| 536 | predicate in the repo — adopt membership, capability convergence, socket |
| 537 | and backing advisories, status pages all inherit it, and every one of those |
| 538 | is *correct* to exclude a sidecar. Widening it centrally would silently |
| 539 | enrol sidecars in capability drop-ins and adopt bookkeeping that make no |
| 540 | sense for them. Each consumer that genuinely wants sidecars opts in by |
| 541 | name: the config-delivery predicate (§9.1) and the restart pass. That is |
| 542 | two deliberate edits instead of one blind one. |
| 543 | |
| 544 | ### 9.4 Consumers that need a guard, and why |
| 545 | |
| 546 | The exhaustive list, from a sweep of every `.Kind` branch and every route |
| 547 | iteration in the tree. Each is a test in this branch. |
| 548 | |
| 549 | | Site | Today with a sidecar row | Change | |
| 550 | |---|---|---| |
| 551 | | `edge.go:364-445` dispatch | unroutable by omission (`default: continue`) | explicit Kind refusal before the switch (§6.1) | |
| 552 | | `fleetbind.go:301-316` publish | advertises a sidecar as served | skip `Kind == "sidecar"` (§6.2a) | |
| 553 | | `provision/reconcile.go:651-673` removal | **deletes every sidecar row, every pass** | see below | |
| 554 | | `registry.go:1341` `DueInstances` | timer wakes never fire | `kind IN ('instance','sidecar')` (§5) | |
| 555 | | `appconfig/syncexec.go:404` | no env file ever written | include sidecars (§9.1) | |
| 556 | | `host/restart.go` (via `Adoptable`) | never cycled | include sidecars (§9.3) | |
| 557 | | `console/instances.go:88-97` | can pick a sidecar as an app's *reporting host*, making a healthy app read "not reporting" | filter by Kind | |
| 558 | | `status/snapshot.go:406-427` `CollectBox` | falls to `default:` → counted as a public **Resident** instance | explicit branch; sidecars are not public | |
| 559 | | `console/overview.go:286-306` `healthWord` | renders "Serving", which is false | a sidecar's own words (§9.5) | |
| 560 | | `console/overview.go:129-144` | `InstanceTotal` counts sidecars | count separately | |
| 561 | | `cli.go:75,194-199` `carlos add -kind` | **no validation at all** — accepts `-kind sidecar` today | rail it off (§3) | |
| 562 | |
| 563 | **The removal-phase collision is the sharpest of these.** The instance |
| 564 | reconciler's removal phase (`reconcile.go:651-673`) is scoped by |
| 565 | `Provisioned`, region, pool and absence-from-records — with **no Kind |
| 566 | check**. Sidecar rows are `Provisioned` (§5) and can never appear in the |
| 567 | instance record set, so the existing pass would delete every sidecar on |
| 568 | every tick: a silent, self-healing-looking loop where sidecars are minted |
| 569 | and destroyed forever, with no error anywhere. |
| 570 | |
| 571 | The fix is that each pass's removal authority is scoped to **its own Kind**: |
| 572 | the instance pass skips `Kind == "sidecar"`, the sidecar pass touches only |
| 573 | sidecar rows. Both halves are asserted directly, because this is the |
| 574 | failure mode with no symptom other than work quietly disappearing. |
| 575 | |
| 576 | ### 9.5 What a sidecar's status reads |
| 577 | |
| 578 | A sidecar never serves a request, so the vocabulary the console uses for |
| 579 | routes is wrong for it. It gets its own words — `Running`, `Asleep`, |
| 580 | `Waking`, `Crashed`, `Never started` — and is counted in its own bucket |
| 581 | rather than folded into an instance count. Sidecars are **not** on the |
| 582 | public status page: their existence is account-visible, not public. |
| 583 | |
| 584 | ### 9.6 Object storage, ledgers, economics |
| 585 | |
| 586 | A sidecar is an ordinary tenant for the object-store primitive and accrues |
| 587 | usage like any exec child. No new meters. |
| 588 | |
| 589 | ## 10. The existing co-spawn stays |
| 590 | |
| 591 | `HasSidecar`/`StartSidecar` on the file-staged path are **not removed**. |
| 592 | Eleven runs on that shape today, and this branch does not touch a live box. |
| 593 | The relationship is stated in code and docs: the file path is legacy, the |
| 594 | record path is native, and a host may not have both (the reconciler refuses |
| 595 | to mint a sidecar row for a host that already has a staged |
| 596 | `<host>-sidecar.env`, naming the conflict). Retiring the file path is a |
| 597 | follow-up issue, gated on Eleven's own migration. |
| 598 | |
| 599 | ## 11. Console API and CLI |
| 600 | |
| 601 | Verbs under `registerCLIRoutes`, bearer, JSON, mirroring the instances |
| 602 | family including its refusal ordering (principal gate → rate limit → |
| 603 | manifest present → name shape → region shape/allow-list → channel → the |
| 604 | create-only write) and its byte-identical-404 discipline: |
| 605 | |
| 606 | GET /api/cli/apps/{account}/{app}/sidecars |
| 607 | GET /api/cli/apps/{account}/{app}/sidecars/{name} |
| 608 | POST /api/cli/apps/{account}/{app}/sidecars |
| 609 | DELETE /api/cli/apps/{account}/{app}/sidecars/{name} |
| 610 | POST /api/cli/apps/{account}/{app}/sidecars/{name}/wake |
| 611 | |
| 612 | The **app-held token** kind that authorizes an app's own instance verbs |
| 613 | extends to its own sidecar verbs — this is the "apps can provision sidecar |
| 614 | instances easily" requirement: Eleven's app process, holding no AWS |
| 615 | credentials and no box access, creates a bot per enrolled member. |
| 616 | |
| 617 | CLI, the `instances` family's shape: |
| 618 | |
| 619 | carlos sidecars enable -app eleven -args 'sidecar run' [-placement no-inbound] |
| 620 | carlos sidecars create -app eleven bot-acme |
| 621 | carlos sidecars ls -app eleven |
| 622 | carlos sidecars wake -app eleven bot-acme |
| 623 | carlos sidecars rm -app eleven bot-acme |
| 624 | carlos sidecars logs -app eleven bot-acme |
| 625 | |
| 626 | Every one of them completes its job with no box access and verifies its own |
| 627 | result, per the no-infra rule. |
| 628 | |
| 629 | ## 12. Testing |
| 630 | |
| 631 | - **Unreachability is a test, not a claim**: a `Kind == "sidecar"` row is |
| 632 | never servable by the edge, never selected as an upstream, never aliased, |
| 633 | never attachable to a domain, never on the status page — asserted |
| 634 | directly, including the adversarial case of a sidecar row carrying a |
| 635 | socket. |
| 636 | - **Reserved suffix**: every door that mints or accepts a host refuses |
| 637 | `.sidecar.internal`. |
| 638 | - **Lifecycle**: wake → ready → next-due → idle → sleep → timer wake, on |
| 639 | the activator's existing fake clock/fake backend harness; crash-backoff; |
| 640 | settle-window failure; `max_run`; lease release on every exit path. |
| 641 | - **Agent socket**: token scoping (a token for host A cannot speak about |
| 642 | host B), stale-token refusal after a stop, malformed input, and the |
| 643 | socket's absence being survivable. |
| 644 | - **Reconciler**: mint/converge/remove with the fail-safe (a read failure |
| 645 | skips the removal phase), placement matching, and the co-spawn conflict |
| 646 | refusal. |
| 647 | - **Additive parse**: a route row and a bucket record written before this |
| 648 | change still parse. |
| 649 | - The gate: `go build ./...`, `go vet ./...`, `go test ./... -count=1`, |
| 650 | `gofmt -l` clean, plus the `GOOS=darwin` compile. |
| 651 | |
| 652 | ## 13. Explicitly out of scope |
| 653 | |
| 654 | - Delivery-wake (§7.5). |
| 655 | - Retiring the file-staged co-spawn (§10). |
| 656 | - Applying any infra (§8) or touching any live box. |
| 657 | - Sidecars for unit-backed tenants — `UnitBackend` refuses agent-managed |
| 658 | sidecars today and continues to; the capabilities spec already names this |
| 659 | a non-goal. |
| 660 | - Multi-box placement/failover for a sidecar: one record, one lease, one |
| 661 | box at a time, exactly as instances work. |
| 662 | |
| 663 | ## 14. Amended 2026-09-05 — the sweep, redone against 600 commits of main |
| 664 | |
| 665 | §9.4's consumer sweep was exhaustive when it was written, against main at |
| 666 | `d7077d1`. This branch then sat unmerged from 2026-08-18 while main moved 600 |
| 667 | commits, and the sweep was re-run on the merge. It is recorded here rather |
| 668 | than folded into §9.4 because the *lesson* is the interesting part: the |
| 669 | consumer list is not a fact about the tree, it is a fact about the tree ON A |
| 670 | DATE. Two of the three new consumers were built by people who had no reason |
| 671 | to know an unroutable serving class was coming. |
| 672 | |
| 673 | ### 14.1 The edges file publishes every served row, and that is now wrong |
| 674 | |
| 675 | `internal/host/edgesfile.go` was added to main after this branch. Every box |
| 676 | publishes `control/edges/<box>.json` naming the hosts it homes, and its own |
| 677 | comment states the rule it was built to: *every SERVED row on this box, ALL |
| 678 | kinds — the serving-class split belongs to the reader, not to this writer.* |
| 679 | |
| 680 | That rule was coherent when every kind was servable. A sidecar row is |
| 681 | `Status: "ready"` — that is what makes it runnable — so it passed the only |
| 682 | filter and was published as `role: "home"`, which is the assertion *this box |
| 683 | serves that host*. Three consumers act on it: |
| 684 | |
| 685 | - **`edge.backhaul`** (`internal/edge/edge.go`, via `homeMap`). This is the |
| 686 | sharp one, and it is sharp because the guarantee this design leans on |
| 687 | cannot reach it. §6.1's edge refusal lives in `Reload`, which builds THIS |
| 688 | box's routing map out of rows THIS box holds. A remote edge holds no |
| 689 | sidecar row and never sees a `Kind` at all. It misses on its own upstream |
| 690 | lookup, reads this file, and dials the home box with the sidecar's own name |
| 691 | as SNI — a live proxy into the one member the platform guarantees nothing |
| 692 | can dial. |
| 693 | - **`deploymentHost`** = `Allowed(host) || homes.KnownHost(host)`, which is |
| 694 | what admits a name to the cert machinery. A published sidecar becomes |
| 695 | orderable deployment-wide. |
| 696 | - **`edgesfile.DeploymentRoutes`**, which feeds the alias pass's |
| 697 | "routes always win" rule fleet-wide. |
| 698 | |
| 699 | The guard therefore belongs to the WRITER, exactly as §6.2a puts |
| 700 | `publishResidentFleetBindings`' there: *a sidecar is never advertised as |
| 701 | served, by this box or any other.* Pinned by |
| 702 | `TestEdgesFileNeverPublishesASidecar`, which fails with the guard removed — |
| 703 | the row comes back `{Role: home}`. |
| 704 | |
| 705 | ### 14.2 Path-based routing mints into a third table |
| 706 | |
| 707 | `ReconcileAliases` derived two tables when §3's reserved-suffix guard was |
| 708 | written. Main added `route_paths`, from the same claims, through a branch the |
| 709 | guard never sees. Both ends are now refused, and they are not the same |
| 710 | finding twice: |
| 711 | |
| 712 | - the **host** end is the alias guard's argument verbatim: a path row keyed on |
| 713 | a reserved-suffix name puts that name in the routing map, and edge.go's |
| 714 | Kind check never sees it, because the row it reads is a path row. |
| 715 | - the **target** end has nothing else standing in front of it. The |
| 716 | cross-tenant check passes a sidecar, because a sidecar belongs to the |
| 717 | claim's own account — that is the ordinary case, not an edge one. The |
| 718 | readiness check passes it, because a live sidecar row is ready. So the |
| 719 | target field is the one place in this pass where an account can route real |
| 720 | traffic into its own undialable member, and only a check on the name closes |
| 721 | it. Removing the guard mints `bot -> bot.eleven.bdm.sidecar.internal`. |
| 722 | |
| 723 | ### 14.3 Two gaps recorded, deliberately not closed here |
| 724 | |
| 725 | Both are in the safe direction — a sidecar is left alone rather than acted on |
| 726 | wrongly — and both are follow-ups rather than merge work. |
| 727 | |
| 728 | - **Narrowed restarts reach no sidecar.** `carlos restart --host` (main, |
| 729 | after this branch) resolves hosts through `scopedHosts`, which gates on |
| 730 | `release.Adoptable`, which is `Kind == "instance"` and stays that way for |
| 731 | §9.3's reason. So a narrowed restart naming a sidecar cycles nothing. That |
| 732 | is the right failure — a scoped bounce with no wake beside it would strand |
| 733 | a worker, which is precisely what `cycleWorkers` exists to prevent — but |
| 734 | reaching a sidecar by name wants a selector of its own. |
| 735 | - **A trashed app's sidecar rows are not retired.** #340 gave the instance |
| 736 | pass a second answer off the trash marker (`removable`, guarded by the |
| 737 | production flag) and moved retirement onto every box. The sidecar pass keeps |
| 738 | the mint-only predicate: retirement is not a question it can answer without |
| 739 | inheriting an authority its removal phase was not designed around. A |
| 740 | trashed app's sidecars therefore linger until their records go — the |
| 741 | pre-#340 behaviour for everything. |
| 742 | |
| 743 | ### 14.4 Checked and correct, for the record |
| 744 | |
| 745 | `internal/schedule` gates on `Kind != "instance"`. `internal/steering` selects |
| 746 | on the `Steering` column, which no sidecar record can set. `movespass` is |
| 747 | instance-only. `status/fleet.go` filters by kind. Metrics and the economics |
| 748 | pass attribute a sidecar's cgroup like any other exec child, which is §9.6's |
| 749 | stated intent, not an oversight. |
| 750 | |
| 751 | One vocabulary inconsistency is left standing: `carlos status --instances` |
| 752 | matches units to routes through `routeForUnit`, which matches a sidecar's |
| 753 | `carlos-exec@<host>` cgroup, so a running sidecar is listed there under |
| 754 | "instances" rather than in the separate bucket §9.5 gives it. The door is |
| 755 | authenticated and account-scoped, so nothing leaks; it is a naming fix, and |
| 756 | changing the shape of a status API is not merge work. |
| 757 | |
| 758 | ## 15. Amended 2026-09-15 — the sweep, redone AGAIN, 405 commits later |
| 759 | |
| 760 | §14 was written against main at `66587fa8` on 2026-09-05 and was exhaustive |
| 761 | then. Main moved 405 commits in the ten days that followed, and the branch |
| 762 | went conflicted rather than merely stale a second time. The sweep was rerun |
| 763 | from scratch, on the principle §14 had already arrived at: a consumer list |
| 764 | is not a fact about the tree, it is a fact about the tree ON A DATE, and the |
| 765 | only way to keep one true is to stop trusting it and look again. |
| 766 | |
| 767 | One new consumer reaches a sidecar. It is worth reading the shape of it, |
| 768 | because it is the THIRD instance of one pattern and that pattern is now |
| 769 | the thing to watch for rather than any of its three instances. |
| 770 | |
| 771 | ### 15.1 The domain-attach door aliases onto a sidecar |
| 772 | |
| 773 | `internal/console/domainattach.go`'s `readyRouteHost` picks the route a |
| 774 | newly-attached custom domain is aliased onto. Its own doc says the selection |
| 775 | is `domains.firstReadyUpstream`'s "minus the upstream copy" — and that is |
| 776 | accurate about the intent and wrong about the consequence. |
| 777 | |
| 778 | `firstReadyUpstream` ends with a clause this one drops: |
| 779 | |
| 780 | if rt.Socket != "" || rt.Addr != "" || (rt.Kind == "static" && rt.Channel != "") |
| 781 | |
| 782 | That clause is doing two jobs. It selects a row with something to copy — |
| 783 | the job it was written for, and the job `readyRouteHost` genuinely does not |
| 784 | need, since it copies nothing and returns only the host. It is ALSO the |
| 785 | question "is this row dialable at all", and that job nobody assigned it is |
| 786 | the entire reason a sidecar never appears in `firstReadyUpstream`'s answer: |
| 787 | a sidecar is precisely the row with no socket and no addr. |
| 788 | |
| 789 | So dropping the clause dropped the guard with it, and every test that |
| 790 | remains passes a sidecar. It belongs to the app. It belongs to the account. |
| 791 | It is not `Platform`. It is `ready` — because ready is what makes it |
| 792 | runnable. Selection is in `reg.List`'s host order, so `bot.<app>...` sorts |
| 793 | ahead of most hosts an app would really serve from: the sidecar is not a |
| 794 | straggler that wins a tie, it is the first candidate. |
| 795 | |
| 796 | The result is not a routing breach — `ReconcileAliases` refuses a claim |
| 797 | whose `Route` carries the reserved suffix (the guard at aliases.go's alias |
| 798 | branch, §14's sibling). It is worse in a quieter way: the member's attach |
| 799 | returns 200, the claim is written naming a sidecar, and the box then refuses |
| 800 | that claim on every pass, forever. A custom domain that never serves and |
| 801 | nothing anywhere says why. |
| 802 | |
| 803 | Guard added at the door, so the console agrees with the box. Verified by |
| 804 | neutralising it: the attach aliases `echo.example.com` onto |
| 805 | `bot.story.bab.sidecar.internal`. |
| 806 | |
| 807 | ### 15.2 The pattern, which is the actual finding |
| 808 | |
| 809 | Three sweeps have now found the same thing three times: |
| 810 | |
| 811 | - §14.1, the edges file: a writer that published "every served row, ALL |
| 812 | kinds", true only while every kind was servable. |
| 813 | - §14.2, path routing: a third table derived from the same claims, through |
| 814 | a branch the reserved-suffix guard never saw. |
| 815 | - §15.1, domain attach: a selection copied from a guarded one, minus the |
| 816 | clause that was the guard. |
| 817 | |
| 818 | None of the three was written carelessly and none is a sidecar bug. Each is |
| 819 | code written before an unroutable serving class existed, by someone with no |
| 820 | reason to imagine one. The common shape is a predicate that EXCLUDED |
| 821 | sidecars as a side effect of asking something else — "has an upstream to |
| 822 | copy", "is a row this box serves" — being narrowed, copied or re-derived by |
| 823 | someone who correctly identified the stated purpose and could not see the |
| 824 | unstated one. |
| 825 | |
| 826 | The practical consequence for whoever sweeps next: grep for |
| 827 | `registry.Route` is not enough, and neither is grepping for `Kind`. What |
| 828 | finds these is asking, of every consumer, "what in this predicate happens to |
| 829 | require a socket or an address?" — and then checking whether that clause is |
| 830 | load-bearing somewhere it was never documented to be. |
| 831 | |
| 832 | ### 15.3 Checked this pass and correct |
| 833 | |
| 834 | - `internal/gate` (new): the password curtain is applied at the edge, in |
| 835 | front of a route serving requests. The edge refuses a sidecar on Kind |
| 836 | before dispatch, so a `gate` hash on a sidecar row is inert. |
| 837 | - `apexHolder` (`internal/console/apex.go`, new): reads `reg.Get` at the |
| 838 | platform apex `<name>.<sqid>.<domain>`. A sidecar host is under |
| 839 | `.sidecar.internal`, a different suffix entirely, so the lookup can never |
| 840 | land on one. |
| 841 | - `edgesfile.DeploymentRoutes`: reads the published edges files, which |
| 842 | §14.1's writer-side guard has already emptied of sidecars. That guard |
| 843 | covers this consumer too, and is the reason it needs none of its own. |
| 844 | - `warnExternalSockets` (`internal/host/socket_advice.go`): explicit |
| 845 | `Kind != "instance"`. |
| 846 | - `versionsweep`: pins a sidecar's `Version`/`PrevVersion` against staged |
| 847 | reaping. Correct and wanted — a sidecar runs the app's binary and the |
| 848 | build it is running must not be swept out from under it. |
| 849 | - `schedule`'s prune: marks a sidecar's host alive, so its schedule rows |
| 850 | survive. Correct and load-bearing — a hibernating sidecar's schedule is |
| 851 | its only way back up. |
| 852 | - `ops/litestream.FleetDBs`: an always-on sidecar's database joins the |
| 853 | box-wide replication set like any other always-on row, which is right; a |
| 854 | hibernating one is excluded there because the activator replicates it |
| 855 | per-wake. |
| 856 | - None of the brand-new packages this pass (`analytics`, `clamd`, |
| 857 | `maintenance*`, `objstore`, `privatefile`, `publicrelease`, `relay`, |
| 858 | `telemetry`, `usage`) reads routes at all. |
| 859 | |
| 860 | ### 15.4 A gap this merge opened, recorded not closed |
| 861 | |
| 862 | Isolated exec (§8.5/§8.6 of its own design) gave `StartSidecar` a layout |
| 863 | dispatch: a nested box starts a sidecar as a unit through the control |
| 864 | channel, a fenced one refuses, and only the default falls through to the |
| 865 | child spawn. `StartWorker` — the NATIVE path, which is what a standalone |
| 866 | `Kind == "sidecar"` row uses — has no such dispatch and always spawns a |
| 867 | bare child. |
| 868 | |
| 869 | On a nested-layout box a native sidecar would therefore come up outside the |
| 870 | isolation every other child gets. It is not wrong today, because no box |
| 871 | runs both native sidecars and the nested layout, but it is exactly the kind |
| 872 | of agreement that stops being true quietly. The fix is `StartWorker` |
| 873 | learning the same dispatch, and it wants its own review rather than a |
| 874 | merge's. |
| 875 | |