Asset Inventory Sync v1 Contract

Contract ID: CONTRACT-ASSET-INVENTORY-SYNC-V1-071  ·  Schema: asset-inventory-sync.v1  ·  Status: Published  ·  Owner: Graph Indexer Guild  ·  Last updated: 2026-04-30

Purpose

asset-inventory-sync.v1 is the local-only delta bundle used to move Graph Asset Registry events between offline or federated StellaOps tenants. The bundle does not define new asset semantics: it carries existing asset-registry.v1append-only events, preserving their payload hashes, Merkle leaf hashes, event hashes, tombstones, and per-asset previous_event_hash chains.

The contract exists so an offline tenant can receive a deterministic bundle, verify it without external services, and append the events to its local Graph Indexer asset event store.

Audience: Graph Indexer implementers and operators moving asset inventory between air-gapped or federated tenants.

Schema Pinning

Envelope Shape

Top-level fields:

FieldRequiredDescription
schema_versionyesAlways asset-inventory-sync.v1.
tenant_idyesSingle tenant carried by every event.
producer_idyesLocal producer identifier, for example graph-indexer.
generated_atyesUTC timestamp supplied by the caller. The codec does not read wall clock time.
since_event_hashnoSender cursor for the delta window, when the bundle starts after a known event. Trimmed and lowercased on parse/create (NormalizeOptionalHash); blank values become null.
previous_bundle_hashnoPrevious sync bundle hash for bundle-chain evidence, when available. Trimmed and lowercased on parse/create (NormalizeOptionalHash); blank values become null.
event_countyesCount of entries in events. Parse fails if it does not equal events.length.
eventsyesAsset registry event entries ordered by occurred_at ASC, event_hash ASC.
bundle_idyesasset-inventory-sync:<bundle_hash_hex>.
bundle_hashyessha256:<64 lowercase hex> over the canonical envelope excluding bundle_id and bundle_hash.

Event entry fields:

FieldRequiredDescription
event_idyesExisting asset-event:<event_hash> id.
tenant_idyesMust match the envelope tenant.
asset_idyesDeterministic Graph asset id (derived from tenant + kind asset + canonical key, see AssetGraphDocumentFactory.ComputeAssetId).
asset_typeyesOne of the closed asset-registry.v1 asset types: host, container, image, service, integration, plugin (AssetRegistryV1.AssetTypes).
event_typeyesasset.created, asset.updated, or asset.removed (AssetRegistryV1.EventTypes).
payloadyesCanonical asset-registry.v1 asset node JSON object. On parse the codec re-canonicalizes this object before hashing.
payload_hashyesExisting 64-character lowercase hex asset payload hash (SHA-256 of the canonical payload JSON).
previous_event_hashnoPrevious event hash for this asset chain, or null for roots. Trimmed and lowercased; blank becomes null.
merkle_leaf_hashyesExisting asset event Merkle leaf hash (64 lowercase hex).
event_hashyesExisting deterministic event hash (64 lowercase hex).
occurred_atyesEvent UTC timestamp. Formatted by the Graph timestamp writer: yyyy-MM-ddTHH:mm:ssZ (or …ss.fffffffZ when sub-second).

Example:

{
  "schema_version": "asset-inventory-sync.v1",
  "tenant_id": "tenant-a",
  "producer_id": "graph-indexer",
  "generated_at": "2026-04-30T11:00:00Z",
  "since_event_hash": null,
  "previous_bundle_hash": null,
  "event_count": 1,
  "events": [
    {
      "event_id": "asset-event:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "tenant_id": "tenant-a",
      "asset_id": "gn:tenant-a:asset:EXAMPLE",
      "asset_type": "service",
      "event_type": "asset.created",
      "payload": {
        "attributes": {
          "asset_id": "gn:tenant-a:asset:EXAMPLE",
          "schema_version": "asset-registry.v1",
          "tenant_id": "tenant-a",
          "type": "service",
          "tombstone": false
        },
        "id": "gn:tenant-a:asset:EXAMPLE",
        "kind": "asset",
        "tenant": "tenant-a"
      },
      "payload_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      "previous_event_hash": null,
      "merkle_leaf_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
      "event_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "occurred_at": "2026-04-30T09:00:00Z"
    }
  ],
  "bundle_id": "asset-inventory-sync:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
  "bundle_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
}

The example illustrates field names only; its hash values are placeholders and the payload is abbreviated. A real asset-registry.v1 asset node carries the full Graph document shape produced by AssetGraphDocumentFactory.CreateAssetNode — top-level tenant, kind, canonical_key, attributes, provenance, valid_from, valid_to, id, and hash, where attributes includes schema_version, asset_id, tenant_id, type, source, external_id, criticality, ownership, depends_on, asset_attributes, first_seen, last_seen, and tombstone. Real event hashes must be recomputed from the canonical payload and metadata.

Canonicalization And Hashing

Apply Semantics

The applier (AssetInventorySyncApplier.ApplyAsync) takes a parsed bundle (or raw JSON) plus an IAssetRegistryEventStore and returns an AssetInventorySyncApplyResult (bundle_id, bundle_hash, appended-event count, skipped-event count, the applied events, and the resulting current asset states, each carrying its latest event_hash, payload_hash, and tombstone flag). The store is the local append-only asset event log; the production implementation is PostgresAssetRegistryEventStore, backed by the asset_registry_events table (Graph Indexer persistence migration 003_asset_registry_events.sql).

Consumers apply a bundle as follows:

  1. Parse the envelope and reject unknown top-level or event fields.
  2. Verify schema_version, event_count, bundle_hash, and bundle_id.
  3. Verify every event by reconstructing the existing Asset Registry event hash material from the included payload.
  4. Verify all events match the envelope tenant_id.
  5. Verify there are no duplicate event_id values.
  6. Verify in-bundle chains for repeated assets: each later event for the same asset_id must point to the prior in-bundle event_hash.
  7. For each new event, read the local chain for that asset_id. Append only when previous_event_hash equals the local latest event hash. If the event hash already exists locally, skip it as an idempotent replay.
  8. Reject the bundle if a new event references a predecessor that is neither local nor earlier in the bundle.

The result is deterministic because the same local chain plus the same bundle produces the same appended event ids and current asset states.

Privacy And Security

Out Of Scope

Those integrations can consume this bundle format later without changing the v1 canonicalization or event-chain rules.

Offline Replay Expectations