Skip to content

Read both node_id formats, and refuse to register what the spec cannot carry - #20

Closed
Purple10101 wants to merge 1 commit into
mainfrom
20260921-node-id-schema-v2
Closed

Purple10101 wants to merge 1 commit into
mainfrom
20260921-node-id-schema-v2

Conversation

@Purple10101

Copy link
Copy Markdown
Collaborator

Part 2 of 5, with owl-os#59, node-infra, retina-gui and retina-node all on 20260921-node-id-schema-v2. This is the one worth real scrutiny — the other four are tolerance widenings that provably change nothing today. This one changes runtime behaviour.

Two changes pulling opposite ways, deliberately

collect/identity.py widens. retg + 15 hex is accepted alongside ret + 8 hex. A node validates only its own id, so either format is judged correctly by the same rule, and a legacy node is unaffected by the new format existing elsewhere. That is what lets the fleet migrate one node at a time rather than at once.

wire/registration.py gates. The ingest spec still pins ^ret[0-9a-f]{8}$, and wire/models.py is generated from it, so a migrated node would otherwise build a payload the generated model rejects with a Pydantic validation error nobody can act on. Instead it is held back on purpose, with a message that names the spec rather than the board.

The gate opens by itself

spec_node_id_pattern() reads the pattern out of the generated model rather than repeating it. wire/models.py is derived from docs/node-ingest-v1.yml, which is the contract, so regenerating it against a spec that carries the current format lifts the gate with no code change here. A second copy of the pattern would be a second place to remember, and therefore the one that gets forgotten.

Only real ids are intercepted

"Unknown", the retina-node config placeholder and anything truncated are not nodes waiting on a spec. Claiming they were would send whoever read it hunting for a migration that never happened. They keep falling through to the generated model, whose validation error is the honest answer. Two existing tests caught this in an earlier draft of the guard and are the reason it is narrowed.

NodeState.NODE_ID_UNSUPPORTED

Sits directly below NO_IDENTITY and above the agreement and wizard gates. Those two are cleared by the owner doing something; this one is not, and telling someone to finish a wizard that cannot unblock them is worse than telling them nothing. It reaches the server in no form, because saying so needs a token.

An operator sees, verbatim:

this node has been migrated to the current node_id format, which the server's ingest spec does not carry yet. Registration is held back on purpose rather than attempted and rejected. The node is otherwise healthy and will register on its own once the spec accepts the format; nothing done here will help.

The consequence to be explicit about

From the moment this ships, any node built from a current image is telemetry-invisible until the spec moves. Not only migrated nodes. A fresh board enrols in Mender, gets a Cloudflare tunnel and reports inventory, and never registers.

That is the intended trade: a deliberate, self-explaining hold instead of a confusing failure. It is worth agreeing to explicitly rather than discovering.

retina-gui needs no change to show it

Checked: telemetry_status.py passes state and detail through verbatim, node_id_unsupported is not in TRANSIENT_STATES, and no template does state-specific rendering.

Verified

Full suite green, ruff check and ruff format --check clean. New tests cover the union pattern, the gate reading the generated model, the refusal naming the spec, the subclass so existing IncompletePayload handlers still catch it, the precedence of the new state, and that garbage still reaches Pydantic.

Vendors tests/node-id-test-vectors.json from owl-os with a test asserting this repo's pattern is the canonical one, so a drifted copy fails rather than looking authoritative.

Not verified

The container has never run on hardware. The gate has not been observed on a real node. Closes naturally in Wave 1 of the rollout, when the image ships via retina-node's TELEMETRY_V pin.

🤖 Generated with Claude Code

… spec cannot carry

Two changes that pull in opposite directions on purpose.

collect/identity.py now accepts retg + 15 hex alongside ret + 8 hex. A node
validates only its own id, so a node on either format is judged correctly by
the same rule and a legacy node is unaffected by the new format existing
elsewhere. That is what lets the fleet migrate one node at a time instead of
at once.

wire/registration.py gates in the other direction. The ingest spec still pins
ret + 8 hex, and wire/models.py is generated from it, so a migrated node would
otherwise build a payload the generated model rejects with a validation error
nobody can act on. Instead it is held back deliberately, with a reason that
names the spec rather than the board.

The gate reads its pattern out of the generated model rather than repeating it.
wire/models.py is derived from the contract, so regenerating it against a spec
that carries the current format lifts the gate by itself, with no code change
here. A second copy of the pattern would be a second place to remember, and
therefore the one that gets forgotten.

Only ids that are genuinely ours are intercepted. "Unknown", the retina-node
config placeholder and anything truncated are not nodes waiting on a spec, and
saying so would send whoever read it hunting for a migration that never
happened. Those keep falling through to the generated model, whose validation
error is the honest answer.

NodeState.NODE_ID_UNSUPPORTED sits directly below NO_IDENTITY and above the
agreement and wizard gates. Those two are cleared by the owner doing something;
this one is not, and telling someone to finish a wizard that cannot unblock
them is worse than telling them nothing. It reaches the server in no form,
because saying so needs a token.

Consequence worth stating plainly: from the moment this ships, any node built
from a current image is telemetry-invisible until the spec moves. Not only
migrated nodes. The node keeps running, keeps its identity, and says why.

retina-gui needs no change to display it: telemetry_status.py passes state and
detail through verbatim and node_id_unsupported is not in TRANSIENT_STATES.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Purple10101

Copy link
Copy Markdown
Collaborator Author

Closing: this work is not being taken forward. The branch is deleted, but the commits stay reachable from this closed PR if it is ever revived.

@Purple10101
Purple10101 deleted the 20260921-node-id-schema-v2 branch September 23, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant