Skip to content

Evidence & estate API

Complete HTTP reference for the evidence & estate API — the read-only service that assembles a per-app evidence record from the authoritative sources and lists the estate. For the workflow see query an app's evidence; for why evidence is assembled this way see the evidence chain.

Sources of truth: specs/003-governance-depth/contracts/evidence-api.md (delta on specs/001-walking-skeleton/contracts/evidence-estate-api.md) and services/evidence-estate-api/.

The read surface is a read-time projection and never mutates App CRs. The API additionally carries the OIDC-gated estate action surface: POST /estate/{app}/actions/{action} for suspend, resume, retire and restore. Its only write is patching a tendtech.io/<action> annotation carrying the server-stamped actor; it never writes status, and the operator is still the sole writer of authoritative state. The App CR remains the single source of truth.

Deployment facts

Property Value
Namespace tend-system
Service evidence-estate-api.tend-system.svc, port 8080 (ClusterIP)
App CR lookup namespace EVIDENCE_WATCH_NAMESPACE (default tend-system; empty = cluster-wide)

GET /apps/{name}/evidence

Returns the full per-app evidence record in one call. The App CR core (scopes, purpose, promotion, attestation refs) is merged with four live sources — Gitea, Harbor attestations, the data-gateway access log, and Argo CD — queried concurrently, each with its own 5-second budget. A slow or unreachable source degrades only the categories it feeds; it never fails the call. Responses are served from a 60-second TTL cache keyed by app (and dataAccessLimit). The performance budget is SC-003: full response in under 10 seconds.

Response When
200 with the EvidenceRecord Normal case, including partial degradation.
404 {"error":"app not found: …"} No App CR with that name in the watch namespace.
503 {"error":"evidence unavailable: …"} Only when no category could be assembled at all. Per-source failure never triggers this — it degrades completeness instead.
Query parameter Meaning
dataAccessLimit Widens the default-bounded dataAccess page. A page filled to exactly the limit is marked partial (more history may exist); auditors page the data gateway query API directly for full lifetime history.

Record fields

Field Type Source
app string App CR (metadata.name).
owner string App CR (spec.owner).
purpose object {category, decisionsAboutPeople} App CR; omitted when unset.
aiActTriage bool spec.purpose.decisionsAboutPeople.
dataScopes []string Declared scope names from the App CR (the 001 field shape is kept; structured scopes live on the CR).
prompts [{hash, at}] Attestation history.
models []string Distinct model identities from attestations.
artifacts [{image, attestationDigest, promptHash, model, builtAt}] Attestation history.
tests [{artifact, passed}] Attestation checks.
promotions [{prUrl, snapshotRef, decision, approver, scope, decidedAt}] CR promotion record, corroborated by Gitea PR metadata.
deployments [{env, image, at}] Argo CD application history (CR-derived production entry as seed).
dataAccess [{source, sourceType, requestedAccess, decision, reason, at}] Data-gateway decision log, newest-first, both outcomes.
tier1Posture object The Tier-1 Posture Record: where this app's inference prompts go and under what provider terms. Always present, never gated on the app's own spec; a missing record degrades it rather than dropping it.
completeness map category → enum One entry per category (below).
provenance map category → source id One entry per category (below).

dataScopes and purpose are CR fields on the record; the completeness/provenance maps cover the seven assembled categories.

Completeness

One entry per categoryprompts, models, artifacts, tests, promotions, deployments, dataAccess, tier1Posture — with values complete, partial, or unavailable. Partial results are never presented as complete; consumers must treat absent-with-unavailable differently from empty-with-complete.

Degradation Effect
Harbor unreachable prompts/models/artifacts (and tests on its own failure) → partial; CR-sourced values are kept, never wiped.
Gitea unreachable promotionspartial (CR record stands, uncorroborated).
Argo CD unreachable deploymentspartial.
Data gateway unreachable dataAccessunavailable — the gateway is the sole source; no CR fallback exists.
dataAccess page filled to the limit partial (possible truncation).

Provenance

The authoritative source that served each category. Hand-entered data is not a legal value.

Category Provenance value
prompts, models, artifacts, tests harbor-attestations (App CR status.attestationRefs as the seed/fallback)
promotions app-cr+gitea
deployments argocd
dataAccess data-gateway

Example response

