From 3109f950f9b1a32a359d1f6dcd4acbe228c16326 Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 10 Sep 2026 08:10:33 +0200 Subject: [PATCH] Cancel withdrawn approval operator reader without inventing auth Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc --- ...-approval-engine-operator-client-read.yaml | 20 +++++-- docs/credential-change-approval.md | 10 +++- .../keycape-approval-client-side-readers.md | 29 ++++------ schemas/credential-change-request.schema.yaml | 15 +++++ scripts/credential-change.py | 20 ++++++- tests/test_credential_change.py | 56 +++++++++++++++++++ ...-WP-0035-credential-lane-implementation.md | 28 +++++++++- 7 files changed, 152 insertions(+), 26 deletions(-) diff --git a/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml b/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml index d4ae81f..1470fb1 100644 --- a/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml +++ b/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml @@ -3,9 +3,20 @@ kind: credential-change-request schema_version: 1 request_type: workload-kv-read title: Client-side read of the approval-engine-operator client secret -status: in_flight +status: cancelled created: '2026-09-09' -updated: '2026-09-09' +updated: '2026-09-10' +cancellation: + owner: approval-engine + reason: >- + Requesting owner withdrew the combined lifecycle operator client: no + presenter exists and no client-side reader is wanted. Human approval belongs + to informed-decision; any future service presenter needs a narrow new request. + Completed verifier custody CCR-2026-0018 remains unchanged. + source_ref: >- + approval-engine@849c75bb094613ff6ac1a1d4cda56a745520c5a1:docs/keycape-service-registrations.md#approval-engine-operator + recorded_at: '2026-09-10' +# Historical omissions retained so cancellation does not invent an auth binding. in_flight: missing_fields: - openbao.auth @@ -85,10 +96,11 @@ access_frontdoor: catalog_id: approval-engine-operator-client selector: approval-engine operator client secret resolvable: false - readiness: pending-review + readiness: disabled delivery: - surface: undetermined + surface: none target: >- + Cancelled on owner withdrawal; the following alternatives were never admitted. Not determined. The delivery surface follows the actor: an attended operator implies a protected workstation file, an in-cluster component implies an ExternalSecret into the eventual approval-engine namespace, and an agent diff --git a/docs/credential-change-approval.md b/docs/credential-change-approval.md index f4a5412..6d10430 100644 --- a/docs/credential-change-approval.md +++ b/docs/credential-change-approval.md @@ -90,12 +90,20 @@ superseded cancelled ``` -`in_flight` is the only state that may explicitly omit completion-only fields. +`in_flight` is the only nonterminal state that may explicitly omit completion-only fields. It must declare every omission in `in_flight.missing_fields`, name the owner and blocking reason, and remain non-resolvable. The validator still checks every other field. This is not an applyable state and must never be used to hide a malformed active lane. +An incomplete request withdrawn by its owner may instead be `cancelled`, retaining +its `in_flight` declaration as history. This requires `cancellation.owner`, +`cancellation.reason` and a non-secret `cancellation.source_ref`, with front-door +readiness `disabled` and `resolvable: false`. Only the same declared omissions are +allowed; all other validation remains. Cancellation cannot authorize apply or +resolve a reader. Moving back to an approval/apply state requires completing the +binding; keeping the historical declaration does not bypass that requirement. + Only `approved` requests may be applied. Only `verified` requests may become `active`. diff --git a/docs/credential-lane-designs/keycape-approval-client-side-readers.md b/docs/credential-lane-designs/keycape-approval-client-side-readers.md index 9b07cdf..19febbc 100644 --- a/docs/credential-lane-designs/keycape-approval-client-side-readers.md +++ b/docs/credential-lane-designs/keycape-approval-client-side-readers.md @@ -4,7 +4,8 @@ RPF-WP-0035-T06, the residual handed over by the completed verifier custody (T05). Requests: [CCR-2026-0019](../../credential-change-requests/CCR-2026-0019-secrets-engine-approval-client-read.yaml) and [CCR-2026-0020](../../credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml). -Both are `in_flight`, neither is approved, and nothing is applied. +CCR-2026-0019 remains `in_flight` and unapproved. CCR-2026-0020 is +`cancelled` on its requesting owner's explicit withdrawal; neither reader was applied. ## Why this exists as separate requests @@ -52,23 +53,17 @@ the consumer's shape rather than engineered away. ## Reader 2 — approval-engine-operator (CCR-2026-0020) -Held open deliberately. The exact-path read policy -`workload-kv-read-approval-engine-operator-client` is written and committed so -the eventual reader inherits a bounded grant rather than one drafted under time -pressure. Writing the policy is not admitting a reader. +Cancelled 2026-09-10. Approval Engine explicitly requests cancellation because +no presenter exists and no client-side reader is wanted. Source: +`approval-engine@849c75bb094613ff6ac1a1d4cda56a745520c5a1`, +`docs/keycape-service-registrations.md`, operator withdrawal section. +The retained exact-path policy is an unapplied historical artifact, not an +admission. No role, credential delivery, rotation or verifier change follows. -**Missing:** the presenting actor and its placement. This client carries the -widest scope set in the pair — `approval:create`, `read`, `approve`, `revoke`, -`supersede`, `observe`, `emit`, and deliberately not `consume`. approval-engine -records `principal_type` on every entry rather than restricting who may supply -approver evidence, and its owner records that whether a non-human principal -*should* is approval doctrine belonging to `gate-house`. Naming a reader here -would hand the widest approval scope to a guessed identity, so the request names -what is undetermined instead. - -The delivery surface follows the actor: an attended operator implies a protected -workstation file; an in-cluster component implies an ExternalSecret into the -eventual `approval-engine` namespace; an agent surface implies neither. +Human approval belongs to informed-decision's PKCE client, INFD-WP-0001-T07/T08; +a future service requester needs its own narrow registration under the existing +Approval Engine owner work. Do not recreate the combined create/approve bundle. +The completed CCR-2026-0018 verifier custody remains unchanged. ## Not in scope diff --git a/schemas/credential-change-request.schema.yaml b/schemas/credential-change-request.schema.yaml index 49f4604..d4f3469 100644 --- a/schemas/credential-change-request.schema.yaml +++ b/schemas/credential-change-request.schema.yaml @@ -84,6 +84,21 @@ workload_kv_read: - rotate - compromised conditional: + status=cancelled_with_in_flight_declaration: + required: + - in_flight.missing_fields + - in_flight.blocking_reason + - in_flight.owner + - cancellation.owner + - cancellation.reason + - cancellation.source_ref + allowed_missing_fields: + - openbao.policy_file + - openbao.auth + access_frontdoor: + readiness: disabled + resolvable: false + note: Retain abandoned omissions without inventing a binding; all other validation and apply refusals remain. status=in_flight: required: - in_flight.missing_fields diff --git a/scripts/credential-change.py b/scripts/credential-change.py index f274439..60356cb 100755 --- a/scripts/credential-change.py +++ b/scripts/credential-change.py @@ -191,8 +191,16 @@ def reject_secret_text(text: str, field: str) -> None: def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings: list[str]) -> None: in_flight = ccr.get("status") == "in_flight" + abandoned_incomplete = ccr.get("status") == "cancelled" and "in_flight" in ccr missing_fields: set[str] = set() - if in_flight: + if abandoned_incomplete: + cancellation = require_object(ccr.get("cancellation"), "cancellation", errors) + for field in ("owner", "reason", "source_ref"): + require_string(cancellation.get(field), f"cancellation.{field}", errors) + frontdoor = ccr.get("access_frontdoor") or {} + if not isinstance(frontdoor, dict) or frontdoor.get("readiness") != "disabled": + errors.append("cancelled incomplete request requires disabled front door") + if in_flight or abandoned_incomplete: declaration = require_object(ccr.get("in_flight"), "in_flight", errors) listed_missing = require_list( declaration.get("missing_fields"), "in_flight.missing_fields", errors @@ -504,7 +512,15 @@ def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str: ) else: missing = ", ".join(ccr.get("in_flight", {}).get("missing_fields", [])) - lines.append(f" in flight; declared missing: {missing}") + label = "cancelled; retained omissions" if ccr.get("status") == "cancelled" else "in flight; declared missing" + lines.append(f" {label}: {missing}") + if ccr.get("status") == "cancelled" and ccr.get("cancellation"): + cancellation = ccr["cancellation"] + lines.extend([ + f"Cancellation owner: {cancellation['owner']}", + f"Reason: {cancellation['reason']}", + f"Source: {cancellation['source_ref']}", + ]) lines.extend( [ "Access front door:", diff --git a/tests/test_credential_change.py b/tests/test_credential_change.py index 35b54a0..0729282 100644 --- a/tests/test_credential_change.py +++ b/tests/test_credential_change.py @@ -134,6 +134,62 @@ class CredentialChangeTests(unittest.TestCase): _ccr, errors, _warnings = credential_change.validate_ccr(path) self.assertTrue(any("openbao.policy_file" in error for error in errors)) + def cancelled_incomplete_ccr(self) -> Path: + source = REPO_DIR / "credential-change-requests/CCR-2026-0011-scaleway-object-storage-bootstrap.yaml" + path = self.unapproved_ccr(source) + data = credential_change.load_yaml(path) + data["status"] = "cancelled" + data["access_frontdoor"]["readiness"] = "disabled" + data["cancellation"] = { + "owner": "requesting-owner", "reason": "No consumer requires this reader", + "source_ref": "owner/docs/reader-decision.md@reviewed-commit", + } + credential_change.dump_yaml(path, data) + return path + + def test_cancelled_incomplete_request_validates_but_cannot_apply(self) -> None: + path = self.cancelled_incomplete_ccr() + ccr, errors, warnings = credential_change.validate_ccr(path) + self.assertEqual(errors, []) + payload = credential_change.status_payload(ccr, warnings) + self.assertFalse(payload["apply_allowed"]) + self.assertFalse(payload["frontdoor_resolvable"]) + self.assertIn("got cancelled", " ".join(payload["apply_blockers"])) + self.assertIn("cancelled; retained omissions", credential_change.render_summary(ccr, warnings)) + with self.assertRaises(SystemExit): + credential_change.command_apply_plan(type("Args", (), {"ref": str(path)})()) + + def test_cancelled_incomplete_request_requires_owner_evidence(self) -> None: + for field in ("owner", "reason", "source_ref"): + with self.subTest(field=field): + path = self.cancelled_incomplete_ccr() + data = credential_change.load_yaml(path) + del data["cancellation"][field] + credential_change.dump_yaml(path, data) + _, errors, _ = credential_change.validate_ccr(path) + self.assertTrue(any(f"cancellation.{field}" in e for e in errors)) + + def test_cancelled_omissions_do_not_bypass_other_guards(self) -> None: + for label in ("frontdoor", "path", "undeclared", "present", "unsupported", "reactivate"): + with self.subTest(label=label): + path = self.cancelled_incomplete_ccr() + data = credential_change.load_yaml(path) + if label == "frontdoor": + data["access_frontdoor"]["readiness"] = "ready" + elif label == "path": + data["openbao"]["kv_path"] = "platform/*" + elif label == "undeclared": + data["in_flight"]["missing_fields"] = ["openbao.auth"] + elif label == "present": + data["openbao"]["auth"] = {"method": "oidc"} + elif label == "unsupported": + data["in_flight"]["missing_fields"].append("target") + else: + data["status"] = "approved" + credential_change.dump_yaml(path, data) + _, errors, _ = credential_change.validate_ccr(path) + self.assertTrue(errors) + def test_core_hub_runtime_lane_is_split_and_agent_denied(self) -> None: path = ( REPO_DIR diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md index 15f24eb..fd1e102 100644 --- a/workplans/RPF-WP-0035-credential-lane-implementation.md +++ b/workplans/RPF-WP-0035-credential-lane-implementation.md @@ -7,7 +7,7 @@ repo: railiance-platform status: blocked owner: codex created: "2026-09-05" -updated: "2026-09-09" +updated: "2026-09-10" related: - RPF-WP-0032 - RPF-WP-0033 @@ -253,6 +253,29 @@ intervention_note: "" state_hub_task_id: "68bff751-e48b-548b-8fb4-dfb3b16210c2" ``` +**Current return, 2026-09-10:** CCR-2026-0020 is cancelled on the requesting +Approval Engine owner's explicit withdrawal, source +`849c75bb094613ff6ac1a1d4cda56a745520c5a1:docs/keycape-service-registrations.md`. +No presenter exists and no reader is wanted. The validator now accepts terminal +cancellation of an incomplete request only with owner/source/reason evidence, +retained declared omissions and a disabled, non-resolvable front door. Apply +remains refused. No live identity, policy, role, custody or verifier was changed. +Human approval follows existing INFD-WP-0001-T07/T08; future service requesters +need a separate narrow registration, not this withdrawn bundle. + +**Remaining unblock:** CCR-2026-0019 needs the exact authorized operator group +from NetKingdom/KeyCape, then reviewed attended file delivery and its scoped +positive/negative proof. Completed CCR-2026-0017/0018 remain closed. T06 stays +`wait`; its historical two-reader notes below are superseded for reader 2. + +Separate retained owner decision: KeyCape's 2026-09-10 return +`21427688-725f-4dea-aab4-7c78fd4328d2` identifies the already-live, unpresented +CCR-2026-0018 registration. Approval Engine, KeyCape and Platform must explicitly +decide retention/expiry or coordinated disablement. Reader cancellation neither +disables that live registration nor accepts indefinite retention. Keep this +disposition in T06; it is separate from the wanted CCR-2026-0019 factory reader. +No unilateral issuer/config/Secret change is authorized by this closeout. + Residual handoff from RPF-WP-0035-T05; consumes the completed verifier custody without extending CCR-2026-0017/0018. Owner: railiance-platform with the named secrets-engine and approval-engine operator consumers. @@ -313,7 +336,8 @@ own accepted native delivery return. Client-side admission is not ready for a new human decision until exact requests and contained execution/rollback checks are reviewable; the completed verifier reviews must not recur in that queue. -**Done when:** both separate consumer admissions are approved and implemented; +**Done when:** each retained consumer admission is approved and implemented, or +its requesting owner explicitly cancels it (CCR-2026-0020 now satisfies that branch); actual consumer authentication/delivery succeeds; sibling/listing/wrong-reader and scope refusals plus reader expiry/revocation/cleanup are evidenced; version-1 custody and verifier availability remain intact. Return the source and metadata