Beacon Attestation Predicate Contract

Predicate Type: stella.ops/beaconAttestation@v1 Status: Partially implemented (see § Implementation Status) Sprint: SPRINT_20260219_014 Owning module: Signals (src/Signals/StellaOps.Signals)

The beacon attestation predicate is the Stella Ops contract for low-volume proof that a specific artifact actually executed in a real environment. Beacon events are captured via eBPF uprobe (Linux), ETW DynamicTraceProvider (Windows), or dyld interpose (macOS) on a designated “beacon function” — without modifying the artifact. This contract is for integrators emitting beacon events, policy authors gating releases on a verification rate, and reviewers of the Signals attestation surface. Read the implementation-status note below before relying on any field: several elements documented here are roadmap, not yet wired in code.

Implementation status (verified against src/Signals, src/Policy, and src/__Libraries/StellaOps.AuditPack at 2026-05-30). The beacon predicate (BeaconAttestationPredicate), ingest/rate HTTP API, nonce deduplication, sequence-gap detection, verification-rate computation (BeaconAttestationBuilder), the beacon-rate policy gate (BeaconRateGate), and optional audit-pack inclusion are implemented. All beacon code lives in Signals (src/Signals/StellaOps.Signals) and Policy.Engine (src/Policy/StellaOps.Policy.Engine); there is no beacon code in src/Attestor. The following elements described below are design/roadmap, not yet wired in code and are flagged inline where they appear:

  • DSSE signing of beacon predicatesBeaconAttestationBuilder returns a bare BeaconAttestationPredicate record; no DSSE envelope is constructed or signed inside Signals. The BeaconAttestationPredicate.PredicateTypeUri const exists, but nothing wraps the record in an in-toto Statement / DSSE envelope (see § DSSE Signing).
  • Time-based batch windowBatchWindowSeconds is defined in BeaconOptions but is not referenced anywhere in code; the only flush trigger that runs in production is the MaxBatchSize auto-flush in IngestAsync (see § Batching). There is no background flush timer and no HTTP flush endpoint.
  • Enabled flagBeaconOptions.Enabled is defined and defaults to true, but BeaconAttestationBuilder never reads it; ingest and flush run unconditionally. (The separate BeaconRateGateOptions.Enabled is honoured by the policy gate.)
  • Config-driven beacon-function designation — the Signals:Beacon:Functions block shown below does not exist on BeaconOptions; beacon source/function are reported by the ingesting agent as fields on each event (see § Beacon Function Designation).
  • Durable persistence — all state (nonce cache, pending batches, attestation history) is held in-memory in ConcurrentDictionary/ConcurrentBag/List and is lost on process restart; there is no PostgreSQL table for beacons. Note: Signals as a whole requires PostgreSQL for its other surfaces, but the beacon pipeline does not use it.

Overview

Schema

The beacon attestation predicate body (BeaconAttestationPredicate, src/Signals/StellaOps.Signals/Models/BeaconModels.cs) carries only the fields below. When emitted as an in-toto Statement / DSSE envelope, the predicateType and subject are part of the envelope/statement wrapper — they are not members of the predicate record itself.

Note: The outer predicateType / subject shown below illustrate the intended in-toto Statement wrapper. The current code path (BeaconAttestationBuilder.FlushBatchAsync) returns only the bare predicate record — it does not construct the Statement wrapper or a DSSE envelope (see § DSSE Signing, roadmap).

{
  "predicateType": "stella.ops/beaconAttestation@v1",
  "subject": [{"name": "artifact", "digest": {"sha256": "<canonical_id>"}}],
  "predicate": {
    "artifact_id": "sha256:<image_or_binary_digest>",
    "environment_id": "<env_identifier>",
    "beacon_source": "ebpf_uprobe",
    "beacon_function": "<symbol_name_or_address_range>",
    "window_start": "2026-02-19T12:00:00Z",
    "window_end": "2026-02-19T12:05:00Z",
    "beacon_count": 47,
    "first_sequence": 1,
    "last_sequence": 50,
    "sequence_gaps": 3,
    "verification_rate": 0.94,
    "timestamp": "2026-02-19T12:05:01Z"
  }
}

Field naming: the predicate record is serialized with snake_case JSON property names (each field is annotated with an explicit [JsonPropertyName(...)] in BeaconModels.cs).

Field Definitions

Statement / Envelope Fields

These belong to the in-toto Statement wrapping the predicate, not to the predicate record.

FieldTypeRequiredDescription
predicateTypestringyesAlways stella.ops/beaconAttestation@v1 (BeaconAttestationPredicate.PredicateTypeUri)
subjectarrayyesSingle-element array with artifact canonical ID

Predicate Fields

All predicate fields are required (non-nullable init properties on BeaconAttestationPredicate). window_start/window_end/timestamp are serialized as DateTimeOffset.

