Predicate Schema: stella.ops/federatedConsent@v1

Overview

This contract is for engineers integrating with Stella Ops federated telemetry. The stella.ops/federatedConsent@v1 predicate represents a consent attestation for federated telemetry participation: a valid consent proof must exist before any telemetry data can be aggregated and shared with federation peers. The companion bundle contract is federated-telemetry-v1.md(stella.ops/federatedTelemetry@v1).

The consent surface is implemented in StellaOps.Telemetry.Federation (Consent/ConsentManager.cs, Consent/IConsentManager.cs) and exposed over HTTP by the Platform WebService (Endpoints/FederationTelemetryEndpoints.cs). The predicate type string is configurable via FederatedTelemetryOptions.ConsentPredicateType (FederatedTelemetry:ConsentPredicateType) and defaults to stella.ops/federatedConsent@v1.

Implementation note: consent state is held in-memory by the singleton ConsentManager (a ConcurrentDictionary keyed by tenant ID). There is currently no PostgreSQL persistence or migration for federated consent — state does not survive a Platform WebService restart. This is the documented MVP behaviour as of this contract version.

Predicate Type

stella.ops/federatedConsent@v1

The sibling telemetry-bundle predicate is stella.ops/federatedTelemetry@v1 (FederatedTelemetryOptions.BundlePredicateType); it is out of scope for this contract.

Schema

This is the DSSE payload document — the JSON object that is base64-encoded into the payload field of the signed DSSE envelope. It is emitted by ConsentManager.ConsentPayloadDocument and serialized with System.Text.Json (camelCase property names as written below).

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["tenantId", "grantedBy", "grantedAt", "type"],
  "properties": {
    "tenantId": {
      "type": "string",
      "description": "Identifier of the tenant granting consent. On the HTTP API this is taken from the authenticated tenant context, not the request body."
    },
    "grantedBy": {
      "type": "string",
      "description": "Free-form identifier of the actor who granted consent (e.g. an email, service-account name, or operator handle). The code treats it as an opaque string and does not validate it as an email."
    },
    "grantedAt": {
      "type": "string",
      "format": "date-time",
      "description": "UTC timestamp when consent was granted. Set server-side to the current time from the injected TimeProvider."
    },
    "expiresAt": {
      "type": ["string", "null"],
      "format": "date-time",
      "description": "Optional expiry timestamp; null means consent has no TTL. Computed as grantedAt + ttl when a TTL is supplied."
    },
    "type": {
      "type": "string",
      "const": "stella.ops/federatedConsent@v1",
      "description": "Predicate type. Matches FederatedTelemetryOptions.ConsentPredicateType; the const value is the default."
    }
  }
}

The lifecycle is modelled by ConsentManager. Note that “Expired” and “Revoked” are not persisted as distinct records: an expired entry is removed lazily on the next read (GetConsentStateAsync), and a revoke removes the entry outright (RevokeConsentAsync). In both cases GetConsentStateAsync subsequently reports granted: false — i.e. the tenant returns to the Not Granted state. The states below are therefore observable conditions rather than stored status values.

StateDescriptionTransition
Not GrantedDefault state; no federation data shared-> Granted (via POST /api/v1/telemetry/federation/participation/consent/grant)
GrantedActive consent; federation data flows-> Revoked (via POST /api/v1/telemetry/federation/participation/consent/revoke) or -> Expired (TTL)
ExpiredTTL elapsed; entry dropped on next read, reverts to Not Granted-> Granted (re-grant)
RevokedExplicit revocation; entry removed, reverts to Not Granted-> Granted (re-grant)

Validation Rules

The following describe how ConsentManager.VerifyProofAsync actually validates a ConsentProof, plus a note on what is not checked.

  1. tenantId is bound to the authenticated tenant context by the HTTP grant endpoint (the request body has no tenantId field), so it is enforced by construction rather than compared. VerifyProofAsync additionally requires the envelope payload’s tenantId to equal proof.TenantId.
  2. The envelope’s payloadType must equal the configured ConsentPredicateType, and the payload’s type, grantedBy, grantedAt, and expiresAt fields must all match the corresponding ConsentProof values exactly.
  3. If proof.SignerKeyId is set, it must match the verified envelope’s signer key ID.
  4. The proof must not be expired at verification time: if expiresAt is set and now >= expiresAt, verification fails.
  5. The consent proof is DSSE-signed; the envelope digest (computed over the full DSSE envelope bytes, not the payload alone) is recorded and surfaced as sha256:<lowercase-hex> (HmacFederationDsseEnvelopeService.ComputeDigest). VerifyProofAsync recomputes the envelope digest and rejects the proof if it does not match proof.DsseDigest.

