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}/tags returns only the bindings set directly on the component.

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.

TableKey columnsNotes
tagname, applies_to, propagates, allowed_valuesBuilt. The tenant-wide governed key vocabulary; minting a key needs tag:create. applies_to narrows a key to entity kinds (empty = universal); propagates toggles cascade versus flat per-entity binding; allowed_values is the value enum (empty = free text), enforced on the binding write
tag_binding(tag_id, owner arc), valueBuilt. The key: value binding at one owner on the exclusive arc (platform / location / system / component / node); 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