- deploy/senders-scope.{json,yaml}: nk-bounded-executor (source
net-kingdom.bounded-executor, tenants [platform], write-only,
secret_policy reject, load-bearing); inert until a token is seeded
- senders.py: the scope overlay may tighten secret_policy redact->reject,
never relax it
- networkpolicies.yaml: audit-core-nk-bounded-executor-ingress (sso AND
app.kubernetes.io/name=bounded-executor, TCP 8080); not applied
- tests on the executor's envelope shape: 202, 200 duplicate, 409, wrong
source/tenant, secret-shaped step check 400
- T03 unblocked (window bound = admission window.not_after)
- docs: emitter defect: executor AuditCore.emit rejects 202
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
11 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
dataverbatim 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 indata, where they read as claims. The storedoutcomeis alwaysrecordedandactoris alwaysnull(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:
target/actionare singular in the canonical field list but per-step in BEA section 6. Proposal: the receipt-leveltargetis the admission's target and the receipt-levelactionisbounded-execution. Each step's target and action go insteps[]. Audit-core needs one answer in the schema. It will not pick one.decision_refsmust 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.delegationstays absent when there is none.time_interval.boundstays absent when no bound is established. Unknown attribution stays the explicitunknown. This is unchanged fromdocs/cross-repo-evidence-admission.md.- 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,budgetand the typeddecision_refs; - an answer to point 1;
- the exact
sourceand event types; - the tenant binding;
- a decision on the
startedevent and the emission guarantee.
- the versioned receipt schema, validator, path and digest, with positive and
negative fixtures, covering
- 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.
Status 2026-10-31 — T02 prepared after NetKingdom's answers
NetKingdom answered on hub messages ed03ba30 and deea5bf8. The answers are
based on executor.py at net-kingdom a020758 and rapp-bounded-executor 9826a24.
- Source
net-kingdom.bounded-executor. Event typesexecution.run.startedandexecution.attribution.receipt. Tenantplatform, which is the exact string. Note that it is nottenant:platform, which approval-engine and informed-decision use. Thestartedevent is adopted, so the stream isload-bearing. - Registry entry (prepared, not seeded):
nk-bounded-executorindeploy/senders-scope.jsonanddeploy/senders-scope.yaml. The entry admits nothing until the Secret carries a token for that name. The overlay may now tightensecret_policyfromredacttoreject, but it may never relax it (audit_core/senders.py). Even if the Secret entry is minted with the default, the result is thereforereject. - Ingress (prepared, not applied):
audit-core-nk-bounded-executor-ingressindeploy/networkpolicies.yamladmits only namespacessocombined (AND) with pod labelapp.kubernetes.io/name=bounded-executor, on TCP 8080. - Tests:
tests/test_bounded_executor_sender.pyruns on the executor's envelope shape. It checks: 202 accept; 200 duplicate on retry; 409 on a reused run id with a different body; 400source_not_allowedandtenant_not_allowed; 400secret_shaped_fieldfor a secret-shaped step check. - Defect found in the emitter:
bounded-executor/clients.pyAuditCore.emitaccepts only 200 and 201. Audit-core answers a first accept with 202, so the executor would treat a storedstartedevent as a failure and stop withaudit_unavailable. NetKingdom has to accept 202 (accepted) and 200 (duplicate). It should treat 409 as a run-id-reuse defect and must not retry it. - T03 window bound: a
startedevent that has no receipt with the samecorrelation_idbecomes a finding after the admission'swindow.not_after. Pilot windows are 2 h.
Remaining work:
- Founder-run: mint and seed the Secret entry, then roll out the ConfigMap and the NetworkPolicy.
- NetKingdom: the versioned receipt schema with fixtures (NK-WP-0040-T02) and the 202 fix.
- railiance-platform: custody of the sender bearer (ESO projection to
sso).