Verify the sim/header-side half of the tokenless-sim REJECT root cause against current source (second link of the bf-34lwt split). Three facts re-checked against current source: - Sim supplies its token ONLY as the X-Spaxel-Token HTTP header on the WS dial — cmd/sim/main.go:313, mothership/cmd/sim/main.go:633, mothership/cmd/sim/scenario.go:318 (all three connect paths). - The hello JSON map the sim sends has NO token field — cmd/sim/main.go:348-359, mothership/cmd/sim/main.go:652-665, mothership/cmd/sim/scenario.go:331-342. Only "--token" hits are CLI flag defs, never a JSON body key. - The mothership NEVER reads that header on the WS path — grep 'X-Spaxel-Token' mothership/ excluding _test.go and cmd/sim/ returns zero hits. HandleNodeWS (server.go:455-469) passes r straight to Upgrade; the only Header ref on the path is the outbound w.Header().Set response header. Conclusion: header ignored -> hello.Token (message.go:22) empty -> tokenOK (server.go:513) always false. The "have the sim supply valid tokens" option is currently DEAD. Companion to bf-5ig3e (validator half); both roll up into bf-34lwt. Co-Authored-By: Claude <noreply@anthropic.com> Bead-Id: bf-29wyl
7 KiB
Confirmation: sim supplies its token only as the unread X-Spaxel-Token header → token-supply path is DEAD
Tracking: bead bf-29wyl (second link of the bf-34lwt split).
Date: 2026-07-07.
Scope: isolate and re-verify the sim/header-side half of the
tokenless-sim REJECT root cause against current source. This confirms the
three statements "the sim sets the token only as the X-Spaxel-Token
header", "the hello JSON body has no token field", and "the mothership
never reads that header on the WS path" — i.e. the token-supply option is
currently dead. The validator-side half is covered by the sibling note
token-validator-wiring-confirm.md
(bead bf-5ig3e); both roll up into the parent
token-reject-root-cause.md (bead
bf-34lwt). This note re-checks the three header-side facts that chain into
"dead path", so a drift in any of them is detectable on its own.
Path convention. Citations are full repo-relative paths. The
bf-29wyltask body drops themothership/module prefix, so itscmd/sim/main.goismothership/cmd/sim/main.gohere.Two sim copies — both behave identically for this finding.
cmd/sim/main.go— the modulego.worklists (use ./cmd/sim); the dev/test path. This copy has noscenario.go.mothership/cmd/sim/— the copy the Dockerfile actually builds (it doesCOPY mothership/ ./thengo build ./cmd/sim, so./cmd/simresolves here).
Fact 1 — the sim sets its token ONLY as the X-Spaxel-Token HTTP header
The token is attached to the WebSocket dial request as a request header in all three connect paths, and nowhere else:
cmd/sim/main.go:313:reqHeader.Set("X-Spaxel-Token", token)— fed towebsocket.DefaultDialer.DialContext(ctx, u.String(), reqHeader)(the dial a few lines below).mothership/cmd/sim/main.go:633:headers.Set("X-Spaxel-Token", token).mothership/cmd/sim/scenario.go:318:headers.Set("X-Spaxel-Token", token).
token here is the provisioned node token (auto-generated to a 64-hex dummy
if --token is empty — cmd/sim/main.go:120-125,
mothership/cmd/sim/main.go:596-601). It is supplied to the mothership
exclusively via this HTTP header on the upgrade request. There is no second
delivery channel.
Fact 2 — the hello JSON body has NO token field
The hello map built and sent as the node's first WS frame omits token
entirely:
cmd/sim/main.go:348-359:hello := map[string]interface{}{ "type": "hello", "mac": macToString(n.MAC), "firmware_version": "sim-1.0.0", "capabilities": []string{"csi", "ble", "tx", "rx"}, "chip": "ESP32-S3", "flash_mb": 16, "uptime_ms": 1000, "pos_x": n.Position.X, "pos_y": n.Position.Y, "pos_z": n.Position.Z, }mothership/cmd/sim/main.go:652-665: keys aretype, mac, firmware_version, capabilities, chip, flash_mb, uptime_ms, wifi_rssi, ip, pos_x, pos_y, pos_z.mothership/cmd/sim/scenario.go:331-342: keys aretype, mac, firmware_version, capabilities, chip, flash_mb, uptime_ms, wifi_rssi, ip.
None of the three contains a token key. A targeted grep confirms it — the
only "token" hits in each file are the --token flag definition (the CLI
argument), never a JSON body key:
$ grep -n '"token"' cmd/sim/main.go mothership/cmd/sim/main.go mothership/cmd/sim/scenario.go
cmd/sim/main.go:54: flagToken = flag.String("token", "", "Provisioning token (auto-generated if empty)")
mothership/cmd/sim/main.go:49: flagToken = flag.String("token", "", "Provisioning token (auto-generated if empty)")
mothership/cmd/sim/scenario.go: (no "token" hit at all)
HelloMessage.Token carries the JSON tag json:"token,omitempty"
(mothership/internal/ingestion/message.go:22). Since the body never sets
token, that field deserializes to "" from either sim binary.
Fact 3 — the mothership NEVER reads X-Spaxel-Token on the WS path
A grep for the literal header name across the entire mothership tree, excluding the sim sources that set it and the test files that document the contract, returns zero hits:
$ grep -rn 'X-Spaxel-Token' mothership/ | grep -v '_test.go' | grep -v 'cmd/sim/'
(no output; exit status 1)
The only remaining references in the tree are the sim's own Set(...) calls
and an e2e test comment that asserts this exact fact
(mothership/tests/e2e/e2e_test.go:86-88: "spaxel-sim nodes present no
token in their hello message (only the X-Spaxel-Token header, which the
ingestion server does not read)"). There is no production code that reads
it.
The WS upgrade handler inspects nothing off the incoming request either.
mothership/internal/ingestion/server.go:455-469:
func (s *Server) HandleNodeWS(w http.ResponseWriter, r *http.Request) {
...
w.Header().Set("Content-Type", "application/json") // :463 — OUTBOUND response header
...
conn, err := s.upgrader.Upgrade(w, r, nil) // :469 — r passed through, never read for headers
The sole Header reference on this path is w.Header().Set(...) on the
outbound response (:463); r (the request bearing the X-Spaxel-Token
header) is handed straight to Upgrade and never inspected. (Every other
Header.Get / Header.Set hit under mothership/internal/ is on an
outbound webhook/notify path or in its tests — webhook, notify,
notifications/ntfy.go, etc. — none touches the node WS path.)
Conclusion — the token-supply path is currently DEAD
The validator reads only hello.Token
(mothership/internal/ingestion/server.go:513:
tokenOK := hello.Token != "" && validator(hello.MAC, hello.Token)), and
that field comes only from the JSON body (message.go:22). Combining the
three facts above:
- The sim supplies its token only as the
X-Spaxel-TokenHTTP header (Fact 1); - which the mothership never reads on the WS path (Fact 3);
- and the hello body the mothership does read has no
tokenfield (Fact 2).
Therefore for every sim node the header is silently dropped, hello.Token
is "", and hello.Token != "" short-circuits tokenOK to false at
server.go:513 regardless of what token the sim provisioned. The
parent bead's "have the sim supply valid tokens" option is currently
NON-FUNCTIONAL — the token is supplied to a header nobody reads.
The token-supply path is dead: header ignored →
hello.Tokenempty →tokenOKalwaysfalse.
This isolates the sim/header-side half of bf-34lwt. Combined with the
validator-side facts in the sibling note (bf-5ig3e: the validator is
always wired, and acceptance/rejection hinges entirely on hello.Token),
it follows that a sim node is accepted only while the migration window is
open (the default 24h mask), then sendReject-ed with invalid_token
(server.go:519-528) once it closes — because the supplied token never
reaches the validator at all.