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, andsrc/__Libraries/StellaOps.AuditPackat 2026-05-30). The beacon predicate (BeaconAttestationPredicate), ingest/rate HTTP API, nonce deduplication, sequence-gap detection, verification-rate computation (BeaconAttestationBuilder), thebeacon-ratepolicy 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 insrc/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 predicates —
BeaconAttestationBuilderreturns a bareBeaconAttestationPredicaterecord; no DSSE envelope is constructed or signed inside Signals. TheBeaconAttestationPredicate.PredicateTypeUriconst exists, but nothing wraps the record in an in-toto Statement / DSSE envelope (see § DSSE Signing).- Time-based batch window —
BatchWindowSecondsis defined inBeaconOptionsbut is not referenced anywhere in code; the only flush trigger that runs in production is theMaxBatchSizeauto-flush inIngestAsync(see § Batching). There is no background flush timer and no HTTP flush endpoint.Enabledflag —BeaconOptions.Enabledis defined and defaults totrue, butBeaconAttestationBuildernever reads it; ingest and flush run unconditionally. (The separateBeaconRateGateOptions.Enabledis honoured by the policy gate.)- Config-driven beacon-function designation — the
Signals:Beacon:Functionsblock shown below does not exist onBeaconOptions; beaconsource/functionare 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/Listand 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
- Execution proof: Attestation that an artifact ran in a specific environment.
- Agent-reported instrumentation: Beacon
sourceandfunctionare reported as event fields by the eBPF/ETW/dyld observer. No container entrypoint or image modification is performed. (Config-driven designation is roadmap; see below.) - Batched attestation: Events are accumulated per
(artifact_id, environment_id)and summarised into a single predicate when a batch is flushed. (Currently the only production flush trigger isMaxBatchSize; the time window is roadmap.) - Nonce-protected: Per-event nonces prevent replay attacks. Sequence numbers detect gaps.
- Offline-first: No external service dependencies.
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/subjectshown below illustrate the intended in-toto Statement wrapper. The current code path (BeaconAttestationBuilder.FlushBatchAsync) returns only the barepredicaterecord — 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.
| Field | Type | Required | Description |
|---|---|---|---|
predicateType | string | yes | Always stella.ops/beaconAttestation@v1 (BeaconAttestationPredicate.PredicateTypeUri) |
subject | array | yes | Single-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.
| Field | Type | Required | Description |
|---|---|---|---|
artifact_id | string | yes | Canonical artifact identifier (sha256 digest of the container image or binary). Copied verbatim from the ingested events. |
environment_id | string | yes | Identifier of the environment where beacon was observed |
beacon_source | string | yes | Instrumentation 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_function | string | yes | Symbol name or address range of the beacon function, copied from the first event in the batch |
window_start | DateTimeOffset (ISO 8601) | yes | observed_at of the earliest event in the batch (after sorting by sequence then timestamp) |
window_end | DateTimeOffset (ISO 8601) | yes | observed_at of the latest event in the batch |
beacon_count | int | yes | Number of accepted (deduplicated) beacon events in the batch |
first_sequence | long | yes | Lowest beacon_sequence in the batch |
last_sequence | long | yes | Highest beacon_sequence in the batch |
sequence_gaps | int | yes | max(0, (last_sequence - first_sequence + 1) - beacon_count) |
verification_rate | double | yes | beacon_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) |
timestamp | DateTimeOffset (ISO 8601) | yes | When 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
- Nonce: Random unique value per beacon event. The deduplication cache rejects events with duplicate
(artifact_id, environment_id, nonce)tuples. Nonce TTL is configurable (default: 1 hour). - Sequence: Monotonically increasing counter per
(artifact_id, environment_id). Gaps in the sequence indicate missed beacon events (probe failure, artifact restart, or instrumentation issue). - Verification rate:
verified_beacons / (last_sequence - first_sequence + 1)over the batch window.
DSSE Signing
Roadmap — not implemented in Signals. As of 2026-05-30,
BeaconAttestationBuilder.FlushBatchAsyncreturns a bareBeaconAttestationPredicate; 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.
| Option | Type | Default | Description |
|---|---|---|---|
Enabled | bool | true | Defined on BeaconOptions. Not referenced by BeaconAttestationBuilder — the builder ingests and flushes regardless of this flag. (roadmap: gate the pipeline on this value) |
BatchWindowSeconds | int | 300 | Defined on BeaconOptions but not referenced anywhere in code (no background flush timer exists; see § Batching) |
MaxBatchSize | int | 1000 | Max events per batch; the only production flush trigger (IngestAsync auto-flushes a batch once batch.Count >= MaxBatchSize) |
NonceTtlSeconds | int | 3600 | Nonce deduplication cache TTL (1 hour); enforced by EvictStaleNonces on every ingest |
VerificationRateLookbackHours | int | 24 | Lookback window for GetVerificationRate |
The beacon-rate policy gate is configured separately under
PolicyGates:BeaconRate(PolicyGateOptions,src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateOptions.cs), not underSignals: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).
| Method | Path | Required scope | Endpoint name | Audit action |
|---|---|---|---|---|
POST | /signals/beacons | signals:write (SignalsPolicies.Write) | SignalsBeaconIngest | register_beacon (AuditActions.Signals.RegisterBeacon) |
GET | /signals/beacons/rate/{artifactId}/{environmentId} | signals:read (SignalsPolicies.Read) | SignalsBeaconRateQuery | none |
No flush endpoint.
IBeaconAttestationBuilder.FlushBatchAsyncis a service method, not an HTTP route. It is only invoked by theMaxBatchSizeauto-flush insideIngestAsync(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>" }
}
eventsis required and must contain at least one element (the handler returns400for an empty/null list).metadatais an optionalDictionary<string, string?>that is currently accepted but not persisted into the predicate.- A
BeaconEventwith a blankartifact_id,environment_id, ornonceis rejected and counted underrejected_duplicates(there is no separate “invalid” counter).beacon_sequenceis along;observed_atis aDateTimeOffset.
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 key | Type | Description |
|---|---|---|
beacon_verification_rate | double (string) | Verification rate (0.0–1.0) parsed with CultureInfo.InvariantCulture |
beacon_verified_count | int (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.
Related Documents
docs/contracts/execution-evidence-v1.md— Full execution evidence predicate (heavyweight complement)docs/contracts/witness-v1.md— Runtime witness predicatedocs/modules/policy/gates/beacon-rate-gate.md— Policy gate consuming beacon verification rate
Last updated: 2026-05-30 (reconciled against src/Signals, src/Policy, and src/__Libraries/StellaOps.AuditPack).
