- sso delivery is ExternalSecret/Secret sso/bounded-executor-tokens key audit-core (the Secret the rapp run Job already mounts), not nk-bind-executor-audit/audit-token. - CCR check now also pins the new reader shape (audience openbao, explicit max TTL 15m, token_self_lifecycle) and that the CCR names this script as its custody procedure and the bounded-executor-tokens delivery. - Docs/workplan: this script is the single seeder; railiance-platform fences the boundary (nk_bind_executor_custody.py fence) and owns manifests/nk-bind-executor-eso.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 523928@bnt-lap001 Assistant-Session: 6c737afb-9c14-4152-9f02-477214b9c350
19 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).
Status 2026-11-03 — T03 detection implemented
A started event that has no receipt is reported on GET /v1/stream-findings
as opening_without_closing (see docs/stream-completeness.md, "Opening
events that never close"). The pair is declared on nk-bounded-executor in
deploy/senders-scope.{json,yaml} with a 7200 s bound, which matches the pilot
windows. The finding appears 3 h after acceptance at the latest, because the
heartbeat grace allowance is added.
The executor's started body (net-kingdom bounded-executor/executor.py)
does not yet carry the admission window. If NetKingdom adds
data.window.not_after, each run is held to its own window wherever that is
earlier than the declared bound. Without it, admissions longer than 2 h rely
on the declared ceiling.
The executor's emitter now accepts 202 (accepted) and 200 (duplicate)
(clients.AuditCore.emit), which closes the defect recorded on 2026-10-31.
Released 2026-11-03: receiver sha256:5f3d01c8… (built from 2b8c909) and
the scope ConfigMap carrying closing_classes are live on railiance01. The
detection is inert until the nk-bounded-executor token is seeded and the
executor emits.
Status 2026-11-04 — what is still needed live (NK-WP-0056-T15/T16)
Checked read-only on 2026-11-04 with the default (limited) identity:
- Ingress: not live.
audit-core-nk-bounded-executor-ingressis the only object indeploy/networkpolicies.yamlthat is absent on railiance01. The nine other policies in that file match the live specs exactly. The executor's own egress (sso/bounded-executor, toaudit-corepods on 8080) is already live. - Scope ConfigMap: the limited identity cannot read ConfigMaps in
audit-core. The release on 2026-11-03 is recorded in06d0029. The seed script below refuses unless the live entry equalsdeploy/senders-scope.json. - Bearer: not minted. The registry is OpenBao
platform/workloads/audit-core/senders(fieldsenders.json). ExternalSecretaudit-core/audit-core-sendersprojects it into the receiver. The same pattern served tenant-engine and the CCR-2026-0021/0022 senders.
How the token reaches both sides. One founder-run process,
scripts/seed-nk-bounded-executor-sender.py, does the following:
- It mints one bearer.
- It writes the bearer with cas 0 to
platform/workloads/net-kingdom/bounded-executor/audit-sender(AUDIT_TOKEN,CUSTODY_REQUEST=CCR-2026-0063). - It appends the
nk-bounded-executorentry to the registry. The entry is the declared scope plus the token, written with cas on the version read. Every other sender stays byte-equal. - It force-syncs
audit-core-sendersand compares the token by SHA-256 only. - It restarts the receiver.
- It proves the bearer without writing an event. Inside the receiver pod, with
the bearer on stdin:
- a read gets 403 (write-only);
- reconciliation for its own source and tenant
platformgets 200; - tenant
tenant:platformgets 403; - another source gets 403. An unknown bearer gets 401.
- It force-syncs
sso/bounded-executor-tokensand compares keyaudit-coreby hash, but only if that ExternalSecret exists. That is the Secret the rapp run Job already mounts.
The script is silent. The receipt holds versions, booleans and a 12-hex
fingerprint. A rerun reuses its own custody and writes nothing new. It refuses
foreign or drifted custody, a sender registered without custody, a token
collision and a concurrent registry write. Tests:
tests/test_seed_nk_bounded_executor_sender.py.
The script refuses before any write unless all of the following hold:
- CCR-2026-0063 is
approvedor later, with all three owner reviews. Its reader shape must be audienceopenbao, explicit max TTL and token self-lifecycle. Its delivery must besso/bounded-executor-tokenskeyaudit-core, and it must name this script as its custody procedure; - the cluster is railiance01 (kube-system UID);
- the receiver runs the pinned image;
- the live scope ConfigMap matches;
- the ingress policy exists;
- the coding-agent boundary already denies the custody data and metadata paths.
CCR-2026-0063 (railiance-platform f3b0c4f) names this script as its single
seeder. The script does not create the reader policy, the role or the boundary.
railiance-platform owns those: its credential-change applier creates the
policy and role, and scripts/nk_bind_executor_custody.py fence creates the
boundary. Nor does it create ClusterSecretStore openbao-nk-bind-executor-audit
or ExternalSecret sso/bounded-executor-tokens, which are railiance-platform's
manifests/nk-bind-executor-eso.yaml.
Founder runbook — nk-bounded-executor sender
Run these in order. Steps 1 and 2 can run now. Step 3 depends on CCR-2026-0063:
railiance-platform must have it approved by platform-owner, audit-core-owner and
net-kingdom-owner, must have applied its reader policy and role, and must have
run nk_bind_executor_custody.py fence for the coding-agent boundary entries.
1. Ingress (NK-WP-0056-T16). This applies exactly one object. Run the server dry-run first, then the apply:
cd /home/worsch/audit-core && python3 -c 'import sys,yaml; d=[x for x in yaml.safe_load_all(open("deploy/networkpolicies.yaml")) if x and x["metadata"]["name"]=="audit-core-nk-bounded-executor-ingress"]; assert len(d)==1; yaml.safe_dump(d[0],sys.stdout)' | KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl apply --dry-run=server -f -
cd /home/worsch/audit-core && python3 -c 'import sys,yaml; d=[x for x in yaml.safe_load_all(open("deploy/networkpolicies.yaml")) if x and x["metadata"]["name"]=="audit-core-nk-bounded-executor-ingress"]; assert len(d)==1; yaml.safe_dump(d[0],sys.stdout)' | KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl apply -f -
2. Read-only preflight (prints no secrets):
KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl -n audit-core get configmap audit-core-senders-scope -o jsonpath='{.data.senders-scope\.json}' | python3 -c 'import sys,json; print([e for e in json.load(sys.stdin) if e["name"]=="nk-bounded-executor"])'
KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl -n audit-core get deploy audit-core -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
bridge status
The image must end in
sha256:5f3d01c8a1f90411a1429451b46c66a9ac9262fc7fce68790c16e22eeb7871a6.
The openbao-ui-railiance01 tunnel must be up.
3. Seed and reload (NK-WP-0056-T15). Needs CCR-2026-0063 approved and applied. This is one silent command. Read the result from the receipt, not from the terminal:
BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 warden access openbao-platform-admin-login --exec -- /home/worsch/audit-core/scripts/attended-seed-nk-bounded-executor-sender.sh /home/worsch/audit-core/docs/evidence/nk-bounded-executor-sender-seed-r1.json
The receipt is created exclusively. If you rerun, use -r2.json. The receiver
restart is part of this step. No separate rollout is needed.
4. Verify (read-only):
python3 -c 'import json; r=json.load(open("/home/worsch/audit-core/docs/evidence/nk-bounded-executor-sender-seed-r1.json")); print(r["status"], r.get("error"), r.get("receiver_probe"), r.get("registry_secret_hash_match"), r.get("sso_secret_hash_match"), r["credential_values_emitted"])'
KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl -n audit-core get networkpolicy audit-core-nk-bounded-executor-ingress
The expected status is seeded_pending_sso_delivery, or seeded_and_delivered
once railiance-platform's ExternalSecret sso/bounded-executor-tokens exists. The expected probe is
{"read": 403, "own_tenant": 200, "other_tenant": 403, "other_source": 403}.
If railiance-platform applies manifests/nk-bind-executor-eso.yaml after the
seed, rerun step 3 with
-r2.json. It writes no custody or registry and confirms the sso copy by
hash.