FieldTypeRequiredDescription
artifact_idstringyesCanonical artifact identifier (sha256 digest of the container image or binary). Copied verbatim from the ingested events.
environment_idstringyesIdentifier of the environment where beacon was observed
beacon_sourcestringyesInstrumentation source, copied from the first event in the batch. Conventionally ebpf_uprobe, etw_dynamic, or dyld_interpose, but not validated — any non-empty string is accepted (test fixtures use ebpf-uprobe).
beacon_functionstringyesSymbol name or address range of the beacon function, copied from the first event in the batch
window_startDateTimeOffset (ISO 8601)yesobserved_at of the earliest event in the batch (after sorting by sequence then timestamp)
window_endDateTimeOffset (ISO 8601)yesobserved_at of the latest event in the batch
beacon_countintyesNumber of accepted (deduplicated) beacon events in the batch
first_sequencelongyesLowest beacon_sequence in the batch
last_sequencelongyesHighest beacon_sequence in the batch
sequence_gapsintyesmax(0, (last_sequence - first_sequence + 1) - beacon_count)
verification_ratedoubleyesbeacon_count / (last_sequence - first_sequence + 1), clamped to 1.0 when the expected count is <= 0, rounded to 4 decimal places (0.0–1.0)
timestampDateTimeOffset (ISO 8601)yesWhen the attestation predicate was generated (TimeProvider.GetUtcNow() at flush)

Beacon Function Designation

Beacon functions are designated by environment-level configuration, not by modifying the artifact:

Signals:
  Beacon:
    Enabled: true
    Functions:
      - ArtifactPattern: "myapp:*"
        SymbolName: "healthCheck"
        Source: "ebpf_uprobe"
      - ArtifactPattern: "api-gateway:*"
        AddressRange: "0x1000-0x1100"
        Source: "etw_dynamic"

The eBPF/ETW probe attaches to the named symbol or address range externally. No entrypoint injection or image modification is performed. This preserves the attestation chain that Stella Ops signs and verifies.

Nonce and Sequence Semantics

DSSE Signing

Roadmap — not implemented in Signals. As of 2026-05-30, BeaconAttestationBuilder.FlushBatchAsync returns a bare BeaconAttestationPredicate; no DSSE envelope is constructed or signed inside the beacon pipeline, and no Signals endpoint emits a signed beacon attestation. The text below describes the intended design.

The intended design wraps beacon predicates in DSSE envelopes signed using the environment’s configured crypto profile. Targeted algorithms: EdDSA, ECDSA, RSA, GOST R 34.10, SM2, eIDAS QSealC, PQC (ML-DSA). Until implemented, any DSSE wrapping of a beacon predicate must be performed by the caller (e.g. the audit-pack producer that supplies AuditBundleWriteRequest.BeaconAttestation).

Batching

Beacon events are lightweight and high-frequency. Rather than attesting per-event, the pipeline accumulates events per (artifact_id, environment_id) in an in-memory ConcurrentBag and produces a single attestation predicate per batch when flushed.

Current flush trigger: the only flush that runs automatically is the size-based auto-flush inside IngestAsync — when a batch reaches MaxBatchSize (default: 1000) it is flushed by a fire-and-forget call to FlushBatchAsync (_ = FlushBatchAsync(...)). The resulting predicate is recorded into the in-memory attestation history (consumed by the rate query); the fire-and-forget call does not return the predicate to the ingesting HTTP caller.

Roadmap: the time-based BatchWindowSeconds window (default: 300s / 5 minutes) described elsewhere is not wired — there is no background timer that flushes batches on a schedule. A batch that never reaches MaxBatchSize is only summarised if FlushBatchAsync is called explicitly (e.g. from a test).

Configuration

Section: Signals:Beacon (BeaconOptions.SectionName, bound in Program.cs via AddOptions<BeaconOptions>().Bind(...)). Source: src/Signals/StellaOps.Signals/Options/BeaconOptions.cs.

OptionTypeDefaultDescription
EnabledbooltrueDefined on BeaconOptions. Not referenced by BeaconAttestationBuilder — the builder ingests and flushes regardless of this flag. (roadmap: gate the pipeline on this value)
BatchWindowSecondsint300Defined on BeaconOptions but not referenced anywhere in code (no background flush timer exists; see § Batching)
MaxBatchSizeint1000Max events per batch; the only production flush trigger (IngestAsync auto-flushes a batch once batch.Count >= MaxBatchSize)
NonceTtlSecondsint3600Nonce deduplication cache TTL (1 hour); enforced by EvictStaleNonces on every ingest
VerificationRateLookbackHoursint24Lookback window for GetVerificationRate

The beacon-rate policy gate is configured separately under PolicyGates:BeaconRate (PolicyGateOptions, src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateOptions.cs), not under Signals:Beacon. See § Policy Gate.

