Execution Evidence Predicate Contract
Predicate Type: stella.ops/executionEvidence@v1 Status: Partially implemented — predicate build + digest + endpoint + policy gate + audit-pack export are live; DSSE signing and durable storage are not yet wired (see the implementation-status note below and DSSE Signing). Sprint: SPRINT_20260219_013 (archived)
This contract is for engineers integrating with the Stella Ops Signals service and for policy authors who gate releases on runtime behavior. It defines a deterministic attestation that a specific artifact was observed executing in a real environment, converting “it executed” from an implicit signal into a digest-bound executionEvidence@v1 predicate suitable for policy gates and audit packs.
Implementation status (verified against
src/Signals/StellaOps.Signals). The current pipeline (ExecutionEvidenceBuilder,Program.csPOST /signals/execution-evidence) builds the predicate body and computestrace_digest+predicate_digest. DSSE envelope wrapping / Signer-Attestor signing of theexecutionEvidence@v1predicate is not yet wired — see DSSE Signing. The predicate is held in an in-memory cache (not persisted to a database) and is surfaced for offline verification through the Audit Pack export.
Overview
- Auditability: Offline-verifiable proof that an artifact ran in a given environment (via
trace_digest/predicate_digest). - Determinism: Same trace input blob produces byte-identical predicate digests (address-canonicalized, sorted). Verified by
ExecutionEvidenceBuilderTests.BuildAsync_SameInputs_ProducesDeterministicDigest. - Privacy-safe: Coarse trace summary (syscall families, hot symbols, counts) — no raw syscall logs.
- Offline-first: No external service dependencies; the builder makes no network calls.
Schema
The predicate body produced by ExecutionEvidenceBuilder (ExecutionEvidencePredicate in src/Signals/StellaOps.Signals/Models/ExecutionEvidenceModels.cs). The builder emits only the predicate body below; the in-toto _type / predicateType / subject statement envelope shown in the design sprint is not materialised by the current pipeline (see DSSE Signing). The const ExecutionEvidencePredicate.PredicateTypeUri = "stella.ops/executionEvidence@v1" is the predicate-type identifier.
{
"artifact_id": "sha256:<image_or_binary_digest>",
"environment_id": "<env_identifier>",
"trace_source": "ebpf|etw|dyld",
"observation_window": {
"start": "2026-02-19T12:00:00Z",
"end": "2026-02-19T12:00:10Z",
"duration_ms": 10000
},
"trace_summary": {
"syscall_families_observed": ["network", "filesystem", "process"],
"hot_symbols": ["main", "handleRequest", "db.Query"],
"hot_symbol_count": 42,
"unique_call_paths": 17,
"address_canonicalized": true
},
"trace_digest": "sha256:<hash_of_canonical_trace_blob>",
"determinism": {
"inputs_digest": "sha256:<hash_of_frozen_inputs>"
},
"timestamp": "2026-02-19T12:00:10Z"
}
JSON property names are emitted in snake_case (
JsonNamingPolicy.SnakeCaseLower) and null-valued properties are omitted (JsonIgnoreCondition.WhenWritingNull). The current builder never populatesreplay_seedorexpected_output_digest, so they are absent from emitted predicates even though the record declares them (see Determinism Metadata).
Field Definitions
The
predicateType/subjectin-toto statement wrapper is not part of the emitted predicate today. When DSSE wrapping is implemented (see DSSE Signing), the envelope is expected to carrypredicateType = stella.ops/executionEvidence@v1and a single-elementsubjectreferencing the artifact digest.
Predicate Fields
Source: ExecutionEvidencePredicate (ExecutionEvidenceModels.cs).
| Field | Type | Required | Description |
|---|---|---|---|
artifact_id | string | yes | sha256:<digest> of the container image or binary |
environment_id | string | yes | Identifier of the environment where execution was observed |
trace_source | string | yes | Instrumentation source: ebpf, etw, or dyld |
observation_window | object | yes | Time window of the trace capture |
trace_summary | object | yes | Coarse summary of observed behavior |
trace_digest | string | yes | sha256:<hex> digest of the canonical trace blob |
determinism | object | yes | Replay and determinism metadata |
timestamp | ISO 8601 | yes | When the predicate was generated |
Observation Window
| Field | Type | Required | Description |
|---|---|---|---|
start | ISO 8601 | yes | Trace capture start time |
end | ISO 8601 | yes | Trace capture end time |
duration_ms | long | yes | Window duration in milliseconds |
Trace Summary
| Field | Type | Required | Description |
|---|---|---|---|
syscall_families_observed | string[] | yes | Coarse syscall families, sorted. The builder’s SyscallFamilyMap recognises network, filesystem, process, and memory. network is also inferred from any socket_address, process from any process_name, and process is added as a fallback when events exist but no family was classified. |
hot_symbols | string[] | yes | Top-K symbols by aggregated hit count, descending then by name (capped by MaxHotSymbols) |
hot_symbol_count | int | yes | Total number of distinct symbol_id values across all events (computed before the top-K cap) |
unique_call_paths | int | yes | Count of distinct non-empty code_id values observed |
address_canonicalized | bool | yes | Always true — the builder always strips ASLR noise (loader_base → 0x0) |
Determinism Metadata
| Field | Type | Required | Description |
|---|---|---|---|
replay_seed | string | no | Seed for deterministic replay. Declared on DeterminismMetadata but never populated by the current ExecutionEvidenceBuilder; omitted from emitted JSON. |
inputs_digest | string | yes | sha256:<hex> of frozen input set. Computed over artifact_id | environment_id | trace_source | events.Count (ComputeInputsDigest). |
expected_output_digest | string | no | Expected output digest. Declared but never populated by the current builder; omitted from emitted JSON. |
Request Body (ExecutionEvidenceRequest)
The POST /signals/execution-evidence body is a runtime trace, not a pre-built predicate. Source: ExecutionEvidenceRequest (ExecutionEvidenceModels.cs).
| Field | Type | Required | Description |
|---|---|---|---|
artifact_id | string | yes | Canonical artifact identifier (sha256 digest) |
environment_id | string | yes | Environment where the trace was captured |
trace_source | string | yes | Source of the trace (ebpf, etw, dyld) |
events | RuntimeFactEvent[] | yes | Runtime fact events comprising the trace |
observation_start | ISO 8601 | no | Start of the observation window (drives observation_window.start) |
observation_end | ISO 8601 | no | End of the observation window (drives observation_window.end and duration_ms) |
metadata | object<string,string?> | no | Optional provenance metadata (carried on the request, not copied into the predicate) |
Each RuntimeFactEvent (shared with POST /signals/runtime-facts; RuntimeFactsIngestRequest.cs) carries: symbolId (required), codeId, symbolDigest, purl, buildId, loaderBase, processId, processName, socketAddress, containerId, evidenceUri, hitCount (default 1), observedAt, and metadata.
Response Body (ExecutionEvidenceResult)
A successful POST returns 202 Accepted with this body and a Location header of /signals/execution-evidence/{artifactId}/{environmentId}. Source: ExecutionEvidenceResult (ExecutionEvidenceModels.cs).
| Field | Type | Description |
|---|---|---|
evidence_id | string | see-<first-16-hex-of-predicate_digest> |
artifact_id | string | Echoed from the request |
environment_id | string | Echoed from the request |
trace_digest | string | sha256:<hex> of the canonical trace blob |
predicate_digest | string | 64-char lowercase hex SHA256 of the canonicalised predicate JSON (no sha256: prefix) |
created_at | ISO 8601 | Generation timestamp |
rate_limited | bool | true only on the rate-limited path (see Rate Limiting) |
Privacy Canonicalization Rules
The following transformations are applied to raw trace data before predicate generation:
| Data | Canonicalization | Rationale |
|---|---|---|
| Memory addresses (loader base) | Stripped to zero-based offset | Removes ASLR noise |
| Socket addresses | Port stripped, IP retained as family indicator | Privacy — port numbers leak service topology |
| Process names | Retained (coarse family classification only) | Needed for process syscall family |
| Raw syscall sequences | Collapsed to family set (network, filesystem, process, memory) | Privacy-safe, deterministic |
| Symbol names | Retained (hot symbols are public API surface) | Required for trace utility |
| File paths | Not included in predicate | Privacy — paths leak deployment layout |
Digest Computation
Trace Digest (ComputeTraceDigest)
- Group events by
symbol_id(ordinal) and sum each group’shit_count. - Order groups by
symbol_idascending. - Emit one line per group as
"{symbol_id}:{sum_hit_count}\n"(UTF-8). This is a line-oriented canonical blob, not JSON. - Compute
SHA256over the UTF-8 bytes; encode as lowercase hex. - The predicate stores this as
trace_digest = "sha256:" + hex.
Predicate Digest
- Serialize the predicate to canonical JSON (
JsonNamingPolicy.SnakeCaseLower, null-omitting, no whitespace). - Compute
SHA256over the UTF-8 bytes. - Encode as lowercase hex (no
sha256:prefix).evidence_id="see-" + predicate_digest[..16].
DSSE Signing
Not yet implemented for
executionEvidence@v1(verified 2026-05-30).ExecutionEvidenceBuildercomputestrace_digestandpredicate_digestbut does not wrap the predicate in a DSSE envelope or call Signer/Attestor — it has no signing dependency. The original sprint design (SPRINT_20260219_013 SEE-02) called for a “Signer → Attestor proof chain for DSSE wrapping and storage”; that step was not delivered. TheExecutionEvidenceResultreturned by the endpoint contains digests only.The eBPF capture path (
StellaOps.Signals.Ebpf,LocalEvidenceChunkSigner/AttestorEvidenceChunkSigner) does produce signed DSSE statements, but for a different predicate type (stella.ops/runtime-evidence@v1) over evidence chunks — not theexecutionEvidence@v1predicate documented here.
When DSSE wrapping is implemented, predicates are intended to be wrapped in a DSSE envelope and signed using the environment’s configured crypto profile (planned: EdDSA, ECDSA, RSA, GOST R 34.10, SM2, eIDAS QSealC, PQC (ML-DSA), via the shared multi-profile Signer):
{
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64(predicate_json)>",
"signatures": [
{
"keyid": "<key_identifier>",
"sig": "<base64(signature)>"
}
]
}
Rate Limiting
One predicate per (artifact_id, environment_id) per configurable window (RateLimitWindowMinutes, default: 60 minutes). The window is tracked in an in-process ConcurrentDictionary (per Signals instance, lost on restart — there is no shared/persistent rate-limit store). Duplicate submissions within the window return 200 OK with body { "status": "rate_limited", "artifact_id": "…", "environment_id": "…" } and do not generate a new predicate.
Submissions with fewer than MinEventsThreshold events, or when the pipeline is disabled, return 422 Unprocessable Entity with { "error": "Insufficient trace events or pipeline disabled." }.
Configuration
Section: Signals:ExecutionEvidence
| Option | Type | Default | Description |
|---|---|---|---|
Enabled | bool | true | Whether the pipeline is active |
RateLimitWindowMinutes | int | 60 | Rate limit window per artifact/environment pair |
MaxHotSymbols | int | 50 | Maximum hot symbols in trace summary |
MinEventsThreshold | int | 5 | Minimum events required to produce a predicate |
API Endpoints
Mapped under the Signals /signals route group (Program.cs).
| Method | Path | Scope | Description |
|---|---|---|---|
POST | /signals/execution-evidence | signals:write | Submit a runtime trace (ExecutionEvidenceRequest); returns 202 Accepted with ExecutionEvidenceResult, 200 OK when rate-limited, 422 when below threshold/disabled, 400 when artifact_id/environment_id are missing. Emits the submit_execution_evidence audit action and is rejected with 503 when the instance is in sealed mode without the required state. |
No query/GET endpoint exists. The
202response includes aLocationheader of/signals/execution-evidence/{artifactId}/{environmentId}, but that path is not mapped — there is noMapGetfor it. The builder exposesGetCachedPredicate(artifactId, environmentId)in-process only; it is not reachable over HTTP. (Earlier revisions of this doc listed aGEThere; removed as not implemented.)
Authorization is enforced by Program.TryAuthorize against the scopes in SignalsPolicies (signals:read / signals:write / signals:admin); when authority anonymous fallback is enabled, scopes may instead be supplied via the X-Scopes header.
Audit Pack Export
Execution evidence predicates are included in Evidence Bundle / Audit Pack exports for offline verification (SEE-04). The StellaOps.AuditPack writer adds the predicate as an optional bundle entry:
- Bundle path:
predicates/execution-evidence.json(AuditBundleWriter.Entries.cs) - Content type:
BundleContentType.ExecutionEvidence - Sourced from
AuditBundleWriteRequest.ExecutionEvidence
Offline verification relies on the predicate’s trace_digest and predicate_digest; full signature-chain verification awaits the DSSE wrapping described above.
Related Documents
- Witness v1 — Runtime witness predicate (complementary)
- Beacon Attestation v1 — Beacon attestation predicate (lightweight complement)
- Execution Evidence Gate — Policy gate consuming this predicate
Last updated: 2026-05-30 (reconciled against src/Signals/StellaOps.Signals — ExecutionEvidenceBuilder, ExecutionEvidenceModels, ExecutionEvidenceOptions, Program.cs; StellaOps.AuditPack).
