carlos / platform Public

console: the gate copy tables are functions, not package variables (Task 4 fix round 3)

mainGateCopy, mainMoveCopy and phase1MoveCopy each return a fresh struct
literal, so no caller can mutate a shared table. The sentences are
unchanged; TestMoveDoorGateSentences pins them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ce959e105b1c6a20349e15b80ff7a4c7c108b1d9 parent 91d0326
3 files changed, +41 −35
  • internal/console/apicli.go +13 −11
  • internal/console/apiinstancemove.go +27 −23
  • internal/console/apiinstances.go +1 −1
diff --git a/internal/console/apicli.go b/internal/console/apicli.go
index dfc352eb8..863d199e8 100644
--- a/internal/console/apicli.go
+++ b/internal/console/apicli.go
@@ -57,7 +57,7 @@ func (s *Server) apiTrashedApp(w http.ResponseWriter, r *http.Request) (string,
// half resolves first, then `has` — the last gate and the only variable —
// says whether THIS door's subject exists.
func (s *Server) apiAccountAppBy(w http.ResponseWriter, r *http.Request, has func(int64, string) (bool, error)) (string, *Account, string, bool) {
- return s.apiAccountAppSaying(w, r, has, mainGateCopy)
+ return s.apiAccountAppSaying(w, r, has, mainGateCopy())
}
// apiAccountAppSaying is apiAccountAppBy with the sentences of c.
@@ -180,27 +180,29 @@ func (s *Server) serviceBinding(p Principal) (*Account, bool) {
// settle an ambiguity can never itself be ambiguous. It never widens access
// — a non-member's probe meets the same 404 either way.
func (s *Server) apiPrincipalAccount(w http.ResponseWriter, r *http.Request) (Principal, *Account, bool) {
- return s.apiPrincipalAccountSaying(w, r, mainGateCopy)
+ return s.apiPrincipalAccountSaying(w, r, mainGateCopy())
}
// gateCopy is what the shared CLI gates say when they refuse in a sentence
-// of their own. Every door speaks mainGateCopy, main's bytes; a door with
+// of their own. Every door speaks mainGateCopy(), main's bytes; a door with
// its own reviewed copy passes its own table (the Phase 1 move door,
-// phase1MoveCopy) and the gates' logic is unchanged.
+// phase1MoveCopy()) and the gates' logic is unchanged.
type gateCopy struct {
ambiguous func(seg, alts string) string // the account segment matches several of the caller's accounts
tooMany func(noun string) string // the instance verbs' rate limit
noBucket string // the console runs without a deployment bucket
}
-// mainGateCopy is main's sentences, byte for byte (the dashes are escaped
+// mainGateCopy returns main's sentences, byte for byte (the dashes are escaped
// only in the source).
-var mainGateCopy = gateCopy{
- ambiguous: func(seg, alts string) string {
- return fmt.Sprintf("account %q is ambiguous \u2014 pass --account-id with one of: %s", seg, alts)
- },
- tooMany: func(noun string) string { return "too many " + noun + " requests \u2014 wait a minute and try again" },
- noBucket: "no deployment bucket on this host \u2014 the console is running without one",
+func mainGateCopy() gateCopy {
+ return gateCopy{
+ ambiguous: func(seg, alts string) string {
+ return fmt.Sprintf("account %q is ambiguous \u2014 pass --account-id with one of: %s", seg, alts)
+ },
+ tooMany: func(noun string) string { return "too many " + noun + " requests \u2014 wait a minute and try again" },
+ noBucket: "no deployment bucket on this host \u2014 the console is running without one",
+ }
}
// apiPrincipalAccountSaying is apiPrincipalAccount with the sentences of c.
diff --git a/internal/console/apiinstancemove.go b/internal/console/apiinstancemove.go
index 80a2fe56c..cd52673fe 100644
--- a/internal/console/apiinstancemove.go
+++ b/internal/console/apiinstancemove.go
@@ -37,9 +37,9 @@ import (
// (spec §Out of scope), and fleet placements keep their deliberate
// state-does-not-follow semantics.
func (s *Server) moveGate(w http.ResponseWriter, r *http.Request) (acct *Account, app, host string, rec *provision.Record, ok bool) {
- c := mainMoveCopy
+ c := mainMoveCopy()
if s.cfg.MovesEnabled {
- c = phase1MoveCopy
+ c = phase1MoveCopy()
}
_, acct, app, gok := s.instanceGateSaying(w, r, "instance", c.gate)
if !gok {
@@ -86,7 +86,7 @@ func (s *Server) moveGate(w http.ResponseWriter, r *http.Request) (acct *Account
// moveCopy is what moveGate says: its class rails and the shared gates'
// sentences. With the switch off the move doors speak main's bytes
-// (mainMoveCopy); with it on, the Phase 1 sentences (phase1MoveCopy, copy
+// (mainMoveCopy()); with it on, the Phase 1 sentences (phase1MoveCopy(), copy
// record MV-N47 to MV-N54), which no other door ever speaks.
type moveCopy struct {
gate gateCopy
@@ -94,30 +94,34 @@ type moveCopy struct {
addr, fleet, pool, hibernate string
}
-// mainMoveCopy is main's sentences, byte for byte (the dashes are escaped
+// mainMoveCopy returns main's sentences, byte for byte (the dashes are escaped
// only in the source).
-var mainMoveCopy = moveCopy{
- gate: mainGateCopy,
- kind: func(k string) string { return "only exec-backed instances move \u2014 this route is kind " + k },
- addr: "an addr-backed route's process is not platform-managed \u2014 nothing to move",
- fleet: "fleet-placed instances keep state-does-not-follow semantics \u2014 move is for platform regions",
- pool: "pooled instances do not move in v1 \u2014 their data lives on the pool's own box",
- hibernate: "a non-hibernating instance runs outside the activator \u2014 nothing manages its data, nothing can move it",
+func mainMoveCopy() moveCopy {
+ return moveCopy{
+ gate: mainGateCopy(),
+ kind: func(k string) string { return "only exec-backed instances move \u2014 this route is kind " + k },
+ addr: "an addr-backed route's process is not platform-managed \u2014 nothing to move",
+ fleet: "fleet-placed instances keep state-does-not-follow semantics \u2014 move is for platform regions",
+ pool: "pooled instances do not move in v1 \u2014 their data lives on the pool's own box",
+ hibernate: "a non-hibernating instance runs outside the activator \u2014 nothing manages its data, nothing can move it",
+ }
}
-var phase1MoveCopy = moveCopy{
- gate: gateCopy{
- ambiguous: func(seg, alts string) string {
- return fmt.Sprintf("account %q is ambiguous. pass --account-id with one of: %s", seg, alts)
+func phase1MoveCopy() moveCopy {
+ return moveCopy{
+ gate: gateCopy{
+ ambiguous: func(seg, alts string) string {
+ return fmt.Sprintf("account %q is ambiguous. pass --account-id with one of: %s", seg, alts)
+ },
+ tooMany: func(noun string) string { return "too many " + noun + " requests. wait a minute and try again" },
+ noBucket: "this console has no deployment bucket, so it cannot answer this",
},
- tooMany: func(noun string) string { return "too many " + noun + " requests. wait a minute and try again" },
- noBucket: "this console has no deployment bucket, so it cannot answer this",
- },
- kind: func(k string) string { return "only exec-backed instances move. this route is kind " + k },
- addr: "an addr-backed route's process is not platform-managed. there is nothing to move",
- fleet: "fleet-placed instances keep state-does-not-follow semantics. moves are for platform regions",
- pool: "pooled instances do not move yet. their data lives on the pool's own box",
- hibernate: "a non-hibernating instance runs outside the activator. nothing manages its data, so nothing can move it",
+ kind: func(k string) string { return "only exec-backed instances move. this route is kind " + k },
+ addr: "an addr-backed route's process is not platform-managed. there is nothing to move",
+ fleet: "fleet-placed instances keep state-does-not-follow semantics. moves are for platform regions",
+ pool: "pooled instances do not move yet. their data lives on the pool's own box",
+ hibernate: "a non-hibernating instance runs outside the activator. nothing manages its data, so nothing can move it",
+ }
}
// moveView is the flat answer every move door speaks — titogo's consumer
diff --git a/internal/console/apiinstances.go b/internal/console/apiinstances.go
index 3476b1efb..991423de3 100644
--- a/internal/console/apiinstances.go
+++ b/internal/console/apiinstances.go
@@ -79,7 +79,7 @@ func (s *Server) instanceGate(w http.ResponseWriter, r *http.Request) (string, *
// requests" when they hit it. Only the sentence varies; nothing about the
// order, the key or the limiter does.
func (s *Server) instanceGateFor(w http.ResponseWriter, r *http.Request, noun string) (string, *Account, string, bool) {
- return s.instanceGateSaying(w, r, noun, mainGateCopy)
+ return s.instanceGateSaying(w, r, noun, mainGateCopy())
}
// instanceGateSaying is instanceGateFor with the sentences of c.