EU Runtime API Contracts v1

Contract ID: CONTRACT-EU-RUNTIME-API-V1-034 Version: eu-runtime-api.v1 Status: Draft Owner: Graph for asset registry API; source services own their records Last updated: 2026-05-30 (reconciled against implementation)

Purpose

This contract is for engineers building EU compliance consumers on top of the Stella Ops release control plane — Graph, Web, and any service or client that needs asset-registry data or the EU regulatory read APIs. It records the live runtime API boundaries those consumers may depend on, and the ownership rules that keep each record with its source of truth.

Graph owns asset-registry list/query/show/export. Authority, Notify, Policy, and EvidenceLocker remain the source of truth for tenant compliance profiles, reporting timelines, NIS2 control registers, and regulatory artifact ledger records, respectively.

Graph must not copy those upstream records into its own persistence. It may depend on typed clients that fail closed while the source-service read API is not available.

Per-record schema contracts referenced below are documented separately:

Graph Asset Registry Runtime API

All routes require a resolved tenant and the normal Graph header authentication path. Read routes accept graph:read or graph:query; export requires graph:export. (Scopes are enforced by GraphPoliciesGraphPolicies.ReadOrQuery accepts graph:read or graph:query; GraphPolicies.Export requires graph:export. See src/Graph/StellaOps.Graph.Api/Security/GraphPolicies.cs.)

The route group is mounted at /graph/assets (AssetRegistryEndpoints.MapAssetRegistryEndpoints). Both GET /graph/assets and GET /graph/assets/ are registered and behave identically.

The v1 asset type values are exactly: host, container, image, service, integration, and plugin (GraphAssetRegistryService.AssetTypes). Supplying any other type to GET or POST .../query raises an ASSET_REGISTRY_VALIDATION_FAILED 400.

GET /graph/assets

Query parameters:

POST /graph/assets/query

Request:

{
  "types": ["service", "integration"],
  "query": "payments",
  "criticality": "high",
  "filters": { "provider": "github" },
  "includeTombstones": false,
  "includeLineage": true,
  "limit": 100,
  "cursor": null,
  "ordering": "assetId"
}

Response:

{
  "schemaVersion": "asset-registry.v1",
  "items": [
    {
      "schemaVersion": "asset-registry.v1",
      "assetId": "gn:tenant-a:asset:...",
      "type": "integration",
      "tenantId": "tenant-a",
      "attributes": {
        "credential_ref": "cred:github-prod",
        "provider": "github"
      },
      "ownership": {
        "ownerActorRef": "actor:release-owner",
        "ownerTeamRef": "team:platform",
        "credentialRefs": ["cred:github-prod"]
      },
      "criticality": "high",
      "dependsOn": [],
      "firstSeen": "2026-04-30T08:00:00Z",
      "lastSeen": "2026-04-30T08:05:00Z",
      "tombstone": false,
      "lineage": [
        {
          "edgeId": "ge:tenant-a:asset-version:...",
          "eventType": "asset.created",
          "previousAssetHash": null,
          "currentAssetHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
          "effectiveAt": "2026-04-30T08:00:00Z",
          "tombstone": false
        }
      ]
    }
  ],
  "cursor": null,
  "total": 1
}

cursor is null on the final page. When more rows remain it is an object, not a string (AssetRegistryCursor):

{
  "token": "<opaque-offset-token>",
  "resumeUrl": "/graph/assets?cursor=<url-encoded-token>"
}

The opaque token is an offset cursor (CursorCodec); supply it back as the cursor query/body parameter to fetch the next page.

GET /graph/assets/{assetId}

Returns one asset-registry.v1 asset record for the resolved tenant. Query parameter includeLineage=true includes lineage. Lookup includes tombstoned assets. When no asset matches, the endpoint returns 404 with body { "error": "ASSET_NOT_FOUND", "message": "..." }.

POST /graph/assets/export

Request is the query request plus format. Supported formats are ndjson (default) and json; any other value yields an ASSET_REGISTRY_VALIDATION_FAILED 400. Export ignores limit and cursor (all matching assets are emitted). For json the body is an AssetRegistryListResponse (with items and total, no cursor); for ndjson the body is one serialized asset-registry.v1 record per line separated by \n.

