audit-core/docs/bounded-executor-receipts.md
tegwick 1ef939b38a
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
AUDIT-WP-0018: answer NK-WP-0056-T02 — hold bounded-executor receipts, propose event mapping
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 994874@bnt-lap001
Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87
2026-10-31 14:19:10 +01:00

8.5 KiB

Bounded-executor execution-attribution receipts — owner answer

Answers NetKingdom NK-WP-0056-T02 (owner request net-kingdom/docs/proposals/2026-10-31-nk56-t02-owner-requests.md, section "audit-core"; hub message 755cbeed-ecbd-46d9-883e-958fbd88378b). Reviewed 2026-10-31 against net-kingdom/canon/standards/bounded-execution-admission_v0.1.md section 6 and playbook-capability-contract_v0.2.md "Execution Attribution Receipt (proposed)". Work: AUDIT-WP-0018. Canonical receipt validation remains AUDIT-WP-0014-T02.

This is an owner proposal. It activates nothing: registration, ingress and the sender bearer require the owner deliveries and the approved live change listed at the end.

Answer 1 — will audit-core hold the receipts?

Yes. Audit-core accepts the evidence-holder role for the bounded executor's receipts, as it already did for NK-WP-0040 (docs/cross-repo-evidence-admission.md, "Execution-attribution receipts"). What changes is the emitter: no longer a Railiance component but the NetKingdom-written executor, deployed by the new rapp-* package, running as service:nk-bind-executor in namespace sso.

Holding means what it means for every other sender (docs/event-envelope.md): audit-core authenticates the sender, checks source and tenant against the registered identity, stores the event verbatim, and chains it. It records that the executor said the run happened this way. It does not decide whether the decision_refs authorized the run, and it does not authenticate the initiator chain. That check belongs to the independent verifier in BEA section 6 ("Verify"), which reads the receipt from audit-core and compares it with the carrier record.

Until AUDIT-WP-0014-T02 ships canonical schema validation, an accepted receipt is generic JSON custody, not a schema-validated receipt. Audit-core will not describe it otherwise.

Answer 2 — one event, or separate events by run_id?

Both, in a fixed shape: one receipt event per run carries all per-step detail, plus one started event emitted when the executor has consumed the approval and before it takes its first step. Do not split steps into separate events.

Why the steps stay inside the receipt:

  • The envelope stores data verbatim as one object (docs/event-envelope.md). Per-step outcomes, check results, revert yes/no and budget used are the sender's claims. They belong in data, where they read as claims. The stored outcome is always recorded and actor is always null (docs/event-envelope.md, "What the receiver derives").
  • One receipt is the retained evidence unit. BEA section 6 says the receipt is "kept by hash". If steps were split into separate events, nobody could tell whether a step event was missing without a declared count. The receipt would then become a reconstruction, not a record.
  • The validator then sees one document, so decision_refs, budget and step list are checked together.

Why a separate started event is still needed: a receipt is emitted at the end of a run. If the executor crashes mid-run after it has consumed its single approval, it emits nothing. Without a second event, a run that partly executed leaves no record in custody. With a started event, a started that has no matching receipt after the admission window is detectable. Both events share the run_id.

Event mapping (proposal)

Both events use the existing eight-field envelope. No envelope change is needed.

Envelope field started event Receipt event
type execution.run.started execution.attribution.receipt
id / Idempotency-Key run:<run_id>:started run:<run_id>:receipt
source net-kingdom.bounded-executor (exact; emitter confirms) same
subject admission:<spec.id> same
tenant the admission's tenant, exact string (owner-bound, never inferred from namespace) same; must equal data.tenant
correlation_id <run_id> <run_id>
occurred_at consume time, offset-aware time_interval.end, offset-aware
data run_id, admission_id, admission_digest, approval_ref, executing_workload, runtime_principal, plan_digest, steps_planned the canonical receipt (below)

