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.cs POST /signals/execution-evidence) builds the predicate body and computes trace_digest + predicate_digest. DSSE envelope wrapping / Signer-Attestor signing of the executionEvidence@v1 predicate 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

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 populates replay_seed or expected_output_digest, so they are absent from emitted predicates even though the record declares them (see Determinism Metadata).

Field Definitions

The predicateType / subject in-toto statement wrapper is not part of the emitted predicate today. When DSSE wrapping is implemented (see DSSE Signing), the envelope is expected to carry predicateType = stella.ops/executionEvidence@v1 and a single-element subject referencing the artifact digest.

Predicate Fields

Source: ExecutionEvidencePredicate (ExecutionEvidenceModels.cs).

FieldTypeRequiredDescription
artifact_idstringyessha256:<digest> of the container image or binary
environment_idstringyesIdentifier of the environment where execution was observed
trace_sourcestringyesInstrumentation source: ebpf, etw, or dyld
observation_windowobjectyesTime window of the trace capture
trace_summaryobjectyesCoarse summary of observed behavior
trace_digeststringyessha256:<hex> digest of the canonical trace blob
determinismobjectyesReplay and determinism metadata
timestampISO 8601yesWhen the predicate was generated

Observation Window

FieldTypeRequiredDescription
startISO 8601yesTrace capture start time
endISO 8601yesTrace capture end time
duration_mslongyesWindow duration in milliseconds

Trace Summary

FieldTypeRequiredDescription
syscall_families_observedstring[]yesCoarse 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_symbolsstring[]yesTop-K symbols by aggregated hit count, descending then by name (capped by MaxHotSymbols)
hot_symbol_countintyesTotal number of distinct symbol_id values across all events (computed before the top-K cap)
unique_call_pathsintyesCount of distinct non-empty code_id values observed
address_canonicalizedboolyesAlways true — the builder always strips ASLR noise (loader_base0x0)

Determinism Metadata

FieldTypeRequiredDescription
replay_seedstringnoSeed for deterministic replay. Declared on DeterminismMetadata but never populated by the current ExecutionEvidenceBuilder; omitted from emitted JSON.
inputs_digeststringyessha256:<hex> of frozen input set. Computed over artifact_id | environment_id | trace_source | events.Count (ComputeInputsDigest).
expected_output_digeststringnoExpected 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).

FieldTypeRequiredDescription
artifact_idstringyesCanonical artifact identifier (sha256 digest)
environment_idstringyesEnvironment where the trace was captured
trace_sourcestringyesSource of the trace (ebpf, etw, dyld)
eventsRuntimeFactEvent[]yesRuntime fact events comprising the trace
observation_startISO 8601noStart of the observation window (drives observation_window.start)
observation_endISO 8601noEnd of the observation window (drives observation_window.end and duration_ms)
metadataobject<string,string?>noOptional 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).

FieldTypeDescription
evidence_idstringsee-<first-16-hex-of-predicate_digest>
artifact_idstringEchoed from the request
environment_idstringEchoed from the request
trace_digeststringsha256:<hex> of the canonical trace blob
predicate_digeststring64-char lowercase hex SHA256 of the canonicalised predicate JSON (no sha256: prefix)
created_atISO 8601Generation timestamp
rate_limitedbooltrue only on the rate-limited path (see Rate Limiting)

Privacy Canonicalization Rules

The following transformations are applied to raw trace data before predicate generation:

DataCanonicalizationRationale
Memory addresses (loader base)Stripped to zero-based offsetRemoves ASLR noise
Socket addressesPort stripped, IP retained as family indicatorPrivacy — port numbers leak service topology
Process namesRetained (coarse family classification only)Needed for process syscall family
Raw syscall sequencesCollapsed to family set (network, filesystem, process, memory)Privacy-safe, deterministic
Symbol namesRetained (hot symbols are public API surface)Required for trace utility
File pathsNot included in predicatePrivacy — paths leak deployment layout

Digest Computation

Trace Digest (ComputeTraceDigest)

  1. Group events by symbol_id (ordinal) and sum each group’s hit_count.
  2. Order groups by symbol_id ascending.
  3. Emit one line per group as "{symbol_id}:{sum_hit_count}\n" (UTF-8). This is a line-oriented canonical blob, not JSON.
  4. Compute SHA256 over the UTF-8 bytes; encode as lowercase hex.
  5. The predicate stores this as trace_digest = "sha256:" + hex.

Predicate Digest

  1. Serialize the predicate to canonical JSON (JsonNamingPolicy.SnakeCaseLower, null-omitting, no whitespace).
  2. Compute SHA256 over the UTF-8 bytes.
  3. 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). ExecutionEvidenceBuilder computes trace_digest and predicate_digest but 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. The ExecutionEvidenceResult returned 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 the executionEvidence@v1 predicate 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

OptionTypeDefaultDescription
EnabledbooltrueWhether the pipeline is active
RateLimitWindowMinutesint60Rate limit window per artifact/environment pair
MaxHotSymbolsint50Maximum hot symbols in trace summary
MinEventsThresholdint5Minimum events required to produce a predicate

API Endpoints

Mapped under the Signals /signals route group (Program.cs).

MethodPathScopeDescription
POST/signals/execution-evidencesignals:writeSubmit 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 202 response includes a Location header of /signals/execution-evidence/{artifactId}/{environmentId}, but that path is not mapped — there is no MapGet for it. The builder exposes GetCachedPredicate(artifactId, environmentId) in-process only; it is not reachable over HTTP. (Earlier revisions of this doc listed a GET here; 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:

Offline verification relies on the predicate’s trace_digest and predicate_digest; full signature-chain verification awaits the DSSE wrapping described above.


Last updated: 2026-05-30 (reconciled against src/Signals/StellaOps.SignalsExecutionEvidenceBuilder, ExecutionEvidenceModels, ExecutionEvidenceOptions, Program.cs; StellaOps.AuditPack).