Skip to content

Tags

Partial

A tag is an operator key: value label attached to an entity to organize, filter, and scope by dimensions Omniglass does not model natively (category: audio-dsp, environment: prod, cost_center: 4021). A tag is not a signal and carries no lifecycle; it rides 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, which is the tenant boundary). It owns no value.
  • tag_binding is the value cell: it sets 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 is what keeps the vocabulary normalized: the key environment is minted once and reused, so no one invents env beside environment beside Environment. The key name is validated as a lowercase identifier in a pure internal/tag package, so the canonical spelling is enforced on the way in, not by convention.

Minting a key and setting a value are two different permissions, and that split is deliberate:

  • Minting a key is a tenant-wide governance action, gated by an all-scope tag:create grant (an admin or curator, identity and access). Editing a key’s governance fields is tag:update and deleting it is tag:delete, both all-scope.
  • Setting a value is the ordinary entity write. Binding environment: prod onto a component is a component:update, onto a system a system:update, onto a location a location:update, the same write an operator already holds on the entity. Binding needs no new permission: an operator who may edit a component may tag it, using the keys the vocabulary already governs. A platform binding (the install-wide value for a key) has no owning entity to defer to, so it is gated by tag:update plus the install-wide platform:update.

So the vocabulary stays curated while tagging stays routine. Reading the vocabulary and an entity’s tags 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 how its bindings behave:

  • applies_to narrows a key to a subset of entity kinds (component, system, location, node). An empty set is universal (the key applies everywhere); 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). The vocabulary carries its own scoping rules, so a key means the same thing wherever it is legal.
  • propagates says whether a bound value cascades. A propagating key (the default) resolves down the tree: tag a location once and every system and component beneath it inherits the value. A non-propagating key binds as a flat per-entity set: it resolves only from a binding on the entity itself, never from an ancestor. That flat case is the shape a file needs (a file is not on the structural arc, so it has no parent to cascade from); the same key model serves both, toggled by this one field.

Values resolve union-on-key, override-on-value

Section titled “Values resolve union-on-key, override-on-value”

The effective tags for a component resolve down the structural cascade (platform -> location tree -> system tree -> component tree), the same walk the variable and secret resolvers use, but with a different combinator:

  • Keys union. An entity surfaces every key bound at or above it. Two different keys set at two different scopes both appear; they do not compete.
  • Values override. For a single key set at several scopes, the most-specific binding wins (highest band, then nearest depth), exactly like a variable. The shadowed candidates come back too, so the surface can teach the override.

A non-propagating key is admitted into the resolve only from the target entity itself, so its ancestor bindings never leak downward. The GET /components/{name}/effective-tags route returns the resolved set (winner plus shadowed candidates); GET /components/{name}:listTags (a custom method) returns only the bindings set directly on the component, and the writes are the :setTag / :removeTag custom methods. The system band resolves through system_member membership (the primary membership by default, ?system= to ask as a specific system the component belongs to), so a shared device answers differently per system.

Systems and locations resolve too. A component walks the full arc, but every entity has an effective set. 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): a system in a PCI building surfaces compliance: pci, the same way a component picks up its own location’s tags. A node is estate-wide, not a scope tree, so it resolves platform plus its own direct bindings only (no inheritance). 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, no per-row fetch). Provenance (which scope a value came from) stays in the per-entity effective-tags detail view, not the column.

A key may constrain its values to an enum. A key carries an allowed_values set: empty (the default) leaves it free text, and a non-empty set is the enum a bound value must belong to, so environment can be declared as one of prod, staging, dev. The binding write enforces membership (a value outside a key’s allowed set is a 422), and the add control renders a strict dropdown for an enum key. A free key instead autocompletes the distinct values already in use for it (a GET /tags/{name}:values read), so an operator reaches for prod instead of retyping it, without the key having to declare the set up front (ADR-0024).

The key vocabulary and the value cell; the physical layout (the owner arc, the cascade key) lives on storage. Both tables are built. The column mechanics below render from the generated schema facts (make gen), so they cannot drift; the notes stay 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
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, which takes 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)