carlos / platform Public

console: extract placeAppTarget — placement's mechanism in one place

handleAPIPlace's body was the only implementation of placement, and the
browser doors (the app Instances tab, the fleet page) need the same one:
the store-of-record write FIRST, then routes, then instance records, in
that order and no other. Extracted verbatim — the ORDER MATTERS and
TRIPWIRE comment blocks move WITH the code they guard, because they
guard the mechanism, not the door.

The return is a translation contract: (0, "") landed; a status with a
message is an ordinary refusal each caller renders in its own idiom; a
status with an EMPTY message is the one cross-account target refusal,
which every caller must answer as a byte-identical http.NotFound.

handleAPIPlace keeps what is genuinely the CLI API's: bearer resolution,
the write budget, the JSON body, the "(cli)" audit line. Its existing
tests pass unmodified — that is the proof this refactor changed nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell b0bdc8b143b29e4697a260f680f07607f01dbc95 parent cba3fc3
3 files changed, +86 −57
  • internal/console/apicli.go +2 −2
  • internal/console/fleets.go +83 −54
  • internal/console/server.go +1 −1
diff --git a/internal/console/apicli.go b/internal/console/apicli.go
index 32630b3a1..265d2a58c 100644
--- a/internal/console/apicli.go
+++ b/internal/console/apicli.go
@@ -151,7 +151,7 @@ func (s *Server) apiAccount(w http.ResponseWriter, r *http.Request) (string, *Ac
//
// The optional "place" field (2026-08-06 ruling: `carlos apps create -place
// fleet/<f>`) is validated with the EXACT SAME two checks
-// handleAPIPlace applies to its target — grammar (registry.FleetPlacement)
+// placeAppTarget applies to its target — grammar (registry.FleetPlacement)
// and that the fleet belongs to the CALLING account — before claimApp is
// ever called, and with the same refusal: an unknown-or-foreign fleet is
// the byte-identical 404 apiFleet gives everywhere else, never a 400 that
@@ -198,7 +198,7 @@ func (s *Server) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
return
}
if f == nil || f.Account != acct.Sqid {
- // Same byte-identical 404 apiFleet and handleAPIPlace's target
+ // Same byte-identical 404 apiFleet and placeAppTarget's target
// check both give an unknown-or-foreign fleet — see this
// handler's doc comment.
http.NotFound(w, r)
diff --git a/internal/console/fleets.go b/internal/console/fleets.go
index fb10e8dd9..46ced56bf 100644
--- a/internal/console/fleets.go
+++ b/internal/console/fleets.go
@@ -451,14 +451,14 @@ func (s *Server) handleAPIFleetDetachBox(w http.ResponseWriter, r *http.Request)
//
// 2026-08-06 ruling: the STORE's placement column (Store.AppPlacement) is
// consulted FIRST — the fact `carlos apps place` and `carlos apps create
-// -place` both write (handleAPIPlace, handleAPICreateApp) — because it is
+// -place` both write (placeAppTarget, handleAPICreateApp) — because it is
// the only source that exists the instant an app is placed with zero
// routes yet, which a route-derived reading structurally cannot answer (see
// the KNOWN LIMITATION this comment used to carry, below). Only when the
// store has never recorded a placement for this app (an app placed by an
// OLDER build, before this column existed, still carries its placement
// only in its routes) does this fall back to the original route-derived
-// reading: handleAPIPlace stamps every one of an app's routes with the SAME
+// reading: placeAppTarget stamps every one of an app's routes with the SAME
// target in one call, so by construction they are uniform once placed, and
// the first fleet-shaped Region found among them is authoritative.
//
@@ -466,7 +466,7 @@ func (s *Server) handleAPIFleetDetachBox(w http.ResponseWriter, r *http.Request)
// (apiinstances.go): a fleet-placed app's NEW instances must inherit its
// placement uniformly (spec's "one flag on the app, all its instances
// follow") rather than trusting whatever region a create request's body
-// happens to send — see handleAPIPlace's doc comment for why that
+// happens to send — see placeAppTarget's doc comment for why that
// inheritance has to be a separate mechanism from the route/record stamping
// below, not the same one.
func (s *Server) appPlacement(reg *registry.Registry, acct *Account, app string) (string, error) {
@@ -494,16 +494,33 @@ func (s *Server) appPlacement(reg *registry.Registry, acct *Account, app string)
return "", nil
}
-// handleAPIPlace answers `carlos apps place`: repoints where an app's routes
-// (and its already-provisioned instances) should run. target "" clears
-// placement back to platform-served/unconstrained; any other value must
-// parse as "fleet/<name>" (registry.FleetPlacement) naming a fleet the
-// CALLER'S OWN account owns — placing onto another account's fleet is
-// refused with the same byte-identical 404 apiFleet gives an unowned one,
-// since fleet names are a flat, cross-account namespace (see this file's
-// package doc comment).
+// placeAppTarget is placement's whole mechanism, and the ONLY place it
+// lives: repoints where an app's routes (and its already-provisioned
+// instances) should run. target "" clears placement back to
+// platform-served/unconstrained; any other value must parse as
+// "fleet/<name>" (registry.FleetPlacement) naming a fleet acct ITSELF owns —
+// placing onto another account's fleet is refused with the same
+// byte-identical 404 apiFleet gives an unowned one, since fleet names are a
+// flat, cross-account namespace (see this file's package doc comment).
//
-// THE TRIPWIRE this handler exists to close: placement is stored in
+// Every door onto placement calls this — the CLI API's handleAPIPlace, the
+// app Instances tab's handleAppPlace, the fleet page's
+// handleFleetPlacePage — because the ordering discipline below is the whole
+// correctness of the operation, and a second copy of it is a second place
+// for it to be got wrong. Callers own authentication, the app-ownership
+// gate, rate limiting, the audit line and how the outcome is rendered;
+// nothing else.
+//
+// The return is a translation contract, not an HTTP response: (0, "") means
+// it landed. A non-zero status with a MESSAGE is an ordinary refusal the
+// caller renders in its own idiom (jsonError, or a flash). A non-zero status
+// with an EMPTY message is exactly one case — http.StatusNotFound, the
+// cross-account target refusal — and it means the caller must answer with a
+// byte-identical http.NotFound, never a message of its own: a distinguishable
+// error there would turn this door into an oracle for which fleet names exist
+// in someone else's account.
+//
+// THE TRIPWIRE this function exists to close: placement is stored in
// Route.Region (SetPlacement) AND, separately, in every one of the app's
// already-provisioned instance records' Region field
// (provision.SetRecordRegion) — both are read by provision.Reconcile's
@@ -513,9 +530,9 @@ func (s *Server) appPlacement(reg *registry.Registry, acct *Account, app string)
// Region from the REQUEST BODY (handleAPIInstanceCreate, apiinstances.go),
// and provision.ValidRegion's shape (no "/") would refuse a "fleet/<name>"
// value outright if it somehow arrived that way. A new instance inheriting
-// placement is a SEPARATE mechanism (appPlacement, below, consulted by
+// placement is a SEPARATE mechanism (appPlacement, above, consulted by
// handleAPIInstanceCreate directly) — spec's "one flag on the app, all its
-// instances follow". What THIS handler's record write closes is the gap
+// instances follow". What THIS function's record write closes is the gap
// that inheritance cannot: every instance that ALREADY EXISTS at the moment
// placement changes. Inheritance only ever applies going forward, at
// create time, so without the write below an already-provisioned record
@@ -536,54 +553,35 @@ func (s *Server) appPlacement(reg *registry.Registry, acct *Account, app string)
// A console with no deployment bucket configured (cfg.Releases == nil)
// refuses the whole request rather than silently doing the routes-only
// half.
-func (s *Server) handleAPIPlace(w http.ResponseWriter, r *http.Request) {
- addr, acct, app, ok := s.apiAccountApp(w, r)
- if !ok {
- return
- }
- if !s.cli.write.allow(addr, time.Now()) {
- jsonError(w, http.StatusTooManyRequests, "too many requests — wait a minute and try again")
- return
- }
+func (s *Server) placeAppTarget(ctx context.Context, acct *Account, app, target string) (status int, errMsg string) {
if s.cfg.Releases == nil {
- jsonError(w, http.StatusServiceUnavailable,
- "no deployment bucket on this host — placement needs it to stamp this app's instances")
- return
- }
- var req struct {
- Target string `json:"target"`
+ return http.StatusServiceUnavailable,
+ "no deployment bucket on this host — placement needs it to stamp this app's instances"
}
- if !readJSON(w, r, &req) {
- return
- }
- target := strings.TrimSpace(req.Target)
reg, err := openRegistry(s.cfg.Registry)
if err != nil {
log.Printf("console: place %s/%s: open registry: %v", acct.Sqid, app, err)
- jsonError(w, http.StatusInternalServerError, "could not reach the registry")
- return
+ return http.StatusInternalServerError, "could not reach the registry"
}
if target != "" {
fleetName, validShape := registry.FleetPlacement(target)
if !validShape {
- jsonError(w, http.StatusBadRequest, `invalid target — want "" or "fleet/<name>"`)
- return
+ return http.StatusBadRequest, `invalid target — want "" or "fleet/<name>"`
}
f, err := reg.GetFleet(fleetName)
if err != nil {
log.Printf("console: place %s/%s: get fleet %s: %v", acct.Sqid, app, fleetName, err)
- jsonError(w, http.StatusInternalServerError, "could not check the fleet")
- return
+ return http.StatusInternalServerError, "could not check the fleet"
}
if f == nil || f.Account != acct.Sqid {
// Unknown and not-yours are the same refusal everywhere else in
// this file (apiFleet); place's target check is no exception —
// this door must not become a way to learn which fleet names
- // exist in someone else's account.
- http.NotFound(w, r)
- return
+ // exist in someone else's account. The empty message is the
+ // signal to the caller: answer http.NotFound, say nothing.
+ return http.StatusNotFound, ""
}
}
@@ -591,20 +589,18 @@ func (s *Server) handleAPIPlace(w http.ResponseWriter, r *http.Request) {
// THIS before it ever walks a route. It has to land before routes/records
// are stamped below, not after — the same "never observe an app in a
// getting-placed state that some OTHER path already trusts" reasoning
- // the ORDER MATTERS paragraph below applies to routes-before-records, one
+ // the ORDER MATTERS paragraph above applies to routes-before-records, one
// step earlier: a concurrent instance-create must never inherit a target
// this request has not actually committed to.
if err := s.store.SetAppPlacement(acct.ID, app, target); err != nil {
log.Printf("console: place %s/%s: set placement: %v", acct.Sqid, app, err)
- jsonError(w, http.StatusInternalServerError, "could not record placement — try again")
- return
+ return http.StatusInternalServerError, "could not record placement — try again"
}
routes, err := reg.List()
if err != nil {
log.Printf("console: place %s/%s: list routes: %v", acct.Sqid, app, err)
- jsonError(w, http.StatusBadGateway, "could not read the app's routes — try again")
- return
+ return http.StatusBadGateway, "could not read the app's routes — try again"
}
for _, rt := range routes {
if rt.Account != acct.Sqid || rt.App != app {
@@ -612,19 +608,17 @@ func (s *Server) handleAPIPlace(w http.ResponseWriter, r *http.Request) {
}
if err := reg.SetPlacement(rt.Host, target); err != nil {
log.Printf("console: place %s/%s: set placement on %s: %v", acct.Sqid, app, rt.Host, err)
- jsonError(w, http.StatusBadGateway, "could not update every route — try again")
- return
+ return http.StatusBadGateway, "could not update every route — try again"
}
}
- ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout)
+ ctx, cancel := context.WithTimeout(ctx, instanceStoreTimeout)
defer cancel()
prefix := provision.RecordsPrefix(acct.Sqid, app)
keys, err := s.cfg.Releases.List(ctx, prefix)
if err != nil {
log.Printf("console: place %s/%s: list instances: %v", acct.Sqid, app, err)
- jsonError(w, http.StatusBadGateway, "could not update this app's instances — try again")
- return
+ return http.StatusBadGateway, "could not update this app's instances — try again"
}
for _, key := range keys {
rest, ok := strings.CutPrefix(key, prefix)
@@ -647,10 +641,45 @@ func (s *Server) handleAPIPlace(w http.ResponseWriter, r *http.Request) {
continue
}
log.Printf("console: place %s/%s: stamp instance %s: %v", acct.Sqid, app, host, err)
- jsonError(w, http.StatusBadGateway, "could not update every instance — try again")
- return
+ return http.StatusBadGateway, "could not update every instance — try again"
}
}
+ return 0, ""
+}
+
+// handleAPIPlace answers `carlos apps place`: the bearer-authenticated door
+// onto placeAppTarget. Everything that makes placement CORRECT lives in that
+// function (see its doc comment); what belongs here is the CLI API's own
+// share — bearer resolution, the write budget, the JSON body, the "(cli)"
+// audit line, and translating the core's (status, message) onto this API's
+// refusal shapes.
+func (s *Server) handleAPIPlace(w http.ResponseWriter, r *http.Request) {
+ addr, acct, app, ok := s.apiAccountApp(w, r)
+ if !ok {
+ return
+ }
+ if !s.cli.write.allow(addr, time.Now()) {
+ jsonError(w, http.StatusTooManyRequests, "too many requests — wait a minute and try again")
+ return
+ }
+ var req struct {
+ Target string `json:"target"`
+ }
+ if !readJSON(w, r, &req) {
+ return
+ }
+ target := strings.TrimSpace(req.Target)
+
+ switch status, msg := s.placeAppTarget(r.Context(), acct, app, target); {
+ case status == http.StatusNotFound:
+ // The cross-account target refusal — byte-identical to any other
+ // not-found, never a JSON message of its own.
+ http.NotFound(w, r)
+ return
+ case status != 0:
+ jsonError(w, status, msg)
+ return
+ }
detail := "cleared placement for " + app + " (cli)"
if target != "" {
diff --git a/internal/console/server.go b/internal/console/server.go
index 9f89f7c6b..320749f43 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -1696,7 +1696,7 @@ func normalizeAppName(name string) string {
//
// placement is an ALREADY-VALIDATED "" or "fleet/<name>" string — the
// caller (handleAPICreateApp) checks grammar and fleet ownership itself,
-// the same inline checks handleAPIPlace's target uses, BEFORE calling this,
+// the same inline checks placeAppTarget's target uses, BEFORE calling this,
// so an invalid or foreign placement never reaches here at all and never
// costs a claim attempt. handleNewApp (the browser door) has no placement
// concept and always passes "".