{
  "app": "expense-helper",
  "owner": "u:alice@customer.example",
  "purpose": { "category": "internal-tool", "decisionsAboutPeople": true },
  "aiActTriage": true,
  "dataScopes": ["synthetic", "orders-db"],
  "prompts": [ { "hash": "sha256:77d0…", "at": "2026-07-02T10:14:00Z" } ],
  "models": [ "anthropic.claude-3-5-sonnet-20241022" ],
  "artifacts": [
    { "image": "harbor.tend-system.svc/apps/expense-helper@sha256:9f2c…",
      "attestationDigest": "sha256:41ab…", "promptHash": "sha256:77d0…",
      "model": "anthropic.claude-3-5-sonnet-20241022",
      "builtAt": "2026-07-02T10:14:00Z" }
  ],
  "tests": [ { "artifact": "sha256:41ab…", "passed": true } ],
  "promotions": [
    { "prUrl": "http://gitea.tend-system.svc/tend-apps/expense-helper/pulls/1",
      "snapshotRef": "a1b2c3d", "decision": "approved",
      "approver": "u:bob@customer.example", "scope": ["synthetic", "orders-db"],
      "decidedAt": "2026-07-03T09:00:00Z" }
  ],
  "deployments": [
    { "env": "production", "image": "sha256:9f2c…", "at": "2026-07-03T09:05:00Z" }
  ],
  "dataAccess": [
    { "source": "orders-db", "sourceType": "postgres",
      "requestedAccess": "read", "decision": "deny",
      "reason": "undeclared-source", "at": "2026-07-02T10:00:00Z" }
  ],
  "completeness": {
    "prompts": "complete", "models": "complete", "artifacts": "complete",
    "tests": "complete", "promotions": "complete", "deployments": "complete",
    "dataAccess": "unavailable"
  },
  "provenance": {
    "prompts": "harbor-attestations", "models": "harbor-attestations",
    "artifacts": "harbor-attestations", "tests": "harbor-attestations",
    "promotions": "app-cr+gitea", "deployments": "argocd",
    "dataAccess": "data-gateway"
  }
}

GET /estate

One row per App, sorted by name. All cost/suspension values are read-only projections of the App CR status (see App CRD — CostStatus).

Row field Source
name App CR name.
owner spec.owner.
environment Mirrors status.state.
dataScopes Declared scope names ([]string).
expiry spec.expiry (RFC3339; omitted when unset).
aiActTriage spec.purpose.decisionsAboutPeople.
cost {accrued, currency, capAmount, state}accrued/currency/state from status.costStatus; capAmount from spec.costCap (the cap is owner-declared, accrued is operator-observed). Apps without cost status report state: "unknown".
suspended true if and only if the Suspended condition is True.
Query parameter Meaning
aiActTriage=true Only apps flagged as making decisions about people — the compliance-triage view. Surface, don't adjudicate.
costState=<state> Only apps whose cost.state equals the value (normal, warning-75, warning-90, suspended, unknown).
{
  "apps": [
    { "name": "expense-helper", "owner": "u:alice@customer.example",
      "environment": "production", "dataScopes": ["synthetic", "orders-db"],
      "expiry": "2026-10-08T00:00:00Z", "aiActTriage": true,
      "cost": { "accrued": "37.10", "currency": "EUR",
                "capAmount": "50.00", "state": "warning-75" },
      "suspended": false }
  ]
}

Health endpoints

Endpoint Behavior
GET /healthz Liveness; always 200 ok.
GET /readyz Readiness; always 200 ok. Not gated on source reachability — missing sources degrade categories at read time instead of blocking startup.

Configuration

All configuration is environment variables. Every source client is optional: an unset base URL leaves that client unwired and its categories degrade to unavailable — a missing source config is not a crash.

Variable Default Meaning
EVIDENCE_LISTEN_ADDR :8080 HTTP listen address.
EVIDENCE_WATCH_NAMESPACE tend-system Namespace searched for App CRs; empty = cluster-wide.
GITEA_URL Gitea base URL (promotions/prompts corroboration).
GITEA_TOKEN Gitea API token.
GITEA_ORG tend-apps Gitea organisation app repos live under.
HARBOR_URL Harbor base URL (attestation categories).
HARBOR_PROJECT apps Harbor project holding app artifacts.
HARBOR_ROBOT_NAME Scoped robot account name — never the admin credential.
HARBOR_ROBOT_TOKEN Robot account token.
DATA_GATEWAY_URL Data gateway base URL for the platform query API.
ARGOCD_NAMESPACE argocd Namespace Argo CD Application objects live in.
OIDC_ISSUER Keycloak realm issuer for the client-credentials grant this service uses to call the data-gateway query API as a platform caller.
OIDC_TOKEN_URL derived: <issuer>/protocol/openid-connect/token Token endpoint for the client-credentials grant.
OIDC_CLIENT_ID tend-evidence-estate-api Confidential client id.
OIDC_CLIENT_SECRET Confidential client secret.