Findings Ledger — Runtime Instrumentation API Reference

Companion to architecture.md(overview) and runtime-instrumentation.md(algorithms). Source of truth for the wire shape is the OpenAPI document at openapi/findings-ledger.v1.yaml; this page is the human-readable per-endpoint reference.

All runtime endpoints share these conventions:


1. POST /{findingId}/runtime/traces — ingest a runtime trace

Required scope: findings.runtime.write

Accepts one runtime trace observation produced by an eBPF / syscall / APM probe. The service applies the privacy filter (strips @0x[hex]+ suffixes), persists the redacted trace, upserts the per-finding aggregate, and recomputes the runtime score in-line.

Request body

{
  "capturedAt": "2026-04-28T12:34:56Z",
  "artifactDigest": "sha256:abc123...",
  "componentPurl": "pkg:npm/lodash@4.17.21",
  "containerId": "container://prod/api-7f9c-2",
  "containerName": "api",
  "metadata": { "probe": "ebpf-uprobe", "host": "node-12" },
  "frames": [
    {
      "symbol": "main",
      "file": "/app/server.js",
      "line": 42,
      "isEntryPoint": true,
      "isVulnerableFunction": false,
      "confidence": 0.95
    },
    {
      "symbol": "lodash.merge@0x7FFEDEADBEEF",
      "isEntryPoint": false,
      "isVulnerableFunction": true,
      "confidence": 0.90
    }
  ]
}

containerName and metadata are first-class durable fields on the trace row since sprint 026 (FL-RUNTIME-V2-001…003). Both round-trip on the aggregate read response, sourced from the most recent persisted trace for the (tenant, finding). containerId, artifact/component identity, and redacted frames remain the primary correlation inputs.

Required fields

FieldTypeNotes
capturedAtRFC 3339 timestampWhen the probe captured the observation
artifactDigeststringOCI / build-artifact digest, non-empty
componentPurlstringSBOM component purl, non-empty
framesarray of frame objectAt least one frame; first frame with isVulnerableFunction=true is the bucket key

Response — 202 Accepted

{
  "findingId": "00000000-0000-0000-0000-000000000001",
  "observationId": "0192f5c7-4b18-7c04-8fe7-5b5a3b5b2c00",
  "recordedAt": "2026-04-28T12:34:56.789Z",
  "artifactDigest": "sha256:abc123...",
  "componentPurl": "pkg:npm/lodash@4.17.21",
  "frameCount": 2,
  "privacyFilterApplied": true
}

The Location response header points to the ingested trace (/api/v1/findings/{findingId}/runtime/traces/{observationId}). privacyFilterApplied is always true — the filter is unconditional.

Response — 400 BadRequest

problem+json shape per RFC 7807. Returned when the request body fails validation (missing/empty fields, no frames). Per-field errors are flattened into the errors map of an ASP.NET Core validation problem document:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "frames":         ["frames are required"],
    "artifactDigest": ["artifact digest is required"]
  }
}

Response — 401 Unauthorized / 403 Forbidden

Standard ASP.NET problem document. 403 is returned when the caller authenticates but lacks the findings.runtime.write scope.

Sample curl

curl -sS -X POST \
  "https://stella-ops.local/api/v1/findings/00000000-0000-0000-0000-000000000001/runtime/traces" \
  -H "Authorization: Bearer $STELLA_TOKEN" \
  -H "Content-Type: application/json" \
  -d @sample-trace.json

Common error patterns

StatusClient behavior
400Repair the payload; do not retry the same request as-is
401Refresh the bearer token and retry once
403Surface to operator; missing scope is not auto-recoverable
5xxRetry with exponential backoff (the ingest path is idempotent on (tenant, finding, capturedAt, hash))

2. GET /{findingId}/runtime/traces — list aggregates

Required scope: findings.runtime.read

Returns the redacted per-function aggregates for a finding.

Query parameters

NameDefaultRangeDescription
limit501…200Max aggregates returned
sortByhitshits, recentSort by hit count or recency

Response — 200 OK

