Tags
PartialA tag is an operator key: value label for dimensions Omniglass does not model natively
(category: audio-dsp, environment: prod, cost-center: 4021): not a signal, no lifecycle, riding the
same cascade as
config, secrets, and variables but with a union-on-key, override-on-value
combinator rather than a single most-specific value.
Two layers: the key vocabulary and the value binding
Section titled “Two layers: the key vocabulary and the value binding”tagis the governed key vocabulary: one row per key (category,environment), shared across the whole tenant (one registry per database, the tenant boundary). It owns no value.tag_bindingis the value cell: a value for a key at one owner on the exclusive arc (platform | location | system | component | node), exactly the arc a variable or a secret is owned at.
Splitting them keeps the vocabulary normalized: environment is minted once and reused, no one
inventing env beside environment beside Environment. What an operator binds by is the tag’s
name, and it is an ordinary entity name: storage.ValidateName reads the shape the tag table
declares and applies the one-segment rule (lowercase letters, digits, and hyphens, 100 characters, a uuid
refused), so cost-center is the canonical spelling and cost_center never enters the vocabulary.
That rule tightening is the reason a key also carries a label, the friendly string an operator
reads (“Cost Center”) beside the one they bind by
(ADR-0119).
It is optional and it is not an identifier: no uniqueness, no pattern, and nothing resolves through
it. A key with none renders cost-center verbatim in every list, never re-cased. The directory
is ordered by the label an operator reads, with the unlabelled keys last.
The governance split is the point
Section titled “The governance split is the point”Minting a key and setting a value are two different permissions, deliberately:
- Minting a key is a tenant-wide governance action: an all-scope
tag:creategrant (an admin or curator, identity and access); governance edits aretag:update, deletestag:delete, both all-scope. - Setting a value is the ordinary entity write: binding
environment: prodonto a component is acomponent:update(system and location likewise), no new permission. A platform binding has no owning entity, so it takestag:updateplus the install-wideplatform:update.
The vocabulary stays curated while tagging stays routine; reading rides the viewer floor (tag:read,
component:read).
A key governs where it applies and whether it cascades
Section titled “A key governs where it applies and whether it cascades”Two fields on the key shape its bindings:
applies_tonarrows a key to a subset of entity kinds (component,system,location,node): empty is universal, a non-empty set rejects a binding on any other kind at write time (arack-positionkey thatapplies_to: [location]cannot be bound onto a component).propagatessays whether a bound value cascades. A propagating key (the default) resolves down the cascade; a non-propagating key binds as a flat per-entity set, resolving only from a binding on the entity itself.
Values resolve union-on-key, override-on-value
Section titled “Values resolve union-on-key, override-on-value”An entity’s effective tags resolve down the same cascade as every other operator-set value; the structural chain, precedence key, and fall-through rule are stated once, there. Tags differ only in the combinator: keys union (an entity surfaces every key bound at or above it) and values override (for a single key set at several scopes, the most-specific binding wins, with the shadowed candidates returned so the surface can teach the override).
A non-propagating key is admitted into the resolve only from the target entity itself.
GET /components/{name}/effective-tags returns the resolved set (winner plus shadowed);
GET /components/{name}:listTags returns only the bindings set directly on the component; the writes are
the :setTag / :removeTag custom methods. The system band resolves through system_member membership as
the cascade defines
(?system= asks as a specific system; otherwise the primary membership seeds the chain), so a shared device
answers differently per system.
The cascade is authorized by the component it resolves for, not band by band
(ADR-0093). A caller
who can read the component sees every value that cascades onto it, including one owned by a system or a
location they could not have listed directly, because a resolved value is part of that component’s own
configuration and reporting a filtered one would describe a configuration the platform does not actually
apply. ?system= narrows that answer to one membership and can never widen it: the named system must already
be one the component belongs to.
Systems and locations resolve too. A location resolves platform plus its own location tree; a
system resolves platform, its own system tree, and the location it is placed at (its location_id
tree), so a system in a PCI building surfaces compliance: pci; a node is fleet-wide, resolving
platform plus its own direct bindings only. This is the read behind the directory Tags column: the list
routes (GET /components, /systems, /locations, /nodes) each carry an effective_tags map (key to
winning value, winners only) per row, resolved for the whole page in one batched query (a
Gateway.EffectiveTags per kind); provenance stays in the per-entity detail view.
A key may constrain its values to an enum. A key carries an allowed_values set: empty (the
default) is free text, a non-empty set is enforced on the binding write (a value outside it is a 422) and
rendered as a strict dropdown. A free key instead autocompletes the distinct values already in use (a
GET /tags/{name}:values read)
(ADR-0024).
Storage
Section titled “Storage”The key vocabulary and the value cell; the physical layout lives on storage. Both
tables are built; the column mechanics render from the generated schema facts (make gen), the notes
hand-written.
The tag row is the tenant-wide governed key vocabulary; minting a key needs tag:create.
| Column | Type | Constraints | Notes |
|---|---|---|---|
id | uuid | PK, default uuidv7() | |
name | text | not null | The governed key operators bind by |
applies_to | ARRAY | not null, default '{}'::text[] | Narrows the key to entity kinds; empty is universal |
propagates | boolean | not null, default true | Toggles cascade versus flat per-entity binding |
created_at | timestamp with time zone | not null, default now() | |
updated_at | timestamp with time zone | not null, default now() | |
allowed_values | ARRAY | not null, default '{}'::text[] | The value enum; empty is free text, enforced on the binding write |
label | text | The friendly string an operator reads; NULL falls back to the name, verbatim |
CHECK constraints and unique indexes on tag
-
tag_name_key:CREATE UNIQUE INDEX tag_name_key ON public.tag USING btree (name)
The tag_binding row is the key: value cell at one owner on the exclusive arc
(platform / location / system / component / node); it resolves union on key, override on value down the
cascade. Setting a value is the owner’s own update write, except at platform
(tag:update plus platform:update).
| Column | Type | Constraints | Notes |
|---|---|---|---|
id | uuid | PK, default uuidv7() | |
tag_id | uuid | FK → tag.id, not null | The key being bound |
owner_kind | text | not null | Which arm of the exclusive owner arc holds the cell |
value | text | not null | The bound value the cascade resolves |
created_at | timestamp with time zone | not null, default now() | |
updated_at | timestamp with time zone | not null, default now() | |
node_id | uuid | FK → node.principal_id | |
component_id | uuid | FK → component.id | |
system_id | uuid | FK → system.id | |
location_id | uuid | FK → location.id |
CHECK constraints and unique indexes on tag_binding
-
tag_binding_owner_arc:CHECK ((((owner_kind = 'platform'::text) AND (component_id IS NULL) AND (system_id IS NULL) AND (location_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'component'::text) AND (component_id IS NOT NULL) AND (system_id IS NULL) AND (location_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'system'::text) AND (system_id IS NOT NULL) AND (component_id IS NULL) AND (location_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'location'::text) AND (location_id IS NOT NULL) AND (component_id IS NULL) AND (system_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'node'::text) AND (node_id IS NOT NULL) AND (component_id IS NULL) AND (system_id IS NULL) AND (location_id IS NULL)))) -
tag_binding_owner_kind_check:CHECK ((owner_kind = ANY (ARRAY['platform'::text, 'component'::text, 'system'::text, 'location'::text, 'node'::text]))) -
tag_binding_component_key_uuid:CREATE UNIQUE INDEX tag_binding_component_key_uuid ON public.tag_binding USING btree (tag_id, component_id) WHERE (owner_kind = 'component'::text) -
tag_binding_location_key_uuid:CREATE UNIQUE INDEX tag_binding_location_key_uuid ON public.tag_binding USING btree (tag_id, location_id) WHERE (owner_kind = 'location'::text) -
tag_binding_node_key:CREATE UNIQUE INDEX tag_binding_node_key ON public.tag_binding USING btree (tag_id, node_id) WHERE (owner_kind = 'node'::text) -
tag_binding_platform_key:CREATE UNIQUE INDEX tag_binding_platform_key ON public.tag_binding USING btree (tag_id) WHERE (owner_kind = 'platform'::text) -
tag_binding_system_key_uuid:CREATE UNIQUE INDEX tag_binding_system_key_uuid ON public.tag_binding USING btree (tag_id, system_id) WHERE (owner_kind = 'system'::text)