API Endpoints

Endpoints are mapped on the /signals group in src/Signals/StellaOps.Signals/Program.cs. Authorization is enforced per-handler via Program.TryAuthorize against the signals:* scopes defined in SignalsPolicies (src/Signals/StellaOps.Signals/Routing/SignalsPolicies.cs) — there is no per-route [Authorize] attribute; the handler returns 401 when unauthenticated and no X-Scopes fallback is present, or 403 when the scope is missing. All handlers also pass through the sealed-mode monitor (503 when sealed-mode evidence is invalid).

MethodPathRequired scopeEndpoint nameAudit action
POST/signals/beaconssignals:write (SignalsPolicies.Write)SignalsBeaconIngestregister_beacon (AuditActions.Signals.RegisterBeacon)
GET/signals/beacons/rate/{artifactId}/{environmentId}signals:read (SignalsPolicies.Read)SignalsBeaconRateQuerynone

No flush endpoint. IBeaconAttestationBuilder.FlushBatchAsync is a service method, not an HTTP route. It is only invoked by the MaxBatchSize auto-flush inside IngestAsync (and by tests). There is no operator-facing endpoint to force a batch flush.

POST /signals/beacons request — BeaconIngestRequest

{
  "events": [
    {
      "artifact_id": "sha256:<image_or_binary_digest>",
      "environment_id": "<env_identifier>",
      "beacon_source": "ebpf_uprobe",
      "beacon_function": "<symbol_name_or_address_range>",
      "nonce": "<unique_value>",
      "beacon_sequence": 1,
      "observed_at": "2026-02-19T12:00:00Z"
    }
  ],
  "metadata": { "<key>": "<value>" }
}

POST /signals/beacons response — BeaconIngestResponse

Returned with HTTP 202 Accepted (Location /signals/beacons):

{
  "accepted": 47,
  "rejected_duplicates": 3,
  "stored_at": "2026-02-19T12:05:01Z"
}

GET /signals/beacons/rate/... response — BeaconVerificationRate

Returned with HTTP 200 OK, or 404 when no attestation history exists for the (artifact, environment) pair. The rate is summed across all attestations within VerificationRateLookbackHours:

{
  "artifact_id": "sha256:<...>",
  "environment_id": "<env_identifier>",
  "rate": 0.8889,
  "total_expected": 18,
  "total_verified": 16,
  "window_start": "2026-02-18T12:05:01Z",
  "window_end": "2026-02-19T12:05:01Z"
}

rate = total_verified / total_expected (clamped to 1.0 when total_expected <= 0), rounded to 4 decimal places. window_start is now - VerificationRateLookbackHours; window_end is now.

Policy Gate

The beacon verification rate is consumed by the beacon-ratepolicy gate (BeaconRateGate, src/Policy/StellaOps.Policy.Engine/Gates/BeaconRateGate.cs), registered as an IContextPolicyGate via AddBeaconRateGate(). The gate does not call the Signals API directly — it reads the rate from two PolicyGateContext.Metadata keys supplied by the caller:

Metadata keyTypeDescription
beacon_verification_ratedouble (string)Verification rate (0.0–1.0) parsed with CultureInfo.InvariantCulture
beacon_verified_countint (string)Number of verified beacon events; gated against MinBeaconCount

Gate behaviour (config under PolicyGates:BeaconRate, all defaults are opt-in / warn): when Enabled is false it passes; when the target environment is not in RequiredEnvironments (default ["production"]) it passes; missing beacon data applies MissingBeaconAction (default Warn); a beacon_verified_count below MinBeaconCount (default 10) defers enforcement with a pass; a rate below MinVerificationRate (default 0.8) applies BelowThresholdAction (default Warn). Full reference: docs/modules/policy/gates/beacon-rate-gate.md.

Audit Pack Export

Beacon attestation predicates can be included in audit bundle exports as an optional entry at predicates/beacon-attestation.json. The entry is written by AuditBundleWriter.BuildBundleEntries (src/__Libraries/StellaOps.AuditPack/Services/AuditBundleWriter.Entries.cs) only when the caller supplies AuditBundleWriteRequest.BeaconAttestation (a byte[]?); when null, the bundle omits the entry. Its content type is BundleContentType.BeaconAttestation.

Integrity is not tracked via a checksums.sha256 file. Each bundle entry is recorded in the manifest.json (AuditBundleManifest) as a BundleFileEntry carrying the entry’s RelativePath, SHA-256 Digest, SizeBytes, and ContentType, and all entries are additionally bound by the manifest’s MerkleRoot. Offline verification re-derives each file digest and the Merkle root from the manifest.


Last updated: 2026-05-30 (reconciled against src/Signals, src/Policy, and src/__Libraries/StellaOps.AuditPack).