Skip to content

Tags

Partial

A 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”
  • tag is 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_binding is 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.

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:create grant (an admin or curator, identity and access); governance edits are tag:update, deletes tag:delete, both all-scope.
  • Setting a value is the ordinary entity write: binding environment: prod onto a component is a component:update (system and location likewise), no new permission. A platform binding has no owning entity, so it takes tag:update plus the install-wide platform: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_to narrows 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 (a rack-position key that applies_to: [location] cannot be bound onto a component).
  • propagates says 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).

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.

tag (content): generated from the migrated schema by make gen
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).

tag_binding (content): generated from the migrated schema by make gen
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)