From e1561c09ee681204d9bb11debf3aed6e5969ec31 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 5 Nov 2026 23:10:56 +0100 Subject: [PATCH] NK-WP-0058-T10 done: contract table part 3 (Argo vs manual applies, warden lanes, founder steps, agent-readable status); C3 corrected Only informed-decision lifecycle, the Secret admission guard and shared SecretStores are Argo-managed; two platform-admin session patterns; ~45 founder commands with five hand-carried values; the agent reads only NetworkPolicies. C3: the canon defines materialization as structure, so the order is right. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 943911@bnt-lap001 Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0 --- WORK-RECORDS.md | 2 +- docs/bounded-execution-contract-table.md | 141 ++++++++++++++++-- ...K-WP-0058-bounded-execution-operability.md | 25 +++- 3 files changed, 153 insertions(+), 15 deletions(-) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index d9acb66..86a077e 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -379,7 +379,7 @@ | task | NK-WP-0058-T07 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T08 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T09 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md | -| task | NK-WP-0058-T10 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | +| task | NK-WP-0058-T10 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T11 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md | | intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | | intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | diff --git a/docs/bounded-execution-contract-table.md b/docs/bounded-execution-contract-table.md index 73f948d..8bde1f5 100644 --- a/docs/bounded-execution-contract-table.md +++ b/docs/bounded-execution-contract-table.md @@ -1,7 +1,6 @@ # Bounded execution: cross-repo contract table -NK-WP-0058-T01 (part 1) and T09 (part 2), as of 2026-11-05. Part 3 (T10: operations and the founder) adds rows to -this file. NK-WP-0057-T01 takes the rows marked **owner decision** as inputs and does +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 @@ -235,7 +234,7 @@ NK-WP-0058-T09, as of 2026-11-05. It uses the same revisions as part 1, plus acc | --- | --- | --- | --- | --- | --- | --- | | 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 | net-kingdom commits `materialized` **before** the mint, because the broker refuses anything else | **owner decision** | +| 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**; small **owner decision** on who materializes | | 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 | @@ -269,12 +268,14 @@ NK-WP-0058-T09, as of 2026-11-05. It uses the same revisions as part 1, plus acc - **Contract:** the broker refuses unless the case is `status: materialized` with `acceptance: {at, record, by: [{role: owner}, {role: founder}]}`. -- **Gap:** the go-live runbook (`docs/credential-lane-designs/nk-bind-executor-go-live.md`, gate G1) has - net-kingdom commit `materialized` before the mint that materializes the case. The status claims a fact before it - is true. -- **Owner decision:** net-kingdom owns the case canon. Either the broker gates on `accepted` and the status moves to - `materialized` after verify, or the canon defines `materialized` as "admitted for materialization". This needs - railiance-platform's agreement, because the broker check changes. +- **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. Small + **owner decision:** amend the canon, or route materialization through ops-mason. ### C4: KeyCape client secret delivery @@ -333,7 +334,9 @@ access-engine's policy is the source, with the other checked against it. Until t ## Summary for part 2 -- **keep:** C2, with a net-kingdom guard test for the acknowledged `type.yaml` bytes. +- **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; @@ -343,7 +346,123 @@ access-engine's policy is the source, with the other checked against it. Until t (an owner request). - **owner decision:** - C1: a supported platform interface for window roles (railiance-platform); - - C3: what `materialized` means and when the broker admits (net-kingdom canon with railiance-platform); + - C3, small: who materializes. The canon names ops-mason; railiance-platform did it here; - 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`). diff --git a/workplans/NK-WP-0058-bounded-execution-operability.md b/workplans/NK-WP-0058-bounded-execution-operability.md index 3e9ab02..0db1d31 100644 --- a/workplans/NK-WP-0058-bounded-execution-operability.md +++ b/workplans/NK-WP-0058-bounded-execution-operability.md @@ -189,7 +189,9 @@ Mark the duplicated pieces, for example the two seeders and the two Secret names - **The two seeders and the two Secret names** were already settled on 2026-11-04: audit-core's script is the single seeder, and the Secret is `sso/bounded-executor-tokens:audit-core`. One stale copy remains in railiance-platform `docs/credential-lane-designs/bounded-executor.md`, which is a **delete** request. -- **keep:** C2. Acknowledged `type.yaml` bytes are hash-pinned by the broker, and net-kingdom has no guard test. +- **keep:** + - C2: acknowledged `type.yaml` bytes are hash-pinned by the broker, and net-kingdom has no guard test; + - C3. - **merge:** - C1: `executor-lanes.py` imports three railiance-platform internal helpers; - C4: the KeyCape secret naming convention is written twice; @@ -197,7 +199,8 @@ Mark the duplicated pieces, for example the two seeders and the two Secret names - C6: the executor's submitter profile is in informed-decision, access-engine and the rapp. - **owner decisions:** - C1: a supported platform interface; - - C3: `materialized` is committed before the mint; + - C3, small: who materializes (the canon names ops-mason; railiance-platform did it). An earlier draft called + the status order wrong; the canon confirms it; - C6: which repo is the source of the profile; - C7: routing and visibility. access-engine's `list` admits only the routing recipient, so the routing table decides who can approve. This goes to NK-WP-0057 with E5. @@ -207,7 +210,7 @@ Mark the duplicated pieces, for example the two seeders and the two Secret names ```task id: NK-WP-0058-T10 -status: todo +status: done priority: high assignee: claude-code depends_on: [NK-WP-0058-T01] @@ -223,6 +226,22 @@ Add rows to the T01 table, in the same format, for: **Done when:** every part 3 row has sources and a mark, and the table lists every status the agent had to ask the founder for on 2026-11-04/05 (the input for T05). +**Result, 2026-11-05.** Part 3 of `docs/bounded-execution-contract-table.md` has six rows (O1–O4, F1, F2), a +controller-per-object table, a founder-step table and the status table for T05. + +- **Argo (O1):** only informed-decision `deploy/lifecycle`, the Secret admission guard and the shared + SecretStores are Argo-managed, all with pinned revisions and manual sync. Everything else is applied by hand. + Three surprises came from this split. Which objects Argo owns is an owner decision for railiance-platform. +- **Sessions (O2, O3):** warden lanes for platform and audit-core steps, but a shell `bao login` for net-kingdom's + window commands. Merge on warden lanes. Owner request to ops-warden: strip every token variable. +- **Founder (F1, F2):** about 45 commands across three runbooks, with five kinds of values carried by hand. The + first approval came from the identity that happened to be signed in. These are inputs for T04. +- **For T05:** the agent can read only NetworkPolicies today. The proposed grant and the founder-only rows are in + the status table. +- **For T07:** four tools hard-code kubeconfigs. +- **Correction:** C3 in part 2 was corrected the same day. The canon defines materialization as structure only, so + committing `materialized` before the mint is right. + ### Contract tests for the cross-repo interfaces ```task