# 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::started` | `run::receipt` | | `source` | `net-kingdom.bounded-executor` (exact; emitter confirms) | same | | `subject` | `admission:` | same | | `tenant` | the admission's tenant, exact string (owner-bound, never inferred from namespace) | same; must equal `data.tenant` | | `correlation_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: ```json { "receipt_version": "", "run_id": "…", "initiator": {"iss": "…", "sub": "admin"}, "executing_workload": "service:nk-bind-executor", "runtime_principal": "system:serviceaccount:sso:", "tenant": "…", "environment": "…", "source_commit": "", "artifact_digest": "", "target": "", "action": "bounded-execution", "decision_refs": [ {"kind": "admission", "id": ""}, {"kind": "admission_digest", "id": "sha256:…"}, {"kind": "approval", "id": ""} ], "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.