Each id is derived deterministically from run_id. A retry is therefore a 200 duplicate. A second, different body under the same id is a 409 conflict (audit_core/ingestion.py response table), which exposes reuse of a run id.

Proposed receipt data. The schema belongs to NetKingdom (NK-WP-0040-T02); this lists what audit-core needs to see in it:

{
  "receipt_version": "<canonical schema version>",
  "run_id": "…",
  "initiator": {"iss": "…", "sub": "admin"},
  "executing_workload": "service:nk-bind-executor",
  "runtime_principal": "system:serviceaccount:sso:<sa>",
  "tenant": "…",
  "environment": "…",
  "source_commit": "<plan.source.commit>",
  "artifact_digest": "<plan_digest>",
  "target": "<admission-level target>",
  "action": "bounded-execution",
  "decision_refs": [
    {"kind": "admission", "id": "<spec.id>"},
    {"kind": "admission_digest", "id": "sha256:…"},
    {"kind": "approval", "id": "<carrier approval reference>"}
  ],
  "time_interval": {"start": "…+00:00", "end": "…+00:00", "clock_source": "…"},
  "steps": [
    {"index": 1, "operation": "…", "target": "…", "action": "…",
     "outcome": "succeeded|failed|skipped",
     "checks": [{"id": "…", "result": "pass|fail|unknown"}]}
  ],
  "reverted": false,
  "budget": {"limit": {"…": 0}, "used": {"…": 0}},
  "outcome": "succeeded|failed|reverted"
}

The points that fit the existing model and need NetKingdom to decide:

  1. target/action are singular in the canonical field list but per-step in BEA section 6. Proposal: the receipt-level target is the admission's target and the receipt-level action is bounded-execution. Each step's target and action go in steps[]. Audit-core needs one answer in the schema. It will not pick one.
  2. decision_refs must be typed. The admission id, the admission digest and the approval reference are three different things. An untyped list cannot be checked against the carrier.
  3. delegation stays absent when there is none. time_interval.bound stays absent when no bound is established. Unknown attribution stays the explicit unknown. This is unchanged from docs/cross-repo-evidence-admission.md.
  4. Check results carry an id and a result only. They carry no response bodies, tokens, password values or LLDAP/OpenBao payloads.

Registration (proposal)

Registry field Proposed value Reason
name nk-bounded-executor one identity per executor deployment
sources ["net-kingdom.bounded-executor"] exact, no wildcard
tenants the exact admission tenant(s) not *; the executor acts for named tenants only
may_write / may_read true / false the verifier reads with its own read identity
secret_policy reject a receipt is kept by hash. If audit-core redacted it silently, the stored body would no longer match the hash the executor computed, so a secret-shaped field must refuse the event (400 secret_shaped_field). The executor treats that as a defect
evidence_kind load-bearing, if the started + receipt pair is adopted. Otherwise attributive with an explicit completeness_trade the pair is what makes a missing receipt detectable

Ingress: a new NetworkPolicy in deploy/networkpolicies.yaml admitting namespace sso and the executor's exact pod labels, following audit-core-tenant-engine-ingress. Do not admit the whole sso namespace, because Authelia, LLDAP and KeyCape run there too. The bearer is ESO-projected by railiance-platform, like the other in-cluster senders. No token value appears in this repository.

What audit-core needs back

  • NetKingdom (NK-WP-0040-T02 / NK-WP-0056):
    • the versioned receipt schema, validator, path and digest, with positive and negative fixtures, covering steps, reverted, budget and the typed decision_refs;
    • an answer to point 1;
    • the exact source and event types;
    • the tenant binding;
    • a decision on the started event and the emission guarantee.
  • The rapp-* package owner: the exact pod labels and ServiceAccount, and the image revision that emits.
  • railiance-platform: the ESO projection of the sender bearer (catalog/route via warden route), and the attended seed of the registry entry.
  • Founder: the live registry entry and NetworkPolicy rollout are an approved live change. Audit-core prepares them; the founder runs them.