--- id: NK-WP-0056 type: workplan title: "Bounded executor path: founder admits, executors run" domain: infotech repo: net-kingdom status: finished flavor: design owner: claude topic_slug: netkingdom created: "2026-10-30" updated: "2026-11-05" related: - NK-WP-0054 - NK-WP-0040 - NK-WP-0031 - NK-WP-0055 state_hub_workstream_id: "5bc74ef8-136e-5157-9a7f-da765dbfc977" --- # NK-WP-0056 — Bounded executor path Intake from the founder's direction on 2026-10-30, before the live LLDAP bind split: top-level decisions and guardrails stay with the founder; execution below that is delegated to automation with limited expense budgets and risk control, and the security architecture is the means. Founder-attended runbooks are a transitional state. SI-15 (`canon/standards/service-identity-privilege_v0.1.md`) already names the golden path *prepare → admit → execute → verify → retain* and allows "a bounded executor under the admission" at step 3. Today preparation and verification are strong, admission is prose (a hub decision) and execution falls to the founder, because no executor exists that has its own identity, its own secret delivery and an authority ceiling the founder can admit and bound. SI-14 adds that an interactive agent session holds everything its tools reach, so it cannot be that executor until a session contract exists (glas-harness GLAS-WP-0018, proposed). ## Why the bind split needed the founder (2026-10-30 run, step by step) | Runbook step | Needed the founder because | Class | Delegable once | | --- | --- | --- | --- | | Setup: decrypt custody copy | custody is a personal SOPS/age copy | delegable | service credentials live in OpenBao (T04) | | 1 prepare custody (generate passwords) | writes the personal custody copy | delegable | executor generates into OpenBao (T04) | | 2 plan, 4 verify, 10 verify | sign in as the human `admin` | delegable | read-only executor directory identity (T03) | | 3 create `svc-*` accounts | needs `lldap_admin` (LLDAP has no narrower grant that can create users) | root-class | admitted per run; executor runs it (T03) | | 5-8 switch one consumer | cluster-admin kubeconfig; secret values; founder sign-in check | delegable | scoped service account (T03), OpenBao/ESO (T04), synthetic MFA journey (T05) | | 9 change LLDAP's configured super-admin | changes who administers the directory | root-class | admitted per run; executor runs it | | 10 change `admin`'s own password; seal `ld-admin` in the password safe | the founder's own credential and crisis custody | human-intrinsic | never: stays with the founder | | Afterwards: restore drill on the next generation | `admin` operates drills | delegable | executor identity with the drill's read scope | Root-class means the founder still admits each run, but no longer types it. The run also showed why personal custody does not scale: the custody copy had drifted from live (`AUTHELIA_KEYCAPE_CLIENT_SECRET`) and step 6 had to be done by a one-off path. ## Admission contract ```task id: NK-WP-0056-T01 status: done priority: high assignee: claude-code state_hub_task_id: "99df0ee2-2c17-55f2-9f3e-4277da52e01c" ``` Draft a NetKingdom standard for a **bounded-execution admission**: the founder approves, at `aal2`, an exact plan by digest with a validity window, a scope ceiling (`MODE @ SCOPE` grants, SI terms), a blast-radius rule (for example one consumer at a time), a stop-and-revert policy tied to named machine checks, a risk class, and an expense budget (wall time, attempts, compute/egress cost). The executor consumes the admission once. Map it to SI-15, the Playbook Capability Contract's trust-state requirements and the execution-attribution receipt (NK-WP-0040). Include a validator and synthetic fixtures. **Done when:** the standard is proposed with a schema, a validator, and the bind-split steps expressed as admissions in its examples. **Done 2026-10-31.** Proposed `canon/standards/bounded-execution-admission_v0.1.md` with `canon/schemas/bounded-execution-admission_v0.1.schema.json`, `tools/bounded-execution-admission/validate.py` (rules BEA-1..14 on the request, BEA-C1..C7 on a carrier record; 22 tests) and four examples in `examples/bounded-execution-admission/`: the bind split's read-only verify (low), create `svc-*` accounts and switch LLDAP's super-admin (root-class, high), and one-consumer bind rotation (delegable, medium, the T06 shape). Step 10 is refused as human-intrinsic. Design choices: the request is immutable and bound by digest; the founder's approval lives in a carrier (Approval Engine preferred, founder SSH signature transitional), so no second approval system; `trust_requires` uses the Playbook Capability Contract's trust states; `settle_seconds` carries the T08 privacyIDEA-cache finding; recurring admissions are excluded from v0.1. The Approval Engine mapping is a proposal for T02. ## Owner agreement on the components ```task id: NK-WP-0056-T02 status: done priority: high assignee: claude-code needs_human: true decision_id: 6d1adce9-40e2-472a-92bb-0ae942218ae8 intervention_note: "Founder chooses the executor runtime owner; owner answers are agreements, not agent decisions." depends_on: [NK-WP-0056-T01] state_hub_task_id: "7263417b-b227-54f7-9edc-3e7f97cf132f" ``` Ask the owners which part each carries: approval-engine (the admission as its consumable approval object, plan digest and budget as bound fields, single consume), access-engine (reads the admission as an input claim), secrets-engine and railiance-platform (executor credential delivery through OpenBao), glas-harness (GLAS-WP-0018 session contract, so a harness-launched agent can be an executor), audit-core (receipt holder, as in NK-WP-0040). The founder decides who builds the executor runtime itself. **Done when:** each component has a named owner and an answer, and the founder has chosen the executor runtime owner. **2026-10-31, founder decision (hub `6d1adce9`): net-kingdom writes the executor runtime** (source and tests next to the admission validator, bind lanes and synthetic journey); a new `rapp-*` package (working name `rapp-bounded-executor`, ADR-0015) builds and deploys it in `sso`; glas-harness is the later path for agent executors. Owner requests sent the same day through the hub to approval-engine, access-engine, secrets-engine, railiance-platform, audit-core, glas-harness and railiance-apps ([requests and answer table](../docs/proposals/2026-10-31-nk56-t02-owner-requests.md)). Waiting on their answers. **Done 2026-10-31.** All seven owners answered (worker agents in each repository, founder instruction); answers and the collected founder decisions are in the [answer table](../docs/proposals/2026-10-31-nk56-t02-owner-requests.md). Key results: approval-engine already carries binding, single consume and revoke (aal2 recorded, not yet enforced); access-engine accepts the pilot without the PDP gate if the receipt says `not-evaluated`; railiance-platform added request type `workload-kv-write-next-version` and drafted CCR-2026-0058; secrets-engine prefers self-rotation, which NetKingdom proved on the pinned LLDAP; audit-core holds receipts plus a `run.started` event; railiance-apps: repo-manager creates the rapp, the image is built in net-kingdom, one Job per admission; glas-harness: agent executors after GLAS-WP-0018-T04. **2026-10-31, founder decisions after the answers** (hub c29c6eda, 3d0c8f86, 19afd15e): - The executor's OpenBao write role is window-scoped. CCR-2026-0058 was amended, approved and pushed. - The bind rotates its own password, so `svc-executor-pw` is dropped and RPF-WP-0069-T06 is cancelled. - The pilot runs without the policy gate (`not-evaluated`); later admissions need the gate or a recorded exception. - Packaging is `rapp-bounded-executor`, one Job per admission in `sso`, with the image built in net-kingdom. - Receipts are a started event plus one receipt per run. - These owner workplans are approved: APPROVAL-WP-0007, FLEX-WP-0040, SECRETS-WP-0017, RPF-WP-0069, AUDIT-WP-0018 and GLAS-WP-0018. glas-harness owns agent executors later. Runtime work is split into T11 (core runner) and T12 (pilot operations and packaging). ## Executor identity and authority ceiling ```task id: NK-WP-0056-T03 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T02] state_hub_task_id: "592231dd-1981-57a6-a0e8-5d6fc474c6e7" ``` Declare the executor as a `WORKLOAD` (or harness-launched `AGENT`) principal in `declarations/` with grants no wider than the pilot: a Kubernetes service account whose Role names the `sso` Secrets, ConfigMaps and Deployments it may patch or restart; a read-only directory bind for plan and verify; `lldap_admin` only for root-class steps under an admission. Extend `tools/service-identity` to check it. **Done when:** the declaration validates and the Role is prepared in source with tests, not applied. **Done 2026-10-31.** `tools/service-identity/declarations/bounded-executor.yaml` declares the executor's credentials (all `planned`, all sharing `executor_subject: service:nk-bind-executor`): the Kubernetes ServiceAccount, its OpenBao role, `svc-executor-pw` (`lldap_password_manager`: LLDAP cannot narrow to one account, the admission narrows it), the read-only bind `svc-executor-read`, and `svc-executor-admin` (`lldap_admin`, root-class only, per-run activation open). `tools/service-identity` gained convention `executor` with rule SI-15 (no person, not interactive, every grant `APPROVED`, no `SOVEREIGN`/`GOVERN`/`CONSTRUCT`) and `--admissions`, which checks that an admission's ceiling lies inside the executor's declared grants: the pilot (BEA-2026-0001) is covered; the root-class and read-only examples are correctly reported as needing grants the pilot does not hold. `sso-mfa/k8s/bounded-executor/rbac.yaml` (not applied): ServiceAccount without automount, a Role with `get/watch/patch` on the authelia Deployment and `get/patch` on the authelia ExternalSecret only (no read of credential objects: the pilot's delivery check became "ExternalSecret Ready and refreshed"), and its RoleBinding; `test_rbac.py` ties Role = declaration = pilot ceiling. 44 identity tests, 5 RBAC tests. ## Service credentials into OpenBao ```task id: NK-WP-0056-T04 status: done priority: high assignee: claude-code needs_human: true intervention_note: "Founder runs the custody reconciliation and approves moving service binds out of personal custody." depends_on: [NK-WP-0056-T02] state_hub_task_id: "fd439c0c-9b5e-562c-9bfb-48dd0fb2c570" ``` With railiance-platform, move the LLDAP service binds (`LLDAP_SVC_*_PASS`) and the Authelia secrets from the personal custody copy into OpenBao paths delivered by ESO, so that neither a person nor an agent handles the values in a rotation. First reconcile the stale custody `AUTHELIA_KEYCAPE_CLIENT_SECRET` with the live value (founder-run, value-free comparison as in the 2026-10-30 evidence). Root-class material (`ld-admin`, unseal shares) stays in founder custody. **Done when:** the four service binds are ESO-delivered from OpenBao, and the custody copy no longer needs to be decrypted for a bind rotation. **2026-10-30:** reconciliation tool prepared: `sso-mfa/bootstrap/reconcile-keycape-client-secret.py` (value-free; accepts the live KeyCape value only if it matches Authelia's live hash, writes custody through `rotation-env.py`, no cluster writes; `tests/test_reconcile_keycape_client_secret.py`). **Founder ran it 2026-10-30:** live KeyCape value matches Authelia's hash; custody did not match before, was updated and matches after (value-free output); custody re-sealed and shredded. The reconciliation step is done; the ESO move remains. **2026-10-30, pilot `svc-provisioner` prepared (source only, nothing applied):** railiance-platform CCR-2026-0052 (proposed) and policy `net-kingdom-svc-provisioner-bind-eso`; `sso-mfa/k8s/identity-provisioner/bind-eso.yaml` (ServiceAccount, SecretStore, ExternalSecret with `creationPolicy: Merge` into the existing Secret, `password` only); `tools/service-binds/bind-lane.py setup|verify` (approved sha256-bound CCR, attended platform-admin, custody must equal live, coding-agent fence, KV CAS=0, value-free receipts); `tests/test_service_bind_lane.py` (7). Next: owner approval of the CCR, then the founder-run setup, ESO apply and verify. Authelia, KeyCape and privacyIDEA follow one at a time. **2026-10-30, pilot live and verified:** CCR-2026-0052 approved; founder-run setup, ESO apply and verify passed; `svc-provisioner` now binds with the ESO-delivered value ([evidence](../docs/evidence/2026-10-30-nk56-svc-provisioner-bind/README.md)). Finding: ESO targets need `template.metadata: {}` or the admission guard refuses the Secret update. Remaining: `svc-authelia`, `svc-keycape`, `svc-privacyidea`. **2026-10-31, `svc-authelia` lane prepared (source only, nothing applied):** railiance-platform CCR-2026-0053 (proposed) and policy `net-kingdom-svc-authelia-bind-eso`; `sso-mfa/k8s/authelia/bind-eso.yaml` (Merge of `ldap_password` only into `authelia-secrets`, which also holds the session, storage and OIDC keys). `bind-lane.py` gains per-lane Secret keys: Authelia keeps the bind user in `authelia-config`, so setup refuses unless the live configuration binds `uid=svc-authelia`, and it decodes only the bind key. Authelia reads the file at start-up only, so the identical projection needs no restart; a later rotation does. 12 lane tests. Next: owner approval and binding confirmation, then the founder-run setup, ESO apply, verify and an Authelia restart with a login. **2026-10-31, `svc-authelia` live and verified:** CCR-2026-0053 approved and binding confirmed; founder-run setup, ESO apply (Ready first time), verify and Authelia restart passed, and an `admin` portal sign-in with OTP worked ([evidence](../docs/evidence/2026-10-31-nk56-svc-authelia-bind/README.md)). Two of four binds are ESO-delivered. Remaining: `svc-keycape` (bind inside `sso/keycape-config`), `svc-privacyidea` (resolver password stored in the privacyIDEA database, not a Secret key). **2026-10-31, `svc-keycape` design proposal (for founder decision):** KeyCape's server reads `lldap.bindPW` only from `config.yaml` inside `sso/keycape-config` (`internal/config/config.go`); unlike client secrets (`secretRef: env:...` via `secretKeyRef`) it has no environment indirection. ESO Merge cannot own one field inside a YAML document, and rendering all of `config.yaml` from OpenBao would move every KeyCape value at once. Proposed: 1. key-cape (owner change, new KEY-WP): `config.Load` takes `KEYCAPE_LLDAP_BIND_PW` (the name `lldap-export` already uses). Env set and `bindPW` empty: use env. Both set and equal: use it and log that `bindPW` is redundant. Both set and different: refuse to start. This allows a staged switch with a clean rollback. Image through the owner pipeline; `check-config` reports which source is used, without the value. 2. NetKingdom: CCR-2026-0054 and a `svc-keycape` lane; ESO creates a new dedicated Secret `keycape-lldap-bind` (`creationPolicy: Owner`, key `password`), so no Secret has two writers. Setup compares custody with the live `bindPW` in `keycape-config`. 3. Deployment: add the env from `secretKeyRef keycape-lldap-bind/password` with the new image; values are equal, so behaviour is unchanged. Verify, restart, admin login. 4. Remove `bindPW` from `keycape-config` (`directory-bind-rollout.py --operation clear-password`, guarded on uid/resourceVersion); `create-secrets.py` stops writing it. Rollback at any step: re-add the value or drop **2026-10-31, founder decision: environment override, implemented by claude-code in key-cape.** Done in source: key-cape `e2b5fa1` (KEY-WP-0046-T01; image `main-e2b5fa1` = `sha256:9e082dca…6f8d`, built by the Forgejo workflow; its acceptance job also fails on the previous commit `a1ab8ff`). NetKingdom: CCR-2026-0054 (proposed), `sso-mfa/k8s/keycape/bind-eso.yaml` (dedicated Secret `keycape-lldap-bind`, `creationPolicy: Owner`), `bind-lane.py svc-keycape` (setup reads `bindPW` from `keycape-config` and requires `bindDN` = `svc-keycape`; verify reads the dedicated Secret and requires it to equal `bindPW` while present), `directory-bind-rollout.py --operation clear-password` (only with the service bind and the delivered env; `switch` restores), and `deployment.yaml` pinned to the new image with the `KEYCAPE_LLDAP_BIND_PW` env. The new image also carries key-cape 67a0869..e2c5a94 (online token status, inactive while `tokenStatus` is unset) and identity-facts. Founder order: approve, setup, ESO apply, verify, deployment apply (image + env), login, clear-password, restart, login. **2026-10-31, `svc-keycape` live and verified:** CCR-2026-0054 approved and binding confirmed; founder-run setup, ESO apply, verify, deployment rollout (image `main-e2b5fa1` + env; both sources equal), `admin` login, `clear-password`, restart, env-only verify and a second `admin` login all passed ([evidence](../docs/evidence/2026-10-31-nk56-svc-keycape-bind/README.md)). Three of four binds are ESO-delivered; `keycape-config` no longer holds the bind password. Remaining: `svc-privacyidea` (resolver password stored in the privacyIDEA database, not a Secret key: needs its own design). **2026-10-31, `svc-privacyidea` design proposal (for founder decision):** privacyIDEA keeps the resolver `BINDPW` encrypted in its own database, and only the `pi-admin` API can change it (`realm_bootstrap.py`), so neither ESO nor an unattended job can deliver it without `pi-admin`'s password standing in the cluster (rejected: SI rules). Proposed: **OpenBao custody with attended write-through**, following CCR-2026-0028 (`attended-host-key-delivery`): 1. bind-lane `svc-privacyidea setup`: the custody value must sign in to LLDAP as `svc-privacyidea` (LLDAP is the authority for the current password; the working resolver proves privacyIDEA holds the same one), then KV `directory-binds/svc-privacyidea` CAS=0, coding agents fenced. No Kubernetes role, no ESO, `runtime_access: false`. 2. `realm_bootstrap.py --switch-bind --from-openbao`: reads the KV value in memory under the attended `platform-admin` session and writes only `BINDPW` with the attended `pi-admin`; the existing preservation checks apply, and the write must be proven by an MFA login through the resolver (`--verify-user`). 3. Rotation becomes: change the password in LLDAP, write the next KV version, run step 2, verify with an OTP login. No custody decryption; the founder runs it and proves it with an MFA login. 4. CCR-2026-0055 needs a request type for attended delivery into a service; railiance-platform's `credential-change.py` gains it, modelled on `attended-host-key-delivery`. The done criterion of T04 then reads: three binds ESO-delivered, `svc-privacyidea` held in OpenBao with attended write-through. Alternative: keep it in founder custody as a recorded exception until privacyIDEA can read the resolver password from a file or environment. **2026-10-31, founder decision: OpenBao custody with attended write-through.** Prepared in source: railiance-platform `2d4a383` adds request type `attended-service-credential-delivery` (`scripts/attended_service_delivery.py`, 4 tests; no policy, auth role, ESO or runtime frontdoor allowed) and CCR-2026-0055 (proposed). `bind-lane.py svc-privacyidea`: setup proves the custody value current by an LLDAP sign-in as `svc-privacyidea`, fences coding agents and writes KV v1 (CAS=0); verify proves the KV value current the same way, the fence present and no workload role. `realm_bootstrap.py --rotate-bind --from-openbao` rewrites only `BINDPW` on the service bind from OpenBao (attended platform-admin) and requires `--verify-user`. Correction: `pi-admin` signs in with a password; the MFA proof is the `--verify-user` login through the resolver. **Done 2026-10-31.** `svc-privacyidea` live: CCR-2026-0055 approved; founder-run setup and verify passed (KV v1, value proven current by LLDAP sign-in, no workload role) and `realm_bootstrap.py --rotate-bind --from-openbao` wrote the resolver from OpenBao with an `admin` MFA proof ([evidence](../docs/evidence/2026-10-31-nk56-svc-privacyidea-bind/README.md); a first apply wrote the same value but its check got a code that was not `admin`'s; both MFA prompts now name the user). All four service binds are held in OpenBao: `svc-provisioner`, `svc-authelia` and `svc-keycape` ESO-delivered, `svc-privacyidea` by attended write-through (founder decision 2026-10-31 amends the done criterion accordingly). No bind rotation needs the custody copy decrypted. The custody copy keeps the current values as a fallback. the env. ## Synthetic verification instead of a founder sign-in ```task id: NK-WP-0056-T05 status: done priority: medium assignee: claude-code depends_on: [NK-WP-0056-T04] state_hub_task_id: "bb511d1e-6d5d-56d1-9030-2e0d94006a16" ``` A synthetic test user with an OTP seed held in OpenBao, and a journey that signs in through Authelia and KeyCape with MFA and resolves through privacyIDEA. Reuse the identity-provider journey acceptance. The executor runs it as the check between consumers and reverts on failure. **2026-10-31, founder decision: split into T05 (runner), T08 (synthetic stack on real pinned images), T09 (synthetic user and seed in OpenBao) and T10 (unattended live run).** T05 is now the runner only: a stdlib journey that drives KeyCape `/authorize` (PKCE, `acr_values=urn:netkingdom:aal2`), Authelia `/api/firstfactor` and its OIDC authorization, the KeyCape callback and OTP form (KeyCape validates the code through privacyIDEA), and the KeyCape token exchange, then verifies the ID token's signature and `assurance` (aal2, `otp`). Secrets come from files or descriptors, never argv; the receipt is value-free and names the first failed step. All three consumer binds are on the path (`svc-authelia` at first factor, `svc-keycape` at the callback, `svc-privacyidea` at the OTP check); `svc-provisioner` is not. **Done when:** the runner and its tests (in-process fakes per bind, fail-closed at the named step, no secret in the receipt) pass in the identity-provider journey acceptance workflow. **2026-10-31, implemented:** `sso-mfa/k8s/synthetic-journey/journey.py` (stdlib; RS256 verified against KeyCape's JWKS without third-party packages; `host_map` for an in-cluster executor) with 26 tests in `test_journey.py`. Each broken bind fails closed at its step (`svc-authelia` → `authelia_first_factor`, `svc-keycape` → `keycape_callback`, `svc-privacyidea` → `keycape_otp`), and a sign-in without an OTP challenge, an aal1 token or a tampered signature fails. Green locally and in the workflow's pinned `python:3.12-slim` image with the network off; the job is added to `identity-journeys.yaml`. **Done:** the workflow's `provider` job passed on `0b0c6c9` (run 719). Next: T08. ## Synthetic identity stack on pinned images ```task id: NK-WP-0056-T08 status: done priority: medium assignee: claude-code depends_on: [NK-WP-0056-T05] state_hub_task_id: "02611880-9ea4-5c9d-a5ab-b50847608f92" ``` A throwaway stack on the pinned production images (LLDAP, Authelia, privacyIDEA, KeyCape), published on 127.0.0.1 only, with generated values and production-shaped binds (`svc-authelia`, `svc-keycape`, `svc-privacyidea`), a synthetic user with a TOTP token, and the T05 runner against it. Extends the `bind-split-rehearsal.py` pattern. **Done when:** the journey passes on the healthy stack and fails closed, at the expected step, for a deliberately broken bind of each of the three consumers, with value-free receipts. **Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t08-synthetic-stack/README.md)): `sso-mfa/k8s/synthetic-journey/stack.py` + `pi_fixture.py` on the four pinned images. Healthy passes (aal2, `otp`, signature verified); `svc-authelia` fails closed at `authelia_first_factor`, `svc-keycape` at `keycape_token` (KeyCape binds at token time, not at the callback: mapping corrected), and `svc-privacyidea` at `keycape_otp`, **but only after privacyIDEA's resolver cache expired** (live `CACHE_TIMEOUT` 120 s; the first journey after the break passed). Each scenario passes again after the password is restored. Also: privacyIDEA refuses a reused TOTP time step, so the runner gained `min_time_step`. Consequence for T06/T10: the between-consumers check for `svc-privacyidea` must wait out or clear the resolver cache, and the attended `--rotate-bind` MFA proof has the same blind spot. ## Synthetic user and seed in OpenBao ```task id: NK-WP-0056-T09 status: done priority: medium assignee: claude-code needs_human: true decision_id: 91323969-43fa-460f-9e0a-2b8cd44bdc9a intervention_note: "Founder runs the live setup (LLDAP user, privacyIDEA token, OpenBao KV) under a CCR." depends_on: [NK-WP-0056-T08] state_hub_task_id: "1f788455-f79e-5630-bede-ec3218f6969d" ``` An unprivileged live user (for example `syn-journey`, no groups), its own privacyIDEA TOTP token, and password and seed held in OpenBao with coding agents fenced, a dedicated KeyCape public client for the journey, prepared as `bind-lane.py`-style setup/verify helpers under a CCR. Founder-run. **Done when:** setup and verify pass live and one founder-started journey run passes with a receipt. **2026-10-31, prepared for founder decision** (`sso-mfa/k8s/synthetic-journey/T09-RUNBOOK.md`): CCR-2026-0057 (railiance-platform, proposed; `attended-service-credential-delivery`, KV `platform/workloads/net-kingdom/synthetic-journey/syn-journey` fields `PASSWORD`, `TOTP_SEED`, no workload reader), SecurityCase `security-cases/nk-synthetic-journey.yaml` (public PKCE client, MFA pinned), and `lane.py` setup/verify/teardown with 8 fake-backed tests. Identity route: attended owner trial by `admin` (the route the NK-WP-0049 retirement names for test identities), not User Engine onboarding; name `syn-journey` under a proposed `syn-` convention (SI §5), declared `SERVICE`, status `planned`. Open, recorded: KeyCape marks every token `principal_type: human`; KeyCape has no per-client group gate. Waiting on: CCR approval, case acceptance, founder run of the runbook. **Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t09-synthetic-identity/README.md)): CCR-2026-0057 approved; case accepted; client registered; founder-run `lane.py setup` and `verify` passed. The live journey reached aal2 (`pwd`+`otp`, signature verified) against `kc.coulomb.social`. Two lane fixes on the way: privacyIDEA tokens are looked up by serial, and enrollment uses a privacyIDEA-generated key, because `pi-admin`'s `action=*` enables `totp_force_server_generate`, which silently replaced the supplied key (reproduced on 3.12.2). One partial run was removed with `lane.py teardown`. Next: T10 needs the executor identity (T03) and a `workload-kv-read` request for it. ## Unattended live journey ```task id: NK-WP-0056-T10 status: done priority: medium assignee: claude-code depends_on: [NK-WP-0056-T03, NK-WP-0056-T09] state_hub_task_id: "a68252da-46f7-52eb-81d3-942ddd9ecb9b" ``` The executor identity (T03) reads the journey secrets from OpenBao itself and runs the journey as the check between consumers; failure triggers the revert path. **Done when:** the journey runs unattended against live with no person involved, and fails closed against a deliberately broken bind on a synthetic directory (T08). **2026-10-31, prerequisites:** - Done: the `sso` ingress rules for executor pods were applied by the founder and read back live. - Done: railiance-platform CCR-2026-0059 drafted (`c9b68ea`, `proposed`). It is the journey read with its own role, `net-kingdom-bind-executor-syn-journey-read`. It widens CCR-2026-0057; standing reader (founder decision). - Two executor gaps found while drafting it: 1. **Two roles.** The executor logs in to OpenBao once (`openbao.k8s_role`) and reads the bind and the journey with that token. railiance-platform keeps one role per lane and never combines policies, so the executor needs a second login for the journey read: a `journey.k8s_role` and its own `OpenBao` client, revoked like the first. 2. **Audience.** CCR-2026-0058 and CCR-2026-0059 bind audience `openbao`. The rapp Job projects only the default API-audience token, so it also needs an `openbao`-audience projected token, and `openbao.jwt_file` must point to it. - Both need an executor change, a new image and a new pin. **2026-10-31, gaps fixed** ([evidence](../docs/evidence/2026-10-31-nk56-t10-openbao-lanes/README.md)): - `1cdc18f`: one OpenBao client per lane, and one role for both lanes is refused. - Every login is revoked at exit, also when setup fails. - A third gap was found and fixed: the 15m token is shorter than the 20m budget, so clients now log in again before the lease ends, re-reading the rotating JWT. - Synthetic pilot with per-lane tokens: completed and reverted, `all_met`. Examples re-pinned. - rapp `368fee4`: - a second projected token with audience `openbao`; - the config names both CCR roles; - the renderer refuses other roles and the API token; - tests tie the roles to the platform CCRs. - The founder chose a standing journey reader. CCR-2026-0059 records it (railiance-platform `98a8b08`). Done since: - The image for `4815c58` is `sha256:716dccef6707deb570e19cc2b3edfda4298f9bce580685896bc9320f186bf6b3`. - The identity-journeys executor tests passed in CI. - The executed files in the image are byte-identical to the repo and to the admission pins. - It is pinned in the rapp (`c107afc`, pushed). - CCR-2026-0059 was approved by the founder as platform-owner and net-kingdom-owner (railiance-platform `831a0f9`). **2026-10-31, lanes live** ([evidence](../docs/evidence/2026-10-31-nk56-t10-executor-lanes/README.md)), founder-run with `T10-LANES-RUNBOOK.md`: - The rapp's standing objects are deployed. `sso/nk-bind-executor` has uid `bd15569c…`, and the Role matches the ceiling. - The bindings of CCR-2026-0058 and CCR-2026-0059 are confirmed (railiance-platform `b4bafb0`). - `executor-lanes.py setup`: - both policies applied; - `cas_required` set; - the standing journey-read role created; - the write role absent; - the agent fence holds. - The first setup stopped because platform-admin has no `patch` on `platform/*`. It now uses a metadata write that names only `cas_required`, with a before/after guard (`12c9b2d`). - `verify`: the journey token reads only its path, with a lease of at most 15m. A wrong ServiceAccount, a wrong audience and a login to the write role are all denied. **2026-10-31, journey-only mode** ([evidence](../docs/evidence/2026-10-31-nk56-t10-journey-only/README.md)): - `cli.py --journey-only` is read-only and needs no admission: - it logs in with the journey-read role only; - it builds no Kubernetes or directory client and makes no approval or audit call; - exit codes: `0` pass, `1` fail closed, `4` refused. - The admission check and this mode share `operations.journey_from_kv`. 45 executor tests. - Synthetic proof on the T08 stack with a journey-only OpenBao token: - healthy: pass; - broken svc-authelia bind: fail closed at `authelia_first_factor`; - restored: pass; - the journey token cannot read the bind; - `all_met`, no values. This is T10's fail-closed proof. - rapp: `templates/journey-job.yaml` and `render_job.py --journey-only`: - the config holds only `openbao` (no role) and `journey`; - the Job mounts only the openbao-audience token; - founder-run with `make journey`. The image for `9a81123` is `sha256:550bb9d0db89716835b501b4bc822b9588aa63ea15d0e59c61f6a1768b3f16ee`. Its executed files are byte-identical to the repo, and it is pinned in the rapp (`9cb19d9`). The first live `make journey` was refused at setup (`URLError`). `make netcheck` showed that k3s applies NetworkPolicy to a new pod about 3 s late. The executor now waits up to 30 s for OpenBao's `sys/health` before its first login (`15c9cdb`). The image `sha256:71da4a51e331cb82a5bde1e44025dc3b1b7ea4019dd9dbd64e7196cd3dd21471` is verified and pinned in the rapp. **Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t10-journey-only/README.md)): - The founder started `make journey`, which created Job `sso/bounded-executor-journey-20261031-194719`. - It ran unattended against live with no person in the journey and passed all eight steps to an `aal2` ID token (`pwd` + `otp`), with the signature verified. `effects: false`. The runner's hash equals the repo. - The fail-closed half is the synthetic proof: a broken svc-authelia bind fails at `authelia_first_factor`. - CCR-2026-0059 is now `verified`. For T06, separately: the approval-engine and audit-core replies, and the token Secret. ## Executor runtime: core runner ```task id: NK-WP-0056-T11 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T01, NK-WP-0056-T03] state_hub_task_id: "24121a14-92d8-577c-a651-992afcbdb4cc" ``` The executor itself, in net-kingdom (founder decision 6d1adce9): - consume one admission through the approval-engine carrier (independent GET, digest check, aal2 check, consume once); - refuse outside the window or ceiling; - run the pinned steps one consumer at a time, with checks after each mutating step, settle times, revert on failure, the budget (wall time, attempts) and an `on_unknown: stop`; - emit `execution.run.started` and the receipt (PDP `not-evaluated` for the pilot). Tested against in-process fakes for the carrier, operations and audit sink. **Done when:** the runner passes its tests, including revert on a failed check, budget exhaustion, a stale or reused approval, and a step outside the ceiling. Every refusal must happen before any effect. **Done 2026-10-31.** `bounded-executor/executor.py` with 23 tests (`bounded-executor/tests/test_executor.py`; fakes for carrier, operations, checks, trust, audit, clocks), modelled on approval-engine's real object and consume response. Refusals before any effect cover: - an invalid admission; - another executor; - source drift; - unsatisfied trust; - an unknown operation; - outside the window, or a window shorter than the budget; - a binding digest or actor mismatch; - a non-human, aal1 or unlisted approver; - an already-consumed approval. Consume: - an unclear consume is read back and never repeated; - an idempotent response is refused; - with no audit custody, no run. Failure handling: - a failed check reverts in reverse order, each revert proving the restored state with its own checks; - a clean revert retries within `attempts`; - a failed revert, or a revert check that fails, stops and alerts; - an unknown outcome stops without reverting; - budget exhaustion and the kill switch revert; - settle times are waited and counted; - a receipt-custody failure is reported. Receipts carry no operation values. Standard and validator change: BEA-9 now applies to revert steps, which name their own checks; examples regenerated. ## Executor runtime: pilot operations and packaging ```task id: NK-WP-0056-T12 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T11, NK-WP-0056-T08] state_hub_task_id: "815cc63a-a2a0-5b8f-83ae-2b80e37ea8f5" ``` The pilot's operations: - OpenBao read and CAS write of the next version; - LLDAP self-rotation of the bind; - ExternalSecret force-sync and its status check; - Deployment restart and rollout check; - the synthetic journey. Run them on the T08 synthetic stack, including a revert. Add the Containerfile and image workflow in net-kingdom. The `rapp-bounded-executor` package is created with `rmgr rapp wrap`, and the founder creates the Forgejo remote. It runs one Job per admission in `sso` with an egress-only NetworkPolicy. **Done when:** a synthetic rotation completes and a deliberately failing one reverts, with receipts. **Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t12-synthetic-pilot/README.md)). `bounded-executor/operations.py`: - the OpenBao KV v2 CAS client; - LLDAP self-rotation, the same operation forward and in revert; - the Kubernetes ExternalSecret force-sync and status check, and the rollout restart that raises `RolloutFailed`; - the pilot checks. `bounded-executor/synthetic_pilot.py`: rotation `completed`, and a one-off wrong delivery `reverted` in declared order with the earlier value restored. 29 tests. Findings: - CCR-2026-0058's `required_parameters` refused reads. Corrected in railiance-platform; needs re-approval. - Revert steps now run in declared order (BEA-10). - Definite failures revert; unknown outcomes stop. - Receipts carry error types. The image part moved to T13 (split agreed 2026-10-31). ## Executor runtime: packaging ```task id: NK-WP-0056-T13 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T12] state_hub_task_id: "7355e4a2-95cc-5860-a0b0-52978b8501e0" ``` The CLI entry point for one admission: - reads the admission, the approval id and the configuration; - uses Kubernetes auth to OpenBao; - uses the in-cluster Kubernetes and audit sender; - ships the pinned `lldap_set_password` in the image. Also: the Containerfile and the Forgejo image workflow in net-kingdom, and the `rapp-bounded-executor` package (`rmgr rapp wrap`, founder creates the Forgejo remote). The package holds one Job per admission in `sso` with the T03 ServiceAccount, Role and an egress-only NetworkPolicy. **Done when:** the image builds in the workflow, the rapp renders and passes `server-dry-run` against a synthetic manifest check, and nothing is applied. **2026-10-31, in progress.** Done in net-kingdom: - `bounded-executor/cli.py` (exit codes 0/3/4/5/6), `clients.py` (approval-engine GET/consume, audit-core `POST /v1/events` with `Idempotency-Key`); - events now carry `occurred_at`, which audit-core requires; - `Containerfile`: pinned Python and LLDAP stages, hash-locked requirements, UID 10001; builds locally; - `.forgejo/workflows/bounded-executor-image.yaml`, and the executor tests are in `identity-journeys.yaml`; - the pilot example pins the executed files (`executor.py`, `operations.py`, `journey.py`). 35 tests. **2026-10-31, rapp reshaped.** `~/rapp-bounded-executor` is at `forgejo-remote:coulomb/rapp-bounded-executor` (`4517aaf`; rebased onto the Forgejo stub, so local hashes changed): - Standing objects in `sso`: - `manifests/rbac.yaml`, a byte-identical copy of `sso-mfa/k8s/bounded-executor/rbac.yaml`. A test fails on drift; `make ceiling-sync` copies it. - `manifests/networkpolicy.yaml`: no ingress. Egress only to DNS, the API server (node `/32`, 6443), LLDAP 17170, KeyCape 8080, Authelia 9091, OpenBao 8200, approval-engine 8080 and audit-core 8080. - No Namespace, Deployment, Service, probes or database. - `templates/job.yaml` and `tools/render_job.py` render one ConfigMap (admission + config) and one Job per admission, named `bounded-executor-`: - `make run` uses `kubectl create`, so a second run of the same admission fails; - `backoffLimit: 0`; `activeDeadlineSeconds` = min(budget + 300 s, time left in the window); - projected SA token, no automount; - approval-engine and audit-core bearers come from an ESO Secret `bounded-executor-tokens`; - the renderer refuses a placeholder digest, a window that is not open, another executor, secret-like config keys and token paths that differ from the mounts. - `declarations/rapp.yaml` names no database. `rmgr rapp validate` passes. 12 tests. - `docs/runtime-contract.md` and `docs/platform-handoffs.md` list what must exist before T06. Server dry run with the limited `ops-mason-builder` identity: - ServiceAccount and NetworkPolicy: admitted. - Role, RoleBinding and the per-admission ConfigMap and Job: `Forbidden` for that identity, so the founder runs them. - The SSH path (`kubectl` on railiance01) can't read the k3s kubeconfig. The Makefile therefore takes `KUBECTL=...`. Left: - DONE 2026-10-31, founder: full server dry run with the admin kubeconfig. The ServiceAccount, Role, RoleBinding and NetworkPolicy were applied server-side as a dry run, and the synthetic BEA-2026-0001 ConfigMap and Job were created as a dry run. Nothing was persisted. **Done 2026-10-31.** - The image workflow for `ba9d266` succeeded, and the digest `sha256:8f40f998738789aec21898fe2a36fda56f33c6ae2bb5749f88583d4d70f070c2` is pinned in the rapp (`e9a943b`). - `check-oci-image.sh` found the manifest, and `rmgr rapp validate` passes. 13 rapp tests. - `rmgr rapp pin-image` only scans `manifests/`, so the pin went in by hand. A test keeps the Makefile, Job template and declaration pins equal. - Nothing was applied. Follow-up outside the done criteria: - DONE 2026-10-31: the founder created the Forgejo repo with a README stub. The local commits were rebased onto it (the local README kept) and pushed; HEAD is `e779a22`. Follow-up 2026-10-31 for T10/T06 reachability: - `netpol-sso.yaml` gained `allow-bounded-executor-to-{lldap-api,keycape,authelia}`, which admit only executor pods, on 17170, 8080 and 9091; never LDAP 3890. Tests: `test_netpol.py`, plus a rapp test that its in-namespace egress equals these admissions. Prepared in source, not applied. - Live reads (limited identity): - OpenBao's chart policy admits every namespace on 8200, so nothing is needed there. - approval-engine is deployed but admits only secrets-engine and namespaces labelled `railiance.io/approval-engine-client=true`, and none are. - audit-core has no ingress from `sso`. - Both owners were asked for pod-scoped rules; see the rapp's `docs/platform-handoffs.md`. ## Executor approval identity: service-client case type ```task id: NK-WP-0056-T14 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T13] state_hub_task_id: "ec87ed9f-9604-5843-af08-bc771a58712d" ``` approval-engine only accepts an approval request whose `binding.actor` equals the caller's KeyCape token subject (approval-engine `docs/bounded-execution-admission-carrier.md`). The executor therefore files its own request, the founder approves it, and the executor consumes it. For that it needs its own KeyCape confidential client. KeyCape has no generic service-client registration. Each existing client was registered by its own script (`approval-clients-rollout.py`, `secrets-engine-openbao-client-rollout.py`). The canon route is a new security case type, `oidc-service-client` 0.1, next to `oidc-browser-relying-party`: - `client_credentials` grant, resource audience, service subject, tenant, roles, scopes, a token lifetime of at most 15m; - the client secret minted straight into OpenBao custody, readable only by KeyCape and the consuming service; - `keycape-client.py` support for building the client from such a case; - the first case: `nk-bind-executor-approval` (audience `approval-engine`; scopes `approval:create`, `approval:read` and `approval:consume`; subject `service:nk-bind-executor`; tenant `tenant:platform`). The split of T14 into T14, T18 and T19 is a founder decision (2026-10-31). **Done when:** - the type, its tests and the first case validate; - `keycape-client.py` renders the service client and is tested; - an acknowledgement packet is ready for the founder. Nothing is applied. **Done 2026-10-31.** SecurityCaseType `oidc-service-client` 0.1 is `proposed` (`security-case-types/oidc-service-client/`): - type, README and two test files; - the first case `examples/nk-bind-executor-approval.yaml`: audience `approval-engine`; scopes `approval:create`, `approval:read` and `approval:consume`; subject `service:nk-bind-executor`; tenant `tenant:platform`; 15m; - the consumer reads the secret natively through its own bound role (`sso/nk-bind-executor`, audience `openbao`), with no Secret copy; - new capability declaration `net-kingdom.keycape-service-client-registration`. `keycape-client.py` renders service clients in key-cape's canonical shape. Adopting `secrets-engine-approval` reproduces key-cape's example record exactly. Both builders refuse missing parameters. Tests: 138 pass (type, browser type, validator, keycape-client). Memo `nk-wp-0056-t14-oidc-service-client-r1` (`decision/`) passes informed-decision's intake validator and State Hub's dispatcher contract. Hub decision `f8044f81`. Next: - reviews from ops-mason, ops-warden, key-cape and railiance-platform; - the operator queues the memo; - the founder acknowledges; - the case is accepted and goes live, which makes the type `implemented` (as part of T18/T19). ## Executor approval client and request phase ```task id: NK-WP-0056-T18 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T14] state_hub_task_id: "37186772-2749-573f-bc68-1ccf6840b60b" ``` Executor code: - a fresh KeyCape token exchange per approval call (as secrets-engine does), with the client secret read through an executor OpenBao lane (the case's custody); - a `request` phase that creates the approval (`action: bounded-execution`, target admission id and digest, `human_control: true`, `pdp_path: false`) and prints its id; - the existing run phase then consumes it. Presenting the approval to the founder follows the alignment review's "present an existing approval" contract ([review](../docs/proposals/2026-10-31-decision-services-alignment-review.md), S1 informed-decision). **Done when:** the request and run phases are tested on fakes, and the new image is pinned. **2026-10-31, built:** - `b71f509`: `KeyCapeClientCredentials` (`client_secret_basic`, one scope per token, public Host for the in-cluster URL, no redirects). - `ApprovalEngine` takes a per-scope token provider and can create approvals. - `cli.py --request` files the approval as `service:nk-bind-executor`: - principal is the first approver's subject; - target is the admission id and digest; - validity is the admission window; - `human_control` is on; - `required_count` is the number of listed approvers; - the client secret comes through its own OpenBao lane, is read once, and that login is revoked at once. - audit-core's review found that the audit client rejected 202, the status of a first accept. The executor would have stopped after consuming its approval. Fixed: 202 and 200 count as stored, and 409 is a distinct defect. 58 executor tests. - rapp `48566a6`: - the config names the executor's own client, and the renderer requires it exactly; - the run Job mounts only the audit-core bearer; - `make request` adds a request Job. **Done 2026-10-31.** The image `sha256:c67cb04485ee53b910c2443f43eada0e80273ea879de6853e120a32415194a04` (`7bd6033`) has executed files byte-identical to the repo. It is pinned in the rapp (`b90951a`). A live request also needs T20 and T21. ## Executor approval exercise against the real services ```task id: NK-WP-0056-T19 status: done priority: medium assignee: claude-code depends_on: [NK-WP-0056-T18] state_hub_task_id: "dd9c9500-40a7-550e-9672-14b5705e99ed" ``` Run create, approve, consume, idempotent retry, a different digest, revoke-before-consume and a wrong scope against approval-engine's real source and KeyCape's pinned image with synthetic keys (secrets-engine `tools/exercise_approval_identity.py` pattern). Value-free receipt. **Done when:** the exercise passes and its receipt is recorded. **Done 2026-11-03** ([evidence](../docs/evidence/2026-11-03-nk56-t19-approval-exercise/README.md)). The script is `bounded-executor/exercise_approval_identity.py`. It runs the real request phase, KeyCape client, preflight and consume against KeyCape `9e082dca` and approval-engine `f41a9ca`. 16 checks pass. Covered: create, approve, consume, idempotent retry, different digest, changed admission, revoke-before-consume, wrong scopes and wrong secret, a service approving, and an aal1 entry. **Finding:** approval-engine stores an entry's assurance as KeyCape's JSON object, not a bare `aal2`. The executor would have refused every real founder approval. Fixed in `625ad65` (`entry_is_aal2`). The request-phase tests no longer expire with the example window. Follow-ups, both settled 2026-11-04: - **Image.** The image built for `66dc397` is `sha256:3aa2bcce…500d8a`. The `provider` job (executor tests) passed, and the 7 executed files are byte-identical to the repo. It is pinned in the rapp (`3ddacfc`), replacing T21's `e0b9bf6c`, which had the defect. - **Founder decision: no `required_assurance` at request.** approval-engine's rigidity is per approval: no floor by default, with an optional `required_assurance` level. This decision does not need approval-engine to require aal2. The admission's own aal2 (BEA-C4) is still enforced by the executor. ## Executor audit sender ```task id: NK-WP-0056-T15 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T13] state_hub_task_id: "a3e764ab-e545-5102-a4a7-a07e91ce3f6b" ``` audit-core accepts the `started` event and the receipt only from a registered sender (AUDIT-WP-0018-T02). Needed: - the versioned receipt schema, validator and fixtures (NK-WP-0040-T02 scope); - the sender credential's custody, preferably an OpenBao read role for the executor rather than an ESO Secret; - audit-core's registry entry and ingress rule. Facts sent 2026-10-31 (hub `ed03ba30`). **Done when:** a synthetic run's events are accepted by audit-core's real ingestion (202, duplicate 200, conflict 409, wrong source refused), and the live seed is prepared for the founder. **Done 2026-11-04** ([evidence](../docs/evidence/2026-11-04-nk56-t15-audit-sender/README.md)). - **Schema:** receipt schema v0.1 (`canon/schemas/execution-attribution-receipt_v0.1.schema.json`) covers both events. The validator (`tools/execution-attribution-receipt`, EAR-1..10) has fixtures generated from the executor. - **Executor:** the started event now carries `data.window.not_after` (audit-core's T03 request), the digests and the planned step count. The receipt carries `receipt_version` and `initiator.iss`. - **Exercise:** `bounded-executor/exercise_audit_sender.py` runs against audit-core `876c3f6`'s real ingestion and scope overlay. 8 checks pass: - 202 for each event; - duplicate 200; - 409 reported as run-id reuse; - wrong source, tenant or token refused and not stored; - a receipt-less start flagged by its own window. - **Custody:** CCR-2026-0063 (railiance-platform `f3b0c4f`, proposed). ESO delivers to `sso/bounded-executor-tokens` key `audit-core`, which the run Job mounts. NetKingdom answered "ESO as drafted". - **Live seed:** audit-core `scripts/seed-nk-bounded-executor-sender.py` (`876c3f6`) is the single seeder. Its runbook is in audit-core `docs/bounded-executor-receipts.md`. - **What remains is founder-run:** T06 runbook section 3. ## Owner ingress for the executor pod ```task id: NK-WP-0056-T16 status: done priority: medium assignee: claude-code depends_on: [NK-WP-0056-T10] state_hub_task_id: "a08c6b82-be3f-5dca-82f7-b2a0ced6b335" ``` approval-engine (hub `39a3bffa`) and audit-core (hub `deea5bf8`, `ed03ba30`) each admit only namespace `sso` with pod label `app.kubernetes.io/name=bounded-executor` on 8080. Our side: the rapp's egress policy and `make netcheck`. **Done when:** both rules are live and `make netcheck` (extended to both endpoints) shows them reachable from an executor pod. **2026-11-04: our side is done; the live apply is the founder's.** - Both owner rules are in source: approval-engine `e9e9f93` and audit-core `7728fde`. Neither is live (read-only check: the policy list in each namespace). - rapp `cf4bbb9`: `make netcheck` probes approval-engine and audit-core `/healthz` and informed-decision:8081. - Founder steps: T06 runbook (`sso-mfa/k8s/bounded-executor/T06-PILOT-RUNBOOK.md`) section 1. Each object is applied on its own, because live approval-engine has a policy that its file lacks. **Done 2026-11-04** ([evidence](../docs/evidence/2026-11-04-nk56-t16-owner-ingress/README.md)). The founder applied both rules. `make netcheck` from an executor-labelled pod reaches approval-engine and audit-core (`/healthz` 200), informed-decision:8081, OpenBao, KeyCape and Authelia from 3 s on. ## Founder approval path at aal2 ```task id: NK-WP-0056-T17 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T10] state_hub_task_id: "0964e610-cad3-500c-bd75-4d2b0323e886" ``` The founder must add a human approval entry with an `aal2` token carrying `approval:approve` for audience `approval-engine`. approval-engine names `informed-decision` as the approver surface. Find out whether that path exists and is usable today, and what a founder approval of one admission looks like end to end. Then name the missing pieces and their owners. **Done when:** the approval path is described with evidence (works today, or what is missing and who owns it). **Done 2026-10-31** ([finding](../docs/proposals/2026-10-31-nk56-t17-founder-approval-path.md)): it does not work today. - informed-decision is the live approver surface. It posts the human `approval:approve` entry. - But its intake always creates the approval itself with `actor: informed-decision`, and refuses a pinned `approval_id`. - approval-engine only lets the actor create. - The executor and BEA require the actor `service:nk-bind-executor`. Options: - A, recommended: informed-decision presents an existing approval filed by a registered submitter. - B: relax the actor binding in the executor and BEA. - C: a founder CLI approver. Founder decision needed before T14. ## Service-client type revision r2 from the owner reviews ```task id: NK-WP-0056-T20 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T14] state_hub_task_id: "e09162c4-01ea-540c-bbab-e8772f0f7ef5" ``` The 2026-10-31 reviews of `oidc-service-client` 0.1: - ops-mason: `ops-mason` `3c69a25`; - ops-warden: `ops-warden` `95d31c3`, `history/2026-10-31-oidc-service-client-follow-through-review.md`; - key-cape: `key-cape` `655d8d3`, `docs/oidc-service-client-type-review.md`; - railiance-platform: `railiance-platform` `6858bef`, `docs/credential-lane-designs/oidc-service-client-review.md`. They ask for these changes before the founder acknowledges the type: - The ESO role gets `audience: openbao` and `token_explicit_max_ttl`; the ESO policy gets `lookup-self` and `revoke-self`. - The consumer role name must differ from its policy name: `keycape-{client_id}-{consuming_service}-workload-kv-read`. - Refuse the `default` ServiceAccount and the `external-secrets` and `openbao` namespaces. - **Same namespace:** KeyCape's ESO target sits in `sso`, the consumer's namespace. Add the verification `consumer-cannot-read-delivered-secret` (an RBAC can-i denial) and correct the README. - An excess scope is refused with `invalid_profile_usage`, not `invalid_scope` (key-cape R3). - **Rotation:** the mint step only creates. Rotation is an attended platform-admin write, then an ESO refresh, then a KeyCape restart (railiance-platform R5, ops-warden R5, key-cape R5). - The catalog entry body gets the fields ops-warden R6 asks for. - The `custody-allowed-to-consumer` wording must mention the two self-lifecycle grants. - One restart per case: bind-env and register-client in one rollout (key-cape R4). - Open questions: - the workload name, `bounded-executor` or `net-kingdom-bind-executor` (ops-mason R9); - `consumer_owner_repo` as `rapp-bounded-executor` (ops-warden R10); - an independent claims check (ops-warden R8). Per-case CCRs are still required. railiance-platform drafted CCR-2026-0060 (KeyCape ESO reader) and CCR-2026-0061 (the executor's reader). The broker must admit the type after acknowledgement (railiance-platform R1, ops-warden R1). **Done when:** the type is at r2 with tests green, the memo r2 is validated, and the CCR names agree with the type. **Done 2026-11-01.** `oidc-service-client` is at 0.2 (`proposed`) with every review change applied (README history lists them). Settled here: - `consuming_service` stays `bounded-executor`; - the first case's `consumer_owner_repo` is `rapp-bounded-executor`. The consumer role is `keycape-{client_id}-{consuming_service}-workload-kv-read`. A new test checks CCR-2026-0060 and CCR-2026-0061 against the rendered role and policy names (they agree). 141 related tests pass. Memo `nk-wp-0056-t14-oidc-service-client-r2` passes informed-decision's intake validator and State Hub's dispatcher contract (hub decision `f8044f81`). The rapp follows the role rename (`828992e`). Next for the founder: queue memo r2 through the State Hub memo dispatcher and acknowledge the type. ## Executor submits its admission memo to informed-decision ```task id: NK-WP-0056-T21 status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T18] state_hub_task_id: "ae922bf8-067c-5d83-8129-4507639cfa24" ``` informed-decision built `present_existing` (`0e66a33`, INFD-WP-0010). The executor submits a memo through `/v1/memos` that pins `approval_id` and the binding digest and carries the admission YAML as its packet. informed-decision checks the approval by GET and presents it to the founder. Needed: - a projected token with audience `informed-decision-intake` in the request Job; - a NetworkPolicy path to informed-decision's private listener on 8081; - the request phase submitting the memo after creating the approval. Outside net-kingdom: - access-engine: review policy v13 hard-codes the requester `informed-decision`; - key-cape: the informed-decision requester client needs `approval:read`; - informed-decision: its registry entry and release (INFD-WP-0010-T02). **Done when:** the request phase files the approval and the memo, tested against informed-decision's real intake validator. **Done 2026-11-01.** After creating the approval, `cli.py --request` composes one present_existing memo: - the packet is the admission YAML itself (informed-decision's choice); - three required highlights cover the scope ceiling, the window and budget, and the plan with its revert; - the intent restates the binding without the actor; - the idempotency key is `nk-bind-executor.`. It is submitted to informed-decision's private intake with the projected `informed-decision-intake` token. Exit codes: 0 presented; 7 requested but the memo was refused (the approval stays usable, and resubmitting is idempotent). Tests: - the envelope passes informed-decision's own `validate_submission` with the present_existing profile; - the receipt is checked against the memo; - a refused memo exits 7. 61 executor tests. rapp (T21 commit): the request Job's projected token, the exact config section, and egress to informed-decision:8081. The service-identity declaration notes key-cape's prepared `approval:read` for informed-decision's requester. Live (founder-run, after the owners' releases): - access-engine policy v14; - key-cape `approval:read`; - informed-decision's registry row and its ingress for the executor pod; - the executor image pin. ## Pilot: unattended bind rotation under one admission ```task id: NK-WP-0056-T06 status: done priority: high assignee: claude-code needs_human: true intervention_note: "Founder signs the admission for the single live pilot rotation." depends_on: [NK-WP-0056-T01, NK-WP-0056-T03, NK-WP-0056-T04, NK-WP-0056-T05, NK-WP-0056-T10, NK-WP-0056-T13, NK-WP-0056-T14, NK-WP-0056-T15, NK-WP-0056-T16, NK-WP-0056-T17, NK-WP-0056-T18, NK-WP-0056-T19, NK-WP-0056-T20, NK-WP-0056-T21] state_hub_task_id: "6c3939f8-23a9-5f70-a3b1-94f4b291706b" ``` Rotate `svc-authelia`'s password with no person in the loop beyond the admission: generate into OpenBao, set in LLDAP, let ESO deliver, restart, run the synthetic journey, revert on failure, emit a receipt to audit-core. Rehearse on a synthetic directory first, then one live run under a founder-signed admission. **Done when:** the live rotation completes or reverts on its own, within its budget, with a verified receipt, and the founder's only action was the admission. **2026-11-04: ready for the founder.** Runbook: `sso-mfa/k8s/bounded-executor/T06-PILOT-RUNBOOK.md`. The rehearsal on a synthetic directory is T12's pilot. The go-live of the standing paths runs once, in this order: 1. owner ingress (T16); 2. case acceptance, hub decision `59d570ef`; 3. railiance-platform's custody go-live (`docs/credential-lane-designs/nk-bind-executor-go-live.md`, CCR-2026-0060, -0061 and -0063), with the audit seed inside it; 4. KeyCape bind-env and register-client in one restart. Then the run: `prepare_admission.py` writes `admissions/BEA-2026-0002.yaml`; then `make request`; the founder approves at aal2; `executor-lanes.py window-open`; `make run`; `window-close`. The image is `sha256:2926d7d9…` (net-kingdom `9bd7bf0`), pinned in rapp `cf4bbb9`. The approver subject is the LDAP DN (KeyCape `sub`). **2026-11-04: go-live done** ([evidence](../docs/evidence/2026-11-04-nk56-t06-go-live/README.md)). - The custody requests CCR-2026-0060, -0061 and -0063 are approved and applied, behind the fence. - The audit sender is seeded and delivered; the KeyCape client secret is minted and delivered; custody verify passed. - The case is `materialized`. KeyCape is bound and registered, with one restart. **2026-11-04/05: first pilot attempt, BEA-2026-0002, no effect** ([evidence](../docs/evidence/2026-11-04-nk56-t06-pilot/README.md)). - The request phase worked: the executor's own KeyCape client filed approvals and presented memos. - Two runs refused `setup:URLError`. Four diagnostic pods could not reproduce that. Fixed in `c2a0f2f`: the executor now waits for all endpoints, and errors are named by phase. Image `sha256:5f5df866…` is pinned in rapp `3ecd462`. - The approval was given as `platform-root`, and the executor refused it (`approval_entry`). The cause: informed-decision routes every `net-kingdom-admins` memo to `platform-root`, so `admin` cannot see them. Founder decision: hub `c9bedd9c`. - Next: - the decision; - a new admission, BEA-2026-0003, with pins at `c2a0f2f`; - request, then approve as `admin`, then the run. **Done 2026-11-05** ([evidence](../docs/evidence/2026-11-05-nk56-t06-pilot/README.md)). BEA-2026-0003 completed in 30.5 s, with no revert: - KV advanced; LLDAP self-rotation; ESO synced; Authelia restarted; the synthetic journey passed. - The receipt is valid (schema v0.1), and audit-core holds the started event and the receipt. - The founder's only act was the approval, as `admin` at aal2. - The interim routing `net-kingdom-admins` → `admin` is in informed-decision (`77161fe`; Argo pinned in railiance-platform `31cf74d`). - The write role stayed open overnight after the diagnostics, without use. `pilot-run.sh` now always closes the window. ## Budget and risk enforcement ```task id: NK-WP-0056-T07 status: done priority: medium assignee: claude-code depends_on: [NK-WP-0056-T06] state_hub_task_id: "a87c5edb-7604-5eba-9261-b1ed2ed16655" ``` Enforce the admission's budget and blast radius at run time (attempt and time limits, a kill switch the founder holds, refusal when a check is missing), and feed outcomes and drift into posture feedback (NK-WP-0031) so that recurring admissions can be reviewed in aggregate rather than one by one. **Done when:** a run that exceeds its budget or scope stops and reverts in a synthetic test, and the pilot's receipts appear in a posture-feedback report. **2026-11-04: enforcement and feedback are built; the pilot's receipts are still to come (T06).** - **Budget and scope:** attempt and wall-time limits, refusal of an unknown check or operation, and the scope ceiling were already enforced (T11). Covered by tests: budget exhausted reverts; kill switch reverts; window shorter than budget refused. - **Kill switch (`9bd7bf0`):** SIGTERM or SIGINT sets the stop flag. That happens when the founder runs `kubectl delete job`, and at the Job deadline. The runner reverts at the next step boundary. A stop before consume refuses and spends nothing. rapp `601ada1` sets the termination grace period to the budget. - **Posture feedback v0.1 section 4a (`9bd7bf0`):** `--receipts` reads the executor's events through the receipt validator. Findings: receipt-missing, not-completed, reverted and budget-pressure, plus the aggregate `execution-recurring-revert`. Owners come from `tools/posture-feedback/execution-owners.yaml`, never inferred. - **Left:** run the report over the pilot's receipts after T06. **Done 2026-11-05.** The pilot's receipt appears in a posture-feedback report (`docs/evidence/2026-11-05-nk56-t06-pilot/posture-feedback-report.json`): validated, `ok`, no findings.