# Bounded execution: cross-repo contract table NK-WP-0058-T01 (part 1), T09 (part 2) and T10 (part 3), as of 2026-11-05. NK-WP-0057-T01 takes the rows marked **owner decision** as inputs and does not inventory them again. Each row is one interface on the bounded-execution path. Every claim cites the file that defines it, at these revisions: net-kingdom `23a99c1`, rapp-bounded-executor `3ecd462`, approval-engine `f41a9ca`, informed-decision `77161fe`, audit-core `3157454`, key-cape `82b471f`, railiance-platform `31cf74d`. **Marks.** - **keep:** one owner, one source, so it only needs a contract test (T02). - **merge:** the contract is written down in more than one place. Keep the named source and derive or check the copies. - **delete:** the piece is redundant. - **owner decision:** the owner or the boundary is unclear. It goes to NK-WP-0057 or to the named owner. **Tested today.** The levels are: - **CI:** runs in a Forgejo workflow on every change; - **local:** a test that needs a sibling checkout and skips without one; - **manual:** an exercise script someone has to run; - **live:** proven only by a founder-run step; - **none.** Two facts frame this column: - net-kingdom CI (`.forgejo/workflows/identity-journeys.yaml`) tests a source tarball of net-kingdom alone, so every sibling-dependent test skips there. - rapp-bounded-executor has no CI workflow. ## Part 1: the executor and its services | ID | Interface | Owner and source of truth | Consumer | Tested today | 2026-11-04/05 failure | Mark | | --- | --- | --- | --- | --- | --- | --- | | E1 | OpenBao: Kubernetes login and KV, three lanes | railiance-platform: CCR-2026-0058, -0059, -0061 and `openbao/policies/` | executor (`operations.py OpenBao`); `executor-lanes.py` creates and deletes the write role | CI: fake OpenBao. Local: `tests/test_executor_lanes.py` against the CCRs | Write role left open overnight; `window-close` failed as bare `LaneError` | **merge** | | E2 | KeyCape `client_credentials` token | key-cape: `src/internal/server/oidc/token.go`. Client registration: net-kingdom `sso-mfa/k8s/keycape/` with `keycape-client.py` | executor (`clients.py KeyCapeClientCredentials`) | CI: fakes. Manual: `exercise_approval_identity.py` (T19) against KeyCape source/image | none (the request phase worked) | **keep**; registration ownership is an **owner decision** | | E3 | approval-engine: create, get and consume | approval-engine: `approval_engine/api.py`, `store.py` | executor (`clients.py ApprovalEngine`, `executor.py _check_approval`) | CI: fakes. Manual: T19 against approval-engine source | `platform-root` approval refused as `approval_entry`; finding which field failed took diagnostic pod 4 | **keep** | | E4 | informed-decision: memo intake (`present_existing`) | informed-decision: `informed_decision/intake_service.py`, `deploy/lifecycle/intake-submitters.json` | executor (`clients.py InformedDecision`, `cli.py admission_memo`) | Local: `test_approval_request.py`, path hard-coded to `/home/worsch/informed-decision` | Memo reached `platform-root` only, so one window was wasted (routing, E5) | **keep** | | E5 | The approver's identity across repos | **None.** It is defined four times (below) | executor `_check_approval`, informed-decision routing, the founder | none | One wasted window: the admission named `admin`, routing went to `platform-root` | **merge**; **owner decision** (NK-WP-0057) | | E6 | audit-core: event ingest | audit-core: `docs/event-envelope.md`, `audit_core/ingestion.py`, sender registry. Receipt schema: net-kingdom `canon/schemas/execution-attribution-receipt_v0.1.schema.json` | executor (`clients.py AuditCore`) | CI: fakes. Manual: `exercise_audit_sender.py` (T15) against audit-core source. Receipt validator not in a workflow | none (receipt accepted 2026-11-05) | **keep** | | E7 | LLDAP: sign-in and self password change | LLDAP upstream. The runtime in `sso` has no owner in `docs/responsibility-map.md`; its reference manifests are in net-kingdom `sso-mfa/k8s/` | executor (`operations.py Directory`, the image's `lldap_set_password`) | CI: fakes. Nothing pins the image's tool version against the server | none | **keep**; runtime ownership is an **owner decision** | | E8 | Kubernetes API: ESO force-sync and Deployment restart | API: railiance-cluster. Role content: net-kingdom `sso-mfa/k8s/bounded-executor/rbac.yaml`, copied to rapp `manifests/rbac.yaml` | executor (`operations.py KubeDelivery`, `KubeRestart`) | Make only: `test_rbac.py` needs no sibling, but no workflow runs it. Local: rapp `test_ceiling_is_the_net_kingdom_source` | none in the run (ESO server-side apply was a founder step, T10) | **merge** | | E9 | Network path of the executor pod | rapp `manifests/networkpolicy.yaml` (egress). Ingress belongs to each target: net-kingdom `netpol-sso.yaml` (lldap, keycape), approval-engine `e9e9f93`, audit-core `7728fde`, informed-decision `:8081`, OpenBao | executor `wait_all_reachable`, `make netcheck` | Local: rapp `test_every_in_namespace_egress_is_admitted_by_net_kingdom`. Live: `make netcheck` | Two windows refused `setup:URLError`; four diagnostic pods; live approval-engine had a policy its file lacked | **keep**; add a cross-owner ingress test | | R1 | Image build and pin chain | net-kingdom builds (`.forgejo/workflows/bounded-executor-image.yaml`); rapp pins (`Makefile IMAGE_DIGEST`); the admission pins source files (`plan.source.commit` and file sha256) | renderer (`tools/render_job.py`), the executor's checks | none: commit to digest is recorded only in the rapp commit message | A re-pin and a new admission after `c2a0f2f`, all by hand | **merge** | | R2 | Runtime config, Job template and exit codes | net-kingdom `bounded-executor/cli.py` (config keys in its docstring, `EXIT`). Copies: rapp `examples/config.pilot.json`, `docs/runtime-contract.md`, `render_job.py` allow-lists | rapp renderer, `pilot-run.sh`, the founder | Local: rapp `test_packaging.py` reads `cli.py` (skips without sibling); no rapp CI | `pilot-run.sh` edits `trust_satisfied` into a temporary copy of the config | **merge** | ## Row details ### E1: OpenBao lanes - **Contract:** - address `http://openbao.openbao.svc:8200`; - login `POST /v1/auth/kubernetes/login` with the projected token, audience `openbao`, from `/var/run/secrets/openbao/token`; - one role per lane, with leases of at most 15 minutes, each revoked at exit: - write: `net-kingdom-bind-executor-svc-authelia-write`, KV `platform/workloads/net-kingdom/directory-binds/svc-authelia`, field `LDAP_BIND_PASSWORD`, `cas_required`; - journey: `net-kingdom-bind-executor-syn-journey-read`, KV `.../synthetic-journey/syn-journey`, fields `PASSWORD`, `TOTP_SEED`; - approval client: `keycape-nk-bind-executor-approval-bounded-executor-workload-kv-read`, field `CLIENT_SECRET`, read once and revoked at once. - **Window:** the write role exists only between `executor-lanes.py window-open` and `window-close`. - **Duplicates:** role names, KV paths and fields appear in four places: the CCR files, `executor-lanes.py LANES`, rapp `config.pilot.json` and the rapp renderer's role allow-list. `LANES` also carries the write and journey policy text inline. - **Proposal:** - `LANES` reads names, paths and fields from the CCRs. The policy text is compared against railiance-platform's policy file, not kept as a second copy. - The renderer's allow-list is checked against the same source. - `executor-lanes.py` names the lane error's cause. On 2026-11-05 `phase: identity, LaneError` hid "no platform-admin login" (T03). ### E2: KeyCape client - **Contract:** - `POST http://keycape.sso.svc.cluster.local:8080/token` with Host `kc.coulomb.social`, `client_secret_basic`, client `nk-bind-executor-approval`; - one scope per token (`approval:create`, `approval:read`, `approval:consume`); - the answer must be a bearer token with `expires_in` of at most 900; - a scope outside the list must be answered `invalid_profile_usage`. - **Owners:** KeyCape owns the token behaviour. The client's registration lives in net-kingdom `sso-mfa/k8s/keycape/` (deployment env and `keycape-client.py`), and its case is `security-cases/nk-bind-executor-approval.yaml` (oidc-service-client). - **Open:** - `excess-scope-refused` is not yet proven against live KeyCape. - **Owner decision:** ADR-0015 moves runtime packages to `rapp-*`, but there is no KeyCape rapp, so the client registration sits in net-kingdom's reference manifests. Who owns registering a KeyCape client? ### E3: approval-engine - **Contract:** - create: `POST /v1/approvals` answers 201. `binding.actor` must equal the token's `sub` (`api.py`); the executor sends `human_control: true`, `pdp_path: false` and `required_count` = the number of approvers. - read: `GET /v1/approvals/{id}`. - consume: `POST /v1/approvals/{id}/consume` with `request_digest` and `decision_id`. - the executor accepts an entry only if: - `subject_id` is a listed approver; - `principal_type` is human; - the assurance is aal2; - the subject is not the executor itself. - the deployed approval-engine records assurance but doesn't enforce it, so the executor checks aal2 itself. - **Proposal:** `approval_entry` names the failed field: subject, principal type or assurance. That alone would have replaced diagnostic pod 4. - **Already answered by the owner:** approval-engine replied on 2026-10-31 (message `756016f9`, `docs/decision-services-alignment-answer.md`, `e9e9f93`). `required_assurance` is in source as schema v7: entries below the floor are refused with 403, the claim with `assurance_unsatisfied`, and consume with 409. It waits for a v7 image release and a founder-run rollout with a pre-v7 backup. The executor keeps its own aal2 check until then, and sends `required_assurance` once v7 is live. This is a dependency, not an open decision. ### E4: informed-decision intake - **Contract:** - `POST /v1/memos` on `informed-decision-workload.informed-decision.svc:8081`; - the bearer is the projected ServiceAccount token, audience `informed-decision-intake`, checked by TokenReview; - the registry row is `system:serviceaccount:sso:nk-bind-executor` → submitter `nk-bind-executor`, memo class `bounded-execution`, review group `net-kingdom-admins`, `present_existing.approval_actor` `service:nk-bind-executor`, daily limit 10; - the receipt must echo `memo_id`, `approval_id` and `approval_binding_digest`; - `cli.py --request` exits 7 when the memo is not accepted. Resubmitting is idempotent. - **Test gap:** `bounded-executor/tests/test_approval_request.py:216` hard-codes `/home/worsch/informed-decision` (T07: no hard-coded paths). ### E5: approver identity, defined four times | Where | Value on 2026-11-05 | Owner | | --- | --- | --- | | net-kingdom `admissions/BEA-2026-0003.yaml` `spec.approval.approvers` | `[admin]` (a name) | net-kingdom (`prepare_admission.py`) | | rapp `examples/config.pilot.json` `approver_subjects`, `approver_issuer` | `admin` → `uid=admin,ou=people,dc=netkingdom,dc=local`, `https://kc.coulomb.social` | rapp-bounded-executor | | informed-decision `deploy/lifecycle/review-groups.json` | `net-kingdom-admins` → `uid=admin,…` (interim, decision `c9bedd9c`; it was `platform-root` until `77161fe`) | informed-decision | | key-cape LLDAP adapter | `sub` = the LDAP DN | key-cape | Nothing checks that these four agree, and a mismatch shows only as a refused approval after a window has opened. **Until NK-WP-0057 decides who routes:** the T03 readiness check compares them. The admission's approver names resolve through the rapp config and must equal the routing recipient for the memo's review group. ### E6: audit-core - **Contract:** - `POST /v1/events` with `Idempotency-Key` equal to the event's `id`, and all eight envelope fields; - answers: 202 accepted, 200 duplicate, 400 dead-lettered with a named reason, and 409 for a reused run id with a different body (a defect, never retried); - the bearer is the file `/var/run/secrets/bounded-executor/audit-core`, from `sso/bounded-executor-tokens` key `audit-core` (custody in part 2); - the source is checked against the sender registry; - receipt schema v0.1 and its EAR rules (`tools/execution-attribution-receipt`). - **Proposal:** fold the T15 exercise into the T02 suite, and run `make execution-attribution-receipt-test` in CI. ### E7: LLDAP - **Contract:** - `http://lldap.sso.svc.cluster.local:17170`; - sign-in checks over its HTTP API; - the password change runs the image's `/app/lldap_set_password` with `--base-url`, `--token`, `--username` (OPAQUE). - **Gap:** nothing checks that the tool in the executor image is compatible with the running LLDAP server. - **Owner decision:** `docs/responsibility-map.md` names no owner for the LLDAP, Authelia or KeyCape runtimes in `sso`. SCOPE says the `sso-mfa/k8s/` material is "migration inputs, not blanket authority". ### E8: Kubernetes API - **Contract:** - force-sync: merge-patch `metadata.annotations.force-sync` on `/apis/external-secrets.io/v1/namespaces/sso/externalsecrets/authelia-secrets`, then poll its status; - restart: merge-patch `spec.template.metadata.annotations["kubectl.kubernetes.io/restartedAt"]` on Deployment `authelia`, then wait for the rollout; - Role: `get/watch/patch` on that Deployment and `get/patch` on that ExternalSecret. No Secret access. - **Duplicate:** the Role is written in net-kingdom and copied by hand into rapp (`make ceiling-sync`: "copy, never edit"). The compare test skips without the sibling checkout, and rapp has no CI. - **Proposal:** keep net-kingdom as the source, and make the compare test a T02 contract test that runs in CI. ### E9: network path - **Contract:** - the executor pod is labelled `app.kubernetes.io/name=bounded-executor` in namespace `sso`; - egress per the rapp policy; - each target admits it on its own port: approval-engine and audit-core on 8080, informed-decision on 8081, KeyCape and LLDAP through `netpol-sso.yaml`, and OpenBao on 8200; - the executor waits up to 30 s for all five endpoints. A setup failure names the unreachable one (since `c2a0f2f`). - **Gap:** the ingress side lives in five repos, and only the sso part is compared. The live approval-engine policy differed from its file. - **Proposal:** a T02 test that renders every owner's ingress policy for this label, plus a live comparison in the T03 readiness check. ### R1: image and pins - **Chain:** 1. a net-kingdom commit; 2. the Forgejo image workflow builds a digest; 3. the digest is pinned by hand in rapp's `Makefile IMAGE_DIGEST`, with the source commit only in the rapp commit message; 4. separately, `prepare_admission.py --commit` pins the admission to source file sha256s (`plan.source.files`). - **Gap:** no record links the digest to the commit to the admission pins, so "the admission's pins match the pinned image" cannot be checked. - **Proposal:** - the image workflow writes a value-free pin record (commit, file sha256s, digest); - rapp pins that record; - the renderer refuses when the admission's file hashes are not the image's (T03). ### R2: config, Job and exit codes - **Source:** the config keys and exit codes are defined in `cli.py`: 0 completed, 3 reverted, 4 refused, 5 unknown, 6 not in custody, and 7 for a request whose memo was not accepted. - **Copies:** - rapp `docs/runtime-contract.md` (prose); - `config.pilot.json` (values); - `render_job.py` allow-lists (token paths, role names); - `pilot-run.sh` sets `trust_satisfied` by editing a temporary copy of the config. - **Proposal:** - a versioned config schema file in net-kingdom, which the renderer validates against; - the exit codes stated once, in that file; - `trust_satisfied` becomes a run input recorded in the state file (T04), not a config edit. ## Summary for part 1 - **keep:** E2, E3, E4, E6, E7, E9. Each needs its T02 contract test. The T15 and T19 exercises become CI tests. - **merge:** - E1: lane names and policy text, from the CCRs; - E5: the approver, compared until it has one source; - E8: the RBAC copy; - R1: the pin record; - R2: the config schema. - **delete:** nothing yet. The candidates are the hand copies named under merge, once they are derived or checked. - **owner decision** (to NK-WP-0057-T01 or the named owner): - E5: who owns the approver and routing; - E2: KeyCape client registration with no KeyCape rapp; - E7: the LLDAP, Authelia and KeyCape runtime owner in `sso`; - E3 is not open: approval-engine enforces `required_assurance` from v7, which waits for its release. - **Transparency findings for T03:** - `executor-lanes.py` lane errors without a cause; - `approval_entry` without the failed field. - **Hard-coded path for T07:** `test_approval_request.py:216`. ## Part 2: the approval, case and custody chain NK-WP-0058-T09, as of 2026-11-05. It uses the same revisions as part 1, plus access-engine `535bab7`. | ID | Interface | Owner and source of truth | Consumer | Tested today | 2026-11-04/05 failure | Mark | | --- | --- | --- | --- | --- | --- | --- | | C1 | CCR lifecycle ↔ net-kingdom lane tooling | railiance-platform: `credential-change-requests/CCR-2026-0058`, `-0059`, `scripts/credential-change.py` | `tools/service-binds/executor-lanes.py`, which imports three platform helper modules; `pilot-run.sh` | Local: `tests/test_executor_lanes.py` copies `railiance-platform/scripts` | `window-close` failed as bare `LaneError` (identity phase) | **merge**; **owner decision** on a supported interface | | C2 | Case-type bytes ↔ broker admission | net-kingdom `security-case-types/*/type.yaml` (canon). Pin: railiance-platform `scripts/keycape_client_secret_generate.py ACKNOWLEDGED_TYPES` (sha256) | railiance-platform credential broker | none in net-kingdom. The README errata warns against edits | none (both hashes match today) | **keep**; add a net-kingdom guard test | | C3 | Case status ↔ broker gate | net-kingdom `security-cases/*.yaml` (`status`, `acceptance`); canon `canon/standards/security-case-types_v0.1.md` | broker (`case_not_materialized`), founder runbooks | Local: the broker case check on a synthetic copy | none. The status is committed by hand from a founder message | **keep** (who materializes: tracked in MASON-WP-0010-T03) | | C4 | KeyCape client secret delivery | railiance-platform: CCR-2026-0060 and -0061, `manifests/nk-bind-executor-eso.yaml`, template `keycape-issuer-eso.yaml` | net-kingdom `sso-mfa/k8s/keycape/deployment.yaml` env `KEYCAPE_NK_BIND_EXECUTOR_APPROVAL_CLIENT_SECRET`; `keycape-client.py env_secret_name()`; executor E1 lane | railiance-platform `tests/test_nk_bind_executor_custody.py`, `test_keycape_client_secret_generate.py`; net-kingdom `sso-mfa/k8s/keycape/test_keycape_client.py`; no cross test | none (custody verify passed 2026-11-04) | **merge** | | C5 | audit-core sender bearer custody | railiance-platform: CCR-2026-0063 (path `platform/workloads/net-kingdom/bounded-executor/audit-sender`, field `AUDIT_TOKEN`), ESO to `sso/bounded-executor-tokens` key `audit-core`. Seed: audit-core `scripts/seed-nk-bounded-executor-sender.py` (single seeder) | rapp `templates/job.yaml` mount; executor `audit_core.token_file`; railiance-platform `nk_bind_executor_custody.py verify` | audit-core `tests/test_seed_nk_bounded_executor_sender.py`; railiance-platform custody tests; no cross test | While being prepared: competing seeders in two repos and two Secret names. Settled in RPF-WP-0069 and AUDIT-WP-0018-T04; one stale copy remains | **merge**; **delete** the stale design text | | C6 | Executor's submitter profile | **Two owners, one fact.** informed-decision `deploy/lifecycle/intake-submitters.json` and access-engine `examples/informed-decision-present-existing-review/policy.md present_existing_profiles` (v14) | informed-decision intake; access-engine decisions; executor `executor_identity` (rapp config) | none across the three | none | **merge**; **owner decision** on which is the source | | C7 | Memo routing ↔ who can see it | informed-decision `deploy/lifecycle/review-groups.json` (the recipient). access-engine policy v14 `recipient_ok`: `list` only when `review_recipient == subject.id` | the founder in informed-decision; executor `_check_approval` (E5) | none | The memo was visible only to `platform-root`, so one window was wasted | **owner decision** (NK-WP-0057; this is E5's routing half) | ### C1: CCR lifecycle and the lane tooling - **Contract:** - `executor-lanes.py` acts only under both CCRs, approved by `platform-owner` and `net-kingdom-owner` with `bound_claims_confirmed: true`; - each CCR is passed as the sha256 of its file (`--write-ccr-sha256`, `--journey-ccr-sha256`), computed by `sha256sum` in `pilot-run.sh`; - the identity phase requires an attended `platform-admin` token from `auth/netkingdom/`, never root. - **Gap:** `executor-lanes.py` puts `railiance-platform/scripts` on `sys.path` and imports helpers from three scripts, `state_hub_preflight_lane`, `repair_eso_kubernetes_auth` and `telemetry_smtp_delivery`. None of them was written as an interface for another repo, so a refactor there breaks the window commands without a failing test in either CI. - **Owner decision (railiance-platform):** offer a supported entry point for role create, delete and read, under a CCR digest, or accept these helpers as a versioned interface. Either way T02 adds a test that fails in the consumer. ### C2: case-type hash pins - **Contract:** the broker admits `oidc-browser-relying-party@1.1` (`f705c64e…`) and `oidc-service-client@0.2` (`cd238693…`) only when the net-kingdom `type.yaml` bytes hash to these values. A wording fix in net-kingdom silently breaks the mint. - **Proposal:** a net-kingdom CI test that compares each acknowledged `type.yaml` with a pin list kept in net-kingdom. That list is checked against the broker's `ACKNOWLEDGED_TYPES` once owner source can reach CI (T02). ### C3: case status and the broker - **Contract:** the broker refuses unless the case is `status: materialized` with `acceptance: {at, record, by: [{role: owner}, {role: founder}]}`. - **Consistent with the canon:** materialization is the OpenBao structure only, never values (`security-case-types_v0.1.md`, line 61). Committing `materialized` after go-live steps 0–3 and before the mint is therefore correct; the mint and verification lead to `live`. An earlier draft of this row said otherwise. - **Gap:** - the status is committed by hand by claude-code after the founder reports steps 0–3. No receipt links them; the T04 state file could record that link. - the canon names ops-mason as the materializer, but railiance-platform's go-live steps did it here by hand. That isn't a new question: ops-mason's case build path (`ops-mason build --case`, MASON-WP-0010-T03) exists and waits on railiance-platform's structural evidence. ops-mason reviewed and test-rendered this case on 2026-10-31 (message `91e2205f`). Tracked there; no new routing. ### C4: KeyCape client secret delivery - **Chain:** 1. the broker mints into `platform/workloads/keycape/nk-bind-executor-approval` (`CLIENT_SECRET`); 2. ESO (CCR-2026-0060) delivers it to `sso/keycape-nk-bind-executor-approval-client` key `client-secret`; 3. KeyCape reads it from env `KEYCAPE_NK_BIND_EXECUTOR_APPROVAL_CLIENT_SECRET`; 4. the executor reads the same KV through CCR-2026-0061. - **Duplicates:** the Secret name follows a naming convention written twice: net-kingdom `keycape-client.py env_secret_name()` and railiance-platform's ESO template. The KV path appears in the CCRs, the rapp config and the custody tool. - **Proposal:** one test computes the convention from both sides for every registered client. The executor's KV path is checked against CCR-2026-0061 (T02). ### C5: audit sender custody - **Contract:** - audit-core's seeder mints the bearer CAS 0 into the CCR-2026-0063 path, merges the registry row `nk-bounded-executor` with compare-and-set, force-syncs and probes; - railiance-platform adds only the coding-agent fence and `verify`; - ESO delivers to `sso/bounded-executor-tokens` key `audit-core`; - the rapp Job mounts it at `/var/run/secrets/bounded-executor/audit-core`. - **Settled 2026-11-04:** one seeder (RPF-WP-0069 "One seeder"; AUDIT-WP-0018-T04, audit-core `3157454`). The Secret moved from `sso/nk-bind-executor-audit` key `audit-token` to `sso/bounded-executor-tokens` key `audit-core`. - **Still duplicated or stale:** - railiance-platform `docs/credential-lane-designs/bounded-executor.md:85-90` still names `sso/nk-bind-executor-audit` key `audit-token`. That is a removal or correction request to railiance-platform. - audit-core's seeder hard-codes `/home/worsch/railiance-platform/...CCR-2026-0063...yaml` and the cluster UID (`seed-nk-bounded-executor-sender.py:58-69`), for T07. - the names appear in four repos (CCR, seeder, ESO manifest, rapp Job) and no test compares them. ### C6: the executor's submitter profile The executor is described as one requester profile in three places: | Where | Fields | | --- | --- | | informed-decision `intake-submitters.json` | `system:serviceaccount:sso:nk-bind-executor` → `nk-bind-executor`, class `bounded-execution`, group `net-kingdom-admins`, `present_existing.approval_actor` `service:nk-bind-executor`, limit 10 | | access-engine policy v14 `present_existing_profiles` | `service:nk-bind-executor` → `intake_submitter` `nk-bind-executor`, `memo_class` `bounded-execution`, `approval_action` `bounded-execution` | | rapp `config.pilot.json` | `executor_identity` `service:nk-bind-executor`, `runtime_principal` `system:serviceaccount:sso:nk-bind-executor` | The policy says its profiles are "owner-authored here, not generated". That was a deliberate access-engine choice (GH-DEC-2026-026), so it isn't changed locally. **Owner decision:** whether informed-decision's registry or access-engine's policy is the source, with the other checked against it. Until then, T02 compares the three. ### C7: routing and visibility - informed-decision maps the review group to exactly one recipient (`intake_service.py:189`). - access-engine's `list` allows a viewer only if they are that recipient (`policy.md recipient_ok`). - The two rules together mean the routing table decides who can approve, without the admission seeing it. On 2026-11-04 the admission named `admin`, routing named `platform-root`, and the policy then hid the memo from `admin`. - This is NK-WP-0057's core question. The T03 readiness check compares the admission's approver with the routing recipient before a window opens. ## Summary for part 2 - **keep:** - C2, with a net-kingdom guard test for the acknowledged `type.yaml` bytes; - C3. - **merge:** - C1: lane tooling on platform internals; - C4: the KeyCape secret naming convention and the KV path; - C5: audit custody names in four repos; - C6: the submitter profile in three places. - **delete:** the stale Secret name in railiance-platform `docs/credential-lane-designs/bounded-executor.md` (an owner request). - **owner decision:** - C1: a supported platform interface for window roles (railiance-platform); - C6: the source of the submitter profile (informed-decision and access-engine); - C7: routing and visibility (NK-WP-0057, together with E5). - **For T07:** the hard-coded path and cluster UID in audit-core's seeder. ## Part 3: operations and the founder NK-WP-0058-T10, as of 2026-11-05. It uses the same revisions as parts 1 and 2. Sources for the founder steps: `sso-mfa/k8s/bounded-executor/T06-PILOT-RUNBOOK.md`, railiance-platform `docs/credential-lane-designs/nk-bind-executor-go-live.md`, audit-core `docs/bounded-executor-receipts.md`, and the evidence `docs/evidence/2026-11-04-nk56-t06-go-live/`, `2026-11-04-nk56-t06-pilot/` and `2026-11-05-nk56-t06-pilot/`. | ID | Interface | Owner and source of truth | Consumer | Tested today | 2026-11-04/05 failure | Mark | | --- | --- | --- | --- | --- | --- | --- | | O1 | Argo CD ↔ manual applies | railiance-platform `argocd/railiance01/applications/` (pins, no automated sync). Every other object is applied by hand with `kubectl` (table below) | founder runbooks; agents writing apply commands | none: no drift check between Argo pin, repo file and live | Argo pin for informed-decision older than the live release, so a sync would have reverted it; Argo-managed admission guard refused client-side ESO apply; a live approval-engine policy missing from its file | **owner decision** (railiance-platform); **merge** (drift check in T03, convention in T07) | | O2 | warden lanes ↔ child scripts | ops-warden: `warden access --exec` (silent, one command, fails on output, revokes on every path). Children: railiance-platform and audit-core silent scripts | the founder; seven lane invocations in the go-live runbook | none across repos | Seed r1 refused `token_env_must_be_unset`: warden strips `BAO_TOKEN` and `VAULT_TOKEN`, not `OPENBAO_TOKEN` | **keep**; **owner request** to ops-warden | | O3 | Platform-admin session: lane vs shell | Two patterns: ops-warden lanes (railiance-platform, audit-core) and an interactive `bao login -method=oidc -path=netkingdom role=platform-admin` in the shell (net-kingdom `executor-lanes.py`, `pilot-run.sh`, T06 runbook 6c) | `executor-lanes.py attended_platform_admin()` | none | 2026-11-05: `window-close` failed `phase: identity, LaneError` in a shell without a login | **merge** into one pattern | | O4 | Cluster access routes | Three in one runbook: `kubectl --kubeconfig ~/.kube/config-railiance01` (admin); `keycape-client.py` via `portal-client-rollout.py` hard-coding `/home/worsch/.kube/config-hosteurope` with `--server https://127.0.0.1:16444` (cluster UID pinned); the agent's default context (NetworkPolicy reads only) | founder, agents | none | none directly; costs founder setup | **merge** (T07: no hard-coded kubeconfigs) | | F1 | Founder ↔ runbook steps | net-kingdom T06 runbook, railiance-platform go-live, audit-core founder runbook | the founder | live only | about 50 copy-paste rounds; values carried by hand between steps (table below) | **merge** into T04's commands | | F2 | Founder identity in the browser | informed-decision UI at aal2; the identity model (`admin` for production, `platform-root` kept out) | the founder; executor `_check_approval` | none | The first approval came as `uid=platform-root`, the identity signed in at that moment, and was refused | **keep** the refusal; add identity hygiene to T04 | ### O1: which controller manages each object the path writes | Object | Written by | Managed by | Note | | --- | --- | --- | --- | | informed-decision `deploy/lifecycle` (incl. `review-groups.json`, `intake-submitters.json` ConfigMaps) | founder release (2026-11-01), routing change (2026-11-05) | **Argo CD**, pinned revision, manual sync | The pin lagged live until railiance-platform `31cf74d`; a sync before that would have reverted the present-existing release | | `reject-secret-last-applied` admission policy | — | **Argo CD** (`secret-annotation-guard`) | Refuses Secrets carrying `last-applied`; ESO propagates the annotation from a client-side apply | | OpenBao ClusterSecretStores (shared) | — | **Argo CD** (`openbao-secretstore`) | | | `sso` ESO objects (`manifests/nk-bind-executor-eso.yaml`) | founder, `kubectl apply --server-side` | manual | First apply was client-side and failed (runbook fixed, railiance-platform `9a2495d`) | | approval-engine and audit-core ingress policies | founder, one object each | manual | Live approval-engine also has `informed-decision-review-ingress`, not in its file | | KeyCape Deployment env and client registry | founder (rollout pause, patch, `keycape-client.py apply`) | manual | net-kingdom `sso-mfa/k8s/keycape/deployment.yaml` updated afterwards to match live | | rapp standing objects (ServiceAccount, Role, RoleBinding, NetworkPolicy) | founder, `make deploy` | manual | | | Per-run Job and ConfigMap | founder, `make request`, `make run` | manual (`kubectl create`) | | | OpenBao write role | founder, `executor-lanes.py window-open` / `window-close` | manual | Left open overnight; T11 confirmed no use | **Owner decision (railiance-platform, with railiance-cluster):** which objects on this path Argo should own. The alternative is for each runbook to state the managing controller before it writes. Until that's decided, T07's convention ("check the managing controller before writing apply commands") and T03's readiness check (compare each Argo pin with live) cover the gap. ### O2 and O3: platform-admin sessions - **The ops-warden lane contract:** - one reviewed command per invocation; - the child must be silent and write its own receipt; - output fails the lane, and the token is revoked on every path; - the caller passes `BAO_ADDR`, `VAULT_ADDR` and, where needed, `KUBECONFIG`. - **Owner request to ops-warden:** strip `OPENBAO_TOKEN` too, or every token variable by pattern (T07). - **The second pattern:** net-kingdom's window commands assume a token in the shell instead. They run several commands, and `pilot-run.sh` keeps that session open across a whole run. - **Proposal:** run `executor-lanes.py window-open`, `window-close` and the T03 readiness check as warden lanes. They already write their own receipts and print one line. `pilot-run.sh` then opens one lane per command and needs no shell login. ### F1: the founder's steps, 2026-11-04/05 | Step | Authority it needs | Value carried by hand | Value-free result | Safe to repeat | | --- | --- | --- | --- | --- | | 1 owner ingress, two objects | admin kubeconfig | none | `kubectl` output; `make netcheck` columns | yes | | 2 accept the case | the founder in State Hub | decision id | the decision | yes | | 3.0–3.7 custody go-live | platform-admin through 7 warden lanes; admin kubeconfig for ESO | none, but the case is committed `materialized` by claude-code between 3.3 and 3.5, after a pull | one receipt per step | mostly; the mint is create-only | | 4 KeyCape bind and register | admin kubeconfig; the `keycape-client.py` route (O4); docker, to extract `check-config` | `before.uid` and `before.resource_version` from `inspect` into `apply` | registry count, check-config, rollout status | no: one restart, in a quiet minute | | 5 admission | none (claude-code prepares) | window start | the file | yes, as a new file | | 6 setup | none | `trust_satisfied` edited into a temporary config by an inline Python line | none | yes | | 6a request | admin kubeconfig | **the approval id**, into 6d | `outcome: presented` | yes (idempotent memo) | | 6b approve | **`admin` at aal2 in the browser** | none | none in the shell | — | | 6c window open | platform-admin shell login (O3); CCR sha256s | the two CCR sha256s, receipt numbers | `window_opened` | refuses if already open | | 6d run | admin kubeconfig | approval id; Job name | the result line in the log | no: one consume | | 6e window close | platform-admin shell login | receipt number | `window_closed` | yes | | 6f evidence | the agent reads audit-core by correlation id | — | — | yes | There are about 45 commands across three runbooks, with five kinds of values carried by hand: UID and resource version, approval id, CCR digests, receipt numbers and `trust_satisfied`. That fits the reported ~50 rounds. T04's state file removes the carried values: `ready`, `request`, `run` and `status` read and write it, and the receipt numbers come from the tool. ### F2: the browser identity The approval was refused correctly, because the admission names `admin` and the founder's identity model keeps `platform-root` out of production. But the founder learned which identity was signed in only from the refusal, after the window had opened. T04 records the hygiene: a browser profile per identity, and a check of the signed-in subject and OTP freshness before approving. The T03 check confirms that the memo's recipient is the admission's approver. ### Statuses the agent had to ask the founder for (input to T05) The agent's default context can read NetworkPolicies in `sso`, `approval-engine`, `audit-core`, `informed-decision` and `openbao`. It can't read Jobs, pods, pod logs, Deployments or ConfigMaps (`kubectl auth can-i`, 2026-11-05). ExternalSecret read is not checked: the repository's secret-read guard blocks the question. | Status | Where | Readable by the agent today | Proposal for T05 | | --- | --- | --- | --- | | Run and request Job status and result line | `sso` Jobs, pod logs (value-free by contract) | no | grant `get` on Jobs and pods, `get` on pod logs of `app.kubernetes.io/name=bounded-executor` pods | | Diagnostic pod output | `sso` pods, logs | no | covered by the same grant; T03's `--preflight-only` should make diagnostic pods unnecessary | | ExternalSecret `Ready` (KeyCape client, `bounded-executor-tokens`) | `sso` ExternalSecrets | unknown | `get` on ExternalSecrets in `sso` (status only, no Secret access) | | KeyCape and Authelia rollout status | `sso` Deployments | no | `get` on Deployments in `sso` | | Live ingress policies | NetworkPolicies | **yes** | none needed | | informed-decision registry ConfigMaps vs Argo pin | `informed-decision` ConfigMaps, Argo Application | no | `get` on those ConfigMaps (non-secret), or read the Argo Application status | | Approval entries | approval-engine `GET /v1/approvals/{id}` | no (needed diagnostic pod 4) | the executor's `approval_entry` names the failed field (T03); otherwise an operator read scope | | OpenBao write role present | OpenBao `auth/kubernetes/role/...` | no | **founder-only**: OpenBao read under platform-admin; T03 and T06 report it value-free | | OpenBao audit log | `openbao-0` file | no | **founder-only** (exec); audit forwarding (NK-WP-0055-T11) is the durable path | | audit-core receipt in custody | audit-core operator read | through the run result's `receipt_not_in_custody` flag | keep: the result line already carries it | ## Summary for part 3 - **keep:** - O2: the warden lane contract; - F2: the executor's refusal of the wrong identity. - **merge:** - O1: drift check and controller convention; - O3: one platform-admin pattern, warden lanes for the window commands; - O4: one cluster access route, no hard-coded kubeconfigs; - F1: carried values move to T04's state file. - **owner decision:** O1, which objects Argo owns on this path (railiance-platform with railiance-cluster). - **owner request:** O2, ops-warden strips every token variable. - **For T05:** the status table above. The minimal grant is read-only `get` on Jobs, pods, executor pod logs, Deployments and ExternalSecrets in `sso`, plus the informed-decision registry ConfigMaps. It excludes Secrets, exec and kubeconfigs. The founder-only rows stay founder-only. - **For T07:** `portal-client-rollout.py` and three other tools hard-code kubeconfigs (`config-hosteurope`, `/etc/rancher/k3s/k3s.yaml`). ## Routing of the owner decisions (2026-11-05) | Row | Question | Routed to | Reference | | --- | --- | --- | --- | | E5 + C7 | Approver and routing; the routing table decides who can approve | NK-WP-0057-T01 | workplan inputs | | E2 | Who registers KeyCape clients, with no KeyCape rapp | NK-WP-0057-T01 | workplan inputs | | E7 | Runtime owner of LLDAP, Authelia and KeyCape in `sso` | NK-WP-0057-T01 | workplan inputs | | C1 | A supported platform interface for window roles | railiance-platform | message `f6198285` | | O1 | Which objects on the path Argo CD owns | railiance-platform (with railiance-cluster) | message `f6198285` | | C5 | Correct the stale Secret name in `bounded-executor.md:85-90` | railiance-platform | message `f6198285` | | O2 | Strip every token variable in `warden access --exec`; name the variable in the refusal | ops-warden | message `4acb653c` | | C6 | Which copy of the executor's present-existing profile is the source | informed-decision and access-engine | messages `fa7e25d3`, `df5aa613` | | E3 | `required_assurance` | not routed: answered 2026-10-31 (`756016f9`); waits for approval-engine v7 release | — | | C3 | Who materializes | not routed: tracked in ops-mason MASON-WP-0010-T03 (`91e2205f`) | — |