{
  "findingId": "00000000-0000-0000-0000-000000000001",
  "collectionActive": true,
  "collectionStarted": "2026-04-25T08:00:00Z",
  "summary": {
    "totalHits": 1843,
    "uniquePaths": 3,
    "posture": "EbpfDeep",
    "lastHit": "2026-04-28T12:34:56Z",
    "directPathObserved": true,
    "productionTraffic": false,
    "containerCount": 4
  },
  "traces": [
    {
      "id": "sha256:abc123...|lodash.merge",
      "vulnerableFunction": "lodash.merge",
      "isDirectPath": true,
      "hitCount": 1500,
      "firstSeen": "2026-04-25T08:00:00Z",
      "lastSeen": "2026-04-28T12:34:56Z",
      "containerId": null,
      "containerName": "api",
      "metadata": { "probe": "ebpf-uprobe", "host": "node-12" },
      "callPath": []
    }
  ]
}

containerId is intentionally null on aggregate rows because the aggregate table stores containerCount rather than projecting individual container identities; containerId remains persisted on raw trace rows for retention-window evidence. containerName and metadata are sourced from the most recent persisted raw trace for the (tenant, finding) — sprint 026 (FL-RUNTIME-V2-003) promoted these fields from the v1 accepted-but-not-persisted caveat to first-class durable storage and added them to the aggregate response.

Response — 404 NotFound

Returned when there are no aggregates for (tenant, finding). Body intentionally empty — clients MUST treat 404 as “no data” rather than “server error”.

Sample curl

curl -sS \
  "https://stella-ops.local/api/v1/findings/00000000-0000-0000-0000-000000000001/runtime/traces?limit=10&sortBy=recent" \
  -H "Authorization: Bearer $STELLA_TOKEN"

Common error patterns

StatusClient behavior
401Refresh bearer token, retry once
403Surface; missing findings.runtime.read scope
404Render “no runtime evidence yet” UI; do not retry
5xxRetry with exponential backoff

3. GET /{findingId}/runtime/score — current runtime score

Required scope: findings.runtime.read

Returns the runtime trustworthiness score (0…1, displayed as 0…100) plus the per-component breakdown.

Response — 200 OK

{
  "findingId": "00000000-0000-0000-0000-000000000001",
  "score": {
    "score": 0.78,
    "breakdown": {
      "observationScore": 0.92,
      "recencyFactor": 0.85,
      "qualityFactor": 0.40
    },
    "computedAt": "2026-04-28T12:34:56Z"
  }
}

score.score is a normalized [0, 1] wire value adapted from the persisted domain score (0..100). The wire-side breakdown collapses the four pre-weight components into three buckets to match the existing UI contract:

For the full four-component math see ADR-016 Findings Runtime Score Formula.

Display vs. wire

The score.score field is normalized to [0, 1] on the wire so it composes with other 0…1 metrics (Evidence-Weighted Score, reachability confidence, advisory match score) without per-metric rescaling on the consumer side. Human-facing surfaces — CLI default human output, the Web UI gauge, and dashboards — multiply by 100 to display as a percentage. Concretely:

SurfaceValue rendered for score.score = 0.875
Wire / --output json0.875
stellaops findings runtime score get (human)87.50/100 (wire: 0.875)
Web UI score gauge87.5%
Sample SDK human print87.50/100 (wire: 0.875)

JSON callers MUST treat the wire value as [0, 1]. CLI / SDK callers that parse human output should ignore the post-multiplication form; parse JSON instead. The wire convention is stable across the v1 contract; new metric fields added under breakdown follow the same [0, 1] shape (see ADR-016 Findings Runtime Score Formula §wire shape).

Response — 404 NotFound

No score has been derived yet for (tenant, finding). Empty body.

Sample curl

curl -sS \
  "https://stella-ops.local/api/v1/findings/00000000-0000-0000-0000-000000000001/runtime/score" \
  -H "Authorization: Bearer $STELLA_TOKEN"

Common error patterns

StatusClient behavior
401Refresh bearer, retry once
403Surface; missing findings.runtime.read scope
404Render “no score yet” UI; pair with a “no traces yet” hint
5xxRetry with backoff

4. GET /{findingId}/runtime-timeline — chronological events

Required scope: findings.runtime.read

Note the path uses a dash (runtime-timeline), not a slash. This differs from the /runtime/... endpoints above and is contract-tested.

Returns chronologically-ordered events for a finding within a [from, to] window.

Query parameters

NameDefaultRangeDescription
fromto - 24hRFC 3339Inclusive start
tonowRFC 3339Inclusive end (must be >= from)
bucketHours11…168Time-bucket granularity (clamped to 7 days max)

Response — 200 OK

