Skip to main content
Version: v5.6.1

Node metadata — classification and the register

Included from Basic tier

The classification framework: adoptable, schema'd metadata profiles whose typed values annotate estate nodes. Metadata is what governance selects on — approval-policy and notification predicates, ${{ metadata.* }} conditions, the register — and is structurally absent from the execution plane (invariant I1): a metadata value never reaches action inputs, rendered templates or the runner's step context, so classifying a node cannot change what a step receives. The API surface is MetadataService (see opschain docs api-v2); the CLI noun is opschain metadata (opschain docs cli/metadata); the hands-on walkthrough is opschain learn governance-classification. Base platform — no pack entitlement; PostgreSQL-backed (compositions without the metadata plane answer not_found).

Profiles and the core vocabulary​

A profile is a versioned field schema: each field has a key, a type (enum | bool | date | string forms), optional enum values, applies_to (node kinds), inherit (none | subtree) and an optional protective flag. The canonical core profile ships base-owned, seeded at boot — the generic all-enterprise base vocabulary: core.criticality (how important the node is to the business), core.data_sensitivity (data classification), core.rto_class / core.rpo_class (recovery time/point objective classes), core.cost_centre (string — the FinOps anchor), core.lifecycle (pilot|active|deprecated|retired), core.external_exposure (internal|partner|public_internet) and core.managed_by (internal|vendor|managed_service) paired with core.managed_by_name (string — the named team, vendor or MSP), and core.end_of_support (date, YYYY-MM-DD — when the node's platform or software leaves vendor or organisational support, the end-of-life anchor an unsupported-platform posture selects on) — all subtree-inherited; its schema refuses edits. OpsChain holds no regulatory opinion in core: regulatory bindings (CPS 230/234, SOCI, DORA) ship their own profiles and map core fields into their determinations.

Customer profiles live under the customer-owned x-<org>.* namespace: the profile key and every field family must sit under it — non-x- families are reserved for the base core.* themes and pack-shipped bindings (the packs:bindings-<framework> family).

Revisions: any field-set change bumps the profile version. Additive revisions are free; narrowing ones (a removed field, a shrunk enum) stale-flag existing values — never delete, never auto-migrate. A revision may declare per-field old→new value_mappings; ApplyMetadataProfileMigration applies them to stale values as per-node audited writes — migration is an act, not a side effect — and unmapped stale values stay counted and reportable. Field types never change in place.

Values, inheritance and provenance​

SetNodeMetadata writes and clears field values per node. GetNodeMetadata answers the node's own values with staleness plus the effective view: per-field inherit: none|subtree resolved up the ancestor walk with provenance naming the governing ancestor — operators classify at the environment and the assets follow as resolved values, not copies.

BulkAnnotate is the adoption path: a path-glob scope AND-composed with node kinds and metadata predicates — eq/in/bool, the explicit unclassified selector, and the date comparisons before/after (one YYYY-MM-DD value) and elapsed (the date is in the past at evaluation time; a non-date value matches none of the three) — applied as per-node audited writes. Covered nodes outside the caller's read authz are not covered; nodes no written field applies to are skipped and counted.

The register​

ListMetadataRegister is the estate-wide node × field view: rows with staleness, filterable by profile/field/value/stale, read-authz filtered per row, paged, plus the unclassified-coverage counts. ExportMetadataRegister renders the same rows as CSV — the asset-register evidence lane.

Each row also carries owner (the OWNER column): the node's effective owner resolved from the /governance/owners fabric by the same nearest-declaration walk the impacted-asset-owner approval uses, empty when no declaration covers the node. Ownership lives on the governance fabric; the register joins it, never stores it — it is not a metadata field.

Authorization and the step-up floor​

Writes require the delegable metadata-writer determination — updatable on the reserved /metadata<node path> rule-path family (profile writes and migrations decide at /metadata), grantable per subtree through ordinary path-RBAC rules and never implied by node write authz; denial is permission_denied. Reads ride node read authz, 404-on-deny; the profile catalogue is vocabulary and reads authenticated-only.

Writes touching a protective field (core.criticality, core.data_sensitivity, core.external_exposure) additionally sit on the step-up floor (metadata.protective_write) — lowering a classification is the governance-evasion shape, so it demands an elevated session (see opschain docs identity).

The full per-RPC determination × pack × step-up table is on opschain docs governance-matrix.

Audit and the match snapshot​

Every write is audited with before/after per field (43): api:metadata_profiles:create|update, api:node_metadata:set, api:node_metadata:bulk_annotate (per node, with the selector scope), api:node_metadata:migrate — all riding the node's path, so estate-scoped subscriptions match them ("notify on any reclassification" is one subscription; see opschain docs events).

Writes invalidate the compiled effective-metadata snapshot the match paths read (62 G8): approval-policy and notification metadata predicates and ${{ metadata.* }} conditions see a write at the next sweep/entry, never mid-walk. Predicate-carrying selectors are fail-closed: a policy with metadata predicates matches nothing on an unclassified estate.

What changes classification behaviour​

  • MetadataService writes (metadata-writer determination + conditional step-up) — see opschain docs api-v2.
  • Profile revisions and migrations change staleness, never values, until a migration is explicitly applied.
  • The write-invalidated snapshot bounds when a reclassification reaches the match paths: next sweep/entry, never mid-walk.