The response is returned as a downloadable file (asset-registry-{tenantId}.{format}) with content type application/x-ndjson (ndjson) or application/json (json). Headers:

Ordering is deterministic: asset type ascending, then assetId ascending.

Opaque Reference Boundary

Asset API responses may include opaque references:

Responses must not include dereferenced credential material, tokens, passwords, secret values, person names, emails, or human-actor details. Those remain owned by Authority or secret-provider integrations.

Typed Source-Service Boundaries

Graph exposes typed contracts in src/Graph/StellaOps.Graph.Api/Contracts/EuRuntimeContracts.cs so consumers can compile against stable boundaries without direct database access.

Authority Tenant Compliance Profile

Source of truth: Authority.

Read contract:

The same Authority group also exposes GET /api/v1/tenants/{tenantId}/assurance-packs and GET /api/v1/tenants/{tenantId}/submission-authorizations under the same authority:tenants.read scope; those are outside the EU runtime asset/registry boundary but share the tenant cross-check filter.

Status: available from Authority. Every read also writes an authority.tenant_compliance_profile.read audit event. The response includes only the normalized nullable tenant-compliance-profile.v1 payload and excludes operator settings, deployment posture, channel destinations, trust roots, signing provider, and secret references.

Notify Reporting Timeline

Source of truth: Notify/Timeline.

HTTP read API:

Response:

{
  "schemaVersion": "eu-reporting-timeline.v1",
  "items": [
    {
      "regime": "nis2",
      "incidentId": "incident-nis2-advanced",
      "state": "EARLY_WARNING_SENT",
      "openedAt": "2026-04-30T09:00:00Z",
      "closedAt": null,
      "milestone": "notification",
      "deadline": "2026-05-03T09:00:00Z",
      "deadlineStatus": "MISSED",
      "payloadHash": null,
      "signerRef": null,
      "replayHash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "evidenceRefs": ["cas://evidence/impact-summary"],
      "ledgerRefs": [
        "ledger://notify/nis2/opened/advanced",
        "ledger://notify/nis2/reported/early-warning"
      ],
      "auditEventIds": ["INC-2026-0429"],
      "milestones": [
        {
          "name": "early_warning",
          "transmissionKind": "early_warning",
          "deadline": "2026-05-01T09:00:00Z",
          "deadlineStatus": "COMPLETED",
          "completedAt": "2026-05-01T10:00:00Z",
          "transmissionId": "irtx_...",
          "payloadHash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
          "signerRef": "did:web:stella-ops.example#notify",
          "ledgerEventKind": "nis2.incident.reported",
          "requiredEvidence": ["impact_summary_ref", "incident_opened_event_ref"]
        }
      ],
      "transmissions": [
        {
          "transmissionId": "irtx_...",
          "milestone": "early_warning",
          "transmissionKind": "early_warning",
          "ledgerEventKind": "nis2.incident.reported",
          "sentAt": "2026-05-01T10:00:00Z",
          "payloadHash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
          "signerRef": "did:web:stella-ops.example#notify",
          "evidenceRefs": {
            "impact_summary_ref": "cas://evidence/impact-summary",
            "incident_opened_event_ref": "ledger://notify/nis2/opened/advanced"
          },
          "sourceEventRef": "ledger://notify/nis2/reported/early-warning",
          "hlc": "2026-04-30T08:00:00Z/0002"
        }
      ],
      "alerts": [
        {
          "severity": "SEV-1",
          "kind": "missed_deadline",
          "milestone": "notification",
          "deadline": "2026-05-03T09:00:00Z",
          "observedAt": "2026-05-03T16:00:00Z",
          "elapsedPercent": 1.0
        }
      ],
      "updatedAt": "2026-05-01T10:00:00Z"
    }
  ],
  "cursor": null,
  "sourceOfTruth": "Notify"
}

