One label grammar, admitted wherever a label enters
- Status
- Accepted
Context
Labels are how a workload is selected: a ruleset's scope, a rule's workload peers, a mode change, every scoped read, and the flow map all resolve a selector against workloads' current labels (ADR-0019). Every surface writes a label in one textual form, key=value, with requirements separated by whitespace and a key's alternative values by |: the command line's flags, the read endpoints' repeated label parameter, the console's inputs and chips.
Until now nothing defined what a key or a value may contain. Admission checked only that a key was present and not repeated. A provisioning token was minted from the console with the text app=web env=lab pasted into its label field; the field split on the first = and the control plane stored one label, key app, value web env=lab. Every workload the token enrolled carried that label. No selector could match it (app=web and env=lab are both different labels), so every label filter and selector preview over those workloads was empty, which was correct behavior against data that could not be written back in the form it was read in. The console drew the stored label as app=web env=lab, pixel for pixel what two healthy labels look like, so the fault survived hours of use.
A system that has one textual form for a thing must refuse every value that form cannot round-trip. Otherwise one surface's parsing slip becomes durable state that every other surface misreads in the same plausible way.
Decision
-
The grammar. A label key is 1 to 63 bytes of ASCII letters, digits,
.,_,-, and/, beginning and ending with a letter or digit. A label value is 1 to 63 bytes of ASCII letters, digits,.,_, and-, beginning and ending with a letter or digit. Every other character is outside it. Each bound follows from what a label has to survive:- Parse safety: no separator inside a label. The textual form has separators:
=between key and value,|between a requirement's alternatives, whitespace between requirements, and the comma a pasted list carries. A key or value that could contain any of them could not be told from two labels, and the defect this record answers was exactly that. Quotes and backslashes are excluded with them, so no surface needs a quoting or escaping layer, and a label is read the same by a shell, a form, and a query string with no rule beyond splitting on separators. - Filter encoding: nothing that changes meaning in transit. A label travels as a query parameter (
label=key=value), on the command line as a flag argument, and in log lines. The admitted characters other than/are ones a URL never needs to encode, so a filter typed into an address bar, copied from a log, or built by a script reads identically on arrival./is admitted in keys alone: it is the one separator a key needs, to carry a namespace (team/owner) without a second delimiter, and keys never appear in a path segment, while values need no namespace and stay free of the one admitted character with a meaning in paths. - Display safety: what is drawn is what matches. ASCII alone excludes look-alike characters, combining marks, zero-width characters, and direction overrides, so two labels that look the same on screen are the same bytes and match the same selectors. A leading or trailing
.,_, or-is the punctuation most easily lost on screen or in prose (app=-webbesideapp=web, a trailing.dropped by a sentence), and a leading-reads as a flag on a command line; requiring a letter or digit at both ends removes the ambiguity. A value is never empty:key=draws as a typing slip, indistinguishable from an absent value, and a selector requirement with no value matches nothing. - Size: a selector token, not prose. 63 bytes keeps a label whole in a chip and a table cell, which the console never truncates mid-label, and keeps a pair (
key=value, at most 127 bytes) small enough that a filter of many requirements stays a short URL. It also bounds what each flow record costs: a workload peer's labels are copied into every stored record as its snapshot (ADR-0019), so label size multiplies by flow volume. Text that needs more room is a description and belongs in a field of its own. Because the set is ASCII, bytes and characters are the same count, so the bound reads the same to every surface.
- Parse safety: no separator inside a label. The textual form has separators:
-
One definition, in the domain. The grammar is defined once, in
internal/policy, beside the selector it serves. Every path by which a label enters the system is admitted against it there: a provisioning token's labels at mint, a workload's label set at a label edit, a ruleset's scope and a rule's workload peers at authoring, a selector at preview and at a mode change, and a read's label scope. Handlers and the command line call the same domain functions and parse nothing finer thankey=valuethemselves (ADR-0007 as amended, ADR-0021). A refusal is the same structured findings the rest of admission reports, with stable rule names (label-key,label-value,label-key-duplicate, beside the existinglabel-key-required), and quotes the offending text so whitespace in it shows. A read whose label scope is outside the grammar is refused as an invalid parameter, never answered with an empty result. -
What is already stored is not rewritten, and is not propagated. Labels stored before this decision are left as they are; the operator repairs them with a label edit, which admits only a set wholly in the grammar. Enrollment refuses a token whose stored labels are outside the grammar, after the token authenticates and before any credential is issued, with a reason naming the grammar and asking for a new token, so a token minted before this decision cannot keep producing workloads no selector can match. Stored rulesets are not re-admitted when they are rendered; a ruleset whose scope is outside the grammar renders as before and is refused only when next written.
-
Every surface parses and shows labels the same way. The console's label inputs share one parser that mirrors the grammar: it reads several requirements from one input, separated by whitespace or commas, tolerates spaces around
=and|, and refuses, with the reason, any text it cannot read as requirements in the grammar, so a paste of several labels becomes several labels or nothing. A label whose key or value is outside the grammar, which can only be one stored before this decision, is drawn visibly distinct from a healthy one: its text quoted and marked, so a stored label can never be mistaken for two.
Consequences
- A label is the same object in every surface: what the console shows can be typed back into the command line, a URL, or a selector and mean the same thing.
- Labels stay small and printable. Anything richer (free text, descriptions) belongs in a field of its own, not in a label value.
- The console checks the grammar for the operator's sake, so a mistake is caught before a request is sent; the control plane remains the authority and refuses the same things.
- An estate with labels stored before this decision sees them marked in the console, has their tokens refused at enrollment, and repairs them with label edits; nothing breaks silently, and no rendered policy changes until an operator writes.
- The grammar is a contract. Widening it later (more characters, a longer bound) is additive; narrowing it would strand stored labels and takes a superseding record.