{
  "events": [
    {
      "timestamp": "2026-04-27T12:34:56Z",
      "kind": "trace_ingested",
      "correlationId": "0192f5c7-4b18-7c04-8fe7-5b5a3b5b2c00",
      "payload": { "frames": 7, "containerId": "..." }
    },
    {
      "timestamp": "2026-04-28T08:00:00Z",
      "kind": "score_computed",
      "correlationId": null,
      "payload": { "score": 0.78 }
    }
  ],
  "from": "2026-04-27T12:34:56Z",
  "to":   "2026-04-28T12:34:56Z",
  "bucketHours": 1
}

payload is an opaque JSON object whose shape varies by kind. Known event kinds: trace_ingested, score_computed, aggregate_updated.

Response — 400 BadRequest

Returned when from > to. Problem document with title=invalid_range.

Response — 404 NotFound

No events match the (tenant, finding, [from, to]) tuple.

Sample curl

curl -sS \
  "https://stella-ops.local/api/v1/findings/00000000-0000-0000-0000-000000000001/runtime-timeline?from=2026-04-27T00:00:00Z&to=2026-04-28T23:59:59Z&bucketHours=4" \
  -H "Authorization: Bearer $STELLA_TOKEN"

Common error patterns

StatusClient behavior
400Validate from <= to client-side and retry
401Refresh bearer, retry once
404Render “no timeline events” empty state
5xxRetry with backoff

4.1 v1 → v2 history — containerName + metadata persistence

Sprint reference: SPRINT_20260429_026 tasks FL-RUNTIME-V2-001..004 (lifted the v1 caveat originally codified in SPRINT_20260429_024 task FL-RUNTIME-HYG-003).

containerName and metadata were originally accepted on the wire but not persisted (v1) so that agents could emit a v2-ready payload before the server-side schema landed. Sprint 026 promoted both to first-class durable fields on findings.runtime_traces:

Fieldv2 behavior (current)
containerNamePersisted to the trace row; returned on aggregate reads from the latest trace.
metadataPersisted to the trace row as JSONB; returned on aggregate reads from the latest trace.

Concrete consequences for callers:

Backwards compatibility: pre-existing v1 rows (no values written) are correctly NULL for both columns and round-trip cleanly on read. v1 agents that omit either field continue to work without changes.


4.2 Live (v1.0) — GET /api/v1/capabilities/runtime

Sprint reference: SPRINT_20260429_025_FindingsLedger_runtime_capabilities_endpoint (docs/implplan/). Status: shipped — endpoint is reachable in production builds.

A read-only capabilities endpoint that lets UIs and CLIs distinguish three product states that otherwise look identical on the runtime read endpoints:

StateWire response on /runtime/*Capabilities endpoint returns
Operator has not enabled runtime instrumentation.404 NotFound{ enabled: false, lastIngestAt: null, ingestedTraceCount: 0 }
Enabled, no traces ingested yet.404 NotFound{ enabled: true, lastIngestAt: null, ingestedTraceCount: 0 }
Enabled, traces present.200 OK + body{ enabled: true, lastIngestAt: "2026-..", ingestedTraceCount: N>0 }

Request

GET /api/v1/capabilities/runtime
X-Tenant-Id: <tenant-uuid>
Authorization: Bearer <token>

Response — 200 OK

{
  "enabled": true,
  "lastIngestAt": "2026-04-28T08:30:42.317Z",
  "ingestedTraceCount": 42
}

Response — 401 Unauthorized / 403 Forbidden

Auth

Requires findings:read (same scope used by the runtime list / score read endpoints). The endpoint is tenant-scoped through the standard tenant accessor; data NEVER crosses tenants.

Sample curl

curl -sS -H "Authorization: Bearer ${TOKEN}" \
        -H "X-Tenant-Id: ${TENANT}" \
        https://stella-ops.local/api/v1/capabilities/runtime

5. Disabled-mode behavior (cross-cutting)

When findings:ledger:runtime:enabled = false (the default for new deployments):

EndpointBehavior in disabled mode
Ingest POSTReturns 202 Accepted with frameCount: 0 — no-op acknowledge
List GETReturns 404 NotFound
Score GETReturns 404 NotFound
Timeline GETReturns 404 NotFound

Operators flip the toggle as documented in enabling-runtime-instrumentation.md. Background and rationale: ADR-013 Findings Runtime Disabled Mode.


Cross-references