The response sample above is the live Notify HTTP payload (ReportingTimelineResponse / ReportingTimelineItem in ReportingTimelineEndpoints.cs), which is the rich source-of-truth shape.

Graph typed boundary: IReportingTimelineClient remains the consumer-facing Graph interface. Implementations must call this Notify source-owned API and must not read Notify persistence directly. The typed Graph DTOs (ReportingTimelineResponse / ReportingTimelineItem in EuRuntimeContracts.cs) are a thinner projection of the payload above: the typed item carries only regime, milestone, deadline, state, payloadHash, signerRef, evidenceRefs[], and auditEventIds[] — it does not surface incidentId, replayHash, milestones[], transmissions[], alerts[], or the open/closed/updated timestamps.

Status: available from Notify for persisted reporting timelines. The Policy control-register source API is documented below.

Policy NIS2 Control Register

Source of truth: Policy.

Source endpoint:

Sibling endpoints in the same group (same policy:read/policy:author discipline):

Query parameters:

Graph typed boundary:

Response shape (Nis2ControlRegisterResponse):

reviewStatus is computed against asOfDate: overdue when nextReview is before asOfDate, due-soon within 30 days, otherwise current.

The Graph typed boundary DTOs (Nis2ControlRegisterResponse / Nis2ControlRegisterItem in EuRuntimeContracts.cs) are a thinner projection than the source response above: the typed Nis2ControlRegisterResponse carries only schemaVersion, items[], cursor, and sourceOfTruth, and its Nis2ControlRegisterItem carries controlId, thematicArea, ownerRole, reviewStatus, evidenceRefs[], and tenantOverrideRefs[].

Ordering is deterministic by thematicArea, then controlId. The cursor is derived after filtering, so repeated calls with the same tenant, filters, asOfDate, and register state return the same pages.

Status: available from Policy as the source-owned read API. Graph and Web consumers must call this API or a typed client over it; they must not read Policy persistence tables directly.

EvidenceLocker Regulatory Artifact Ledger

Source of truth: EvidenceLocker.

HTTP read API:

Response:

{
  "schemaVersion": "eu-regulatory-artifact-ledger.v1",
  "items": [
    {
      "artifactId": "11111111-1111-5111-8111-111111111111",
      "regime": "dora",
      "artifactType": "dora-roi-snapshot",
      "reportingYear": 2025,
      "artifactHash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "evidenceRefs": ["bundle:roi-source-a"],
      "auditEventIds": ["22222222-2222-5222-8222-222222222222"],
      "schemaPinId": "eba-dora-roi-reporting-framework-4.0-taxo-package-4.0-errata5",
      "taxonomyPinId": "eba-dora-roi-reporting-framework-4.0-taxo-package-4.0-errata5",
      "signatureRef": "dsse:roi-2025",
      "signerKeyId": "stella-eu-signing-2026",
      "retentionRecordId": "33333333-3333-5333-8333-333333333333",
      "createdAt": "2025-12-31T23:59:00Z"
    }
  ],
  "cursor": null,
  "sourceOfTruth": "EvidenceLocker"
}

The HTTP response (RegulatoryArtifactLedgerResponse) uses schemaVersion = "eu-regulatory-artifact-ledger.v1" and each item carries artifactId, regime, artifactType, reportingYear, artifactHash, evidenceRefs[], auditEventIds[], schemaPinId, taxonomyPinId, signatureRef, signerKeyId, retentionRecordId, and createdAt. The auditEventIds[] are the non-null approval, generation, and signing audit event ids.

Graph typed boundary: IArtifactLedgerClient remains the consumer-facing Graph interface. Implementations must call this EvidenceLocker source-owned API and must not read EvidenceLocker tables directly. The typed Graph DTOs (ArtifactLedgerResponse / ArtifactLedgerItem in EuRuntimeContracts.cs) are a thinner projection: the typed response uses schemaVersion = "regulatory-artifact-ledger.v1" (note the absent eu- prefix versus the live HTTP payload above) and its item carries only artifactId, regime, artifactType, reportingYear, artifactHash, evidenceRefs[], and auditEventIds[].

Determinism