Not enforced: there is no check that grantedAt is “not in the future” and no standalone expiresAt > grantedAt validation. grantedAt is always set server-side to the current time, and expiresAt, when present, is computed as grantedAt + ttl, so these conditions hold by construction rather than being validated.

Example Payload

{
  "tenantId": "org-acme-production",
  "grantedBy": "admin@acme.com",
  "grantedAt": "2026-02-20T10:00:00Z",
  "expiresAt": "2026-03-20T10:00:00Z",
  "type": "stella.ops/federatedConsent@v1"
}

DSSE Envelope and Signing

The payload above is wrapped in a DSSE envelope, not signed directly. HmacFederationDsseEnvelopeService produces an envelope of the form:

{
  "payloadType": "stella.ops/federatedConsent@v1",
  "payload": "<base64 of the JSON payload>",
  "signatures": [ { "keyid": "<DsseSignerKeyId>", "sig": "<base64 signature>" } ]
}

HTTP API

The consent surface is served by Platform under /api/v1/telemetry/federation/participation. All participation endpoints require an authenticated tenant; actor identity is derived from authentication and is never accepted from request JSON.

MethodRouteScopeDescription
GET/api/v1/telemetry/federation/participation/consenttelemetry:participation:readGet current consent state for the authenticated tenant.
POST/api/v1/telemetry/federation/participation/consent/granttelemetry:participation:writeGrant federation telemetry consent.
POST/api/v1/telemetry/federation/participation/consent/revoketelemetry:participation:writeRevoke federation telemetry consent.

Scope policies map as follows (PlatformScopes / PlatformPolicies): the FederationRead policy requires platform:federation:read (StellaOpsScopes.PlatformFederationRead) and the FederationManage policy requires platform:federation:write (StellaOpsScopes.PlatformFederationWrite). The same route group also serves read-only federation endpoints (/status, /bundles, /bundles/{id}, /intelligence, /privacy-budget) and a POST /trigger aggregation endpoint, which are outside the scope of this consent contract.

Grant request

FederationGrantConsentRequest:

{ "ttlHours": 720 }

Grant response

FederationConsentProofResponse:

{
  "tenantId": "org-acme-production",
  "grantedBy": "admin@acme.com",
  "grantedAt": "2026-02-20T10:00:00Z",
  "expiresAt": "2026-03-20T10:00:00Z",
  "dsseDigest": "sha256:<hex>"
}

The HTTP response intentionally omits the raw DSSE envelope bytes and signerKeyId; those are carried on the internal ConsentProof record (IConsentManager) but are not returned over the API.

FederationConsentStateResponse:

{
  "granted": true,
  "grantedBy": "admin@acme.com",
  "grantedAt": "2026-02-20T10:00:00Z",
  "expiresAt": "2026-03-20T10:00:00Z",
  "dsseDigest": "sha256:<hex>"
}

When no consent is recorded (or it has expired), all nullable fields are null and granted is false.

Revoke request

FederationRevokeConsentRequest:

{ "revokedBy": "admin@acme.com" }

Returns { "revoked": true } on success. revokedBy is accepted but, in the current implementation, is not persisted (revocation simply removes the in-memory entry).

Configuration

Bound from the FederatedTelemetry section (FederatedTelemetryOptions):

KeyDefaultNotes
FederatedTelemetry:ConsentPredicateTypestella.ops/federatedConsent@v1DSSE predicate type for consent.
FederatedTelemetry:BundlePredicateTypestella.ops/federatedTelemetry@v1DSSE predicate type for telemetry bundles.
FederatedTelemetry:SiteIddefaultThis site’s identifier in the federation mesh.
FederatedTelemetry:SealedModeEnabledfalseWhen true, no outbound federation traffic.
FederatedTelemetry:DsseSignerKeyId(empty)Key ID used when signing envelopes.
FederatedTelemetry:DsseSignerSecret(empty)HMAC secret for the default signer.
FederatedTelemetry:DsseTrustedKeys(empty map)Trusted signer secrets keyed by key ID, for verification.
FederatedTelemetry:AllowUnsignedDsseFallbackfalseWhen true, missing key material yields unsigned envelopes tagged offline-unsigned-fallback.