From 139f77dd557ca4139cd4a8ec4d0ef90e7ca607ee Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 8 Oct 2026 23:21:46 +0200 Subject: [PATCH] Route OpenBao configuration and provisioning away from the structure survey Configuration intent (policies, auth roles) routes to the attended openbao-platform-admin-login lane and value provisioning to openbao-api-key custody; both fail closed when their front door is missing. The survey lane now points at ops-mason's read-only ops-mason-survey grant (MASON-IN-0004). Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 327285@bnt-lap001 Assistant-Session: d6532cd1-7cd5-417b-baac-b841c9103a6b --- WORK-RECORDS.md | 5 ++ registry/generated/high-risk-data-paths.yaml | 4 +- registry/routing/catalog.yaml | 14 ++--- src/warden/routing/catalog.py | 35 ++++++++++++ tests/test_openbao_configuration_routing.py | 60 ++++++++++++++++++++ tests/test_proxy.py | 2 + wiki/playbooks/openbao-structure-survey.md | 6 +- 7 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 tests/test_openbao_configuration_routing.py diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 70d7daf..df1ef76 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -42,6 +42,7 @@ | workplan | WARDEN-WP-0041 | finished | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | | workplan | WARDEN-WP-0042 | finished | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | | workplan | WARDEN-WP-0043 | ready | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| workplan | WARDEN-WP-0044 | ready | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | | task | WARDEN-WP-ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md | | task | WARDEN-WP-ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md | | task | WARDEN-WP-ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md | @@ -177,6 +178,10 @@ | task | WARDEN-WP-0043-T01 | todo | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | | task | WARDEN-WP-0043-T02 | wait | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | | task | WARDEN-WP-0043-T03 | wait | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | +| task | WARDEN-WP-0044-T01 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T02 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T03 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | +| task | WARDEN-WP-0044-T04 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | | intake | WARDEN-IN-0001 | closed | — | intakes/intakes.md | | intake | WARDEN-IN-0002 | closed | — | intakes/intakes.md | | intake | WARDEN-IN-0003 | closed | — | intakes/intakes.md | diff --git a/registry/generated/high-risk-data-paths.yaml b/registry/generated/high-risk-data-paths.yaml index d0200c4..64fe13d 100644 --- a/registry/generated/high-risk-data-paths.yaml +++ b/registry/generated/high-risk-data-paths.yaml @@ -10,11 +10,11 @@ # declares it, and is null where the field set has not been established -- # null means unknown, never 'one field'. -generated_at: "2026-10-05T14:12:49Z" +generated_at: "2026-10-07T22:52:56Z" source: ops-warden/registry/routing/catalog.yaml catalog_revision: "9084581461545e07d26fc29783c9fd79446e54c6" catalog_revision_date: "2026-10-05T16:12:48+02:00" -catalog_dirty: false +catalog_dirty: true high_risk_lane_count: 32 concrete_path_count: 17 diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index 9d441a8..a851b60 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -1430,25 +1430,25 @@ entries: title: OpenBao existing-structure survey for a construction plan (section 2) # Pointer only (WARDEN-WP-0042). Founder decision: ops-warden routes, # ops-mason surveys. No survey code and no token live in ops-warden; the - # survey runs under ops-mason's own scoped ops-mason-build grant. + # survey runs under ops-mason's read-only ops-mason-survey grant (MASON-IN-0004). risk: high workload_ref: applicability: not-applicable - reason: "Attended builder survey under a founder-minted grant; reads OpenBao structure metadata, not a workload credential." - need_keywords: [survey, structure survey, openbao structure, existing structure, existing policies, existing lane, auth roles, approle roles, policy list, auth list, secrets list, construction plan, construction plan section 2, plan-survey, ops-mason-build] + reason: "Attended read-only survey under a founder-minted grant; reads OpenBao structure metadata, not a workload credential." + need_keywords: [survey, structure survey, openbao structure, existing structure, existing policies, existing lane, auth roles, approle roles, policy list, auth list, secrets list, construction plan, construction plan section 2, plan-survey, ops-mason-survey] owner_repo: ops-mason - subsystem: OpenBao structure metadata via ops-mason-build grant + subsystem: OpenBao structure metadata via read-only ops-mason-survey grant warden_executes: false wiki_ref: wiki/playbooks/openbao-structure-survey.md#worker-checklist canon_ref: ops-mason/docs/construction-plan-format.md - reviewed: "2026-10-05" + reviewed: "2026-10-08" status: active delegation: mode: native intended_owner: ops-mason - reviewed: "2026-10-05" + reviewed: "2026-10-08" verified: source-read - auth_method: "founder-minted scoped grant: ops-mason scripts/bao-session.sh grant (policy ops-mason-build, 45m, display_name claude-)" + auth_method: "founder-minted scoped grant: ops-mason scripts/bao-session.sh grant (policy ops-mason-survey, 45m, display_name claude-)" exec_owner: ops-mason exec_command: "ops-mason/scripts/plan-survey.py --kv-path --policy --approle --consumer --keyword " exec_capable: false diff --git a/src/warden/routing/catalog.py b/src/warden/routing/catalog.py index c04f7d7..54af6db 100644 --- a/src/warden/routing/catalog.py +++ b/src/warden/routing/catalog.py @@ -199,6 +199,28 @@ class CatalogFreshness: } +def _openbao_configuration_need(query: str) -> bool: + """Separate explicit control-plane configuration from metadata discovery. + + This is a routing distinction, never approval. Exact lane IDs are handled + first; reads and surveys continue through ordinary catalog ranking. + """ + text = query.lower().replace("-", " ") + control = re.search(r"\b(?:polic(?:y|ies)|auth\s+roles?|token\s+roles?|" + r"database\s*(?:/| )\s*config|secrets?\s+engine)\b", text) + change = re.search(r"\b(?:configure|configuring|create|apply|install|update|" + r"administration|administer)\b", text) + return bool(re.search(r"\bopenbao\b", text) and control and change) + + +def _openbao_provision_need(query: str) -> bool: + """Value provisioning belongs to custody, never the structure survey.""" + text = query.lower().replace("-", " ") + return bool(re.search(r"\bopenbao\b", text) + and re.search(r"\b(?:provision|provisioning|seed|seeding)\b", text) + and re.search(r"\b(?:keys?|credentials?|secrets?|tokens?|values?)\b", text)) + + @dataclass class Catalog: path: Path @@ -232,6 +254,19 @@ class Catalog: scored = [(e.match_score(tokens), e) for e in pool] scored = [(s, e) for s, e in scored if s > 0] scored.sort(key=lambda pair: (-pair[0], pair[1].id)) + if _openbao_provision_need(query): + custody = self.get("openbao-api-key") + if custody is None or not custody.is_active or custody.lane != "secret": + return [] + return ([custody] + [entry for _, entry in scored if entry.id != custody.id])[:limit] + if _openbao_configuration_need(query): + admin = self.get("openbao-platform-admin-login") + # A missing/inactive administration front door must not fall back to + # a survey ceremony or a secret-read transport. + if admin is None or not admin.is_active or admin.lane != "login": + return [] + ranked = [admin] + [entry for _, entry in scored if entry.id != admin.id] + return ranked[:limit] return [e for _, e in scored[:limit]] def stale( diff --git a/tests/test_openbao_configuration_routing.py b/tests/test_openbao_configuration_routing.py new file mode 100644 index 0000000..d85efee --- /dev/null +++ b/tests/test_openbao_configuration_routing.py @@ -0,0 +1,60 @@ +"""Configuration intent must never inherit a metadata survey's founder act.""" +from pathlib import Path + +import pytest + +from warden.plan import build_plan +from warden.routing.catalog import Catalog, load_catalog + +ROOT = Path(__file__).resolve().parents[1] +NEED = ('attended OpenBao platform administration to configure read policies and auth roles ' + 'for Coulomb and email backup delivery') + + +@pytest.fixture +def catalog(monkeypatch): + monkeypatch.setenv('WARDEN_POSTURE_CATALOG', str(ROOT / 'registry/policy/security-posture.yaml')) + return load_catalog(ROOT / 'registry/routing/catalog.yaml') + + +@pytest.mark.parametrize('need', [NEED, + 'configure OpenBao policies and Kubernetes auth roles for backup delivery', + 'apply OpenBao read policy for an explicitly reviewed service account']) +def test_configuration_requires_attended_admin_identity(catalog, need): + plan = build_plan(need, catalog=catalog) + assert plan.lane_id == 'openbao-platform-admin-login' + assert plan.verdict == 'founder_required' and plan.founder_act.kind == 'oidc_login' + assert all('--fetch' not in command and '--out' not in command for command in plan.commands) + + +@pytest.mark.parametrize('need', [ + 'survey existing OpenBao policies and roles for backup delivery', + 'OpenBao read-only metadata survey policy approle consumer backup', + 'inspect OpenBao structure metadata policy approle']) +def test_survey_remains_metadata_only(catalog, need): + plan = build_plan(need, catalog=catalog) + assert plan.lane_id == 'openbao-structure-survey' + assert plan.founder_act.kind == 'approve' + + +def test_explicit_lane_id_still_wins_and_missing_admin_fails_closed(catalog): + assert catalog.find('openbao-structure-survey')[0].id == 'openbao-structure-survey' + unavailable = Catalog(catalog.path, [entry for entry in catalog.entries + if entry.id != 'openbao-platform-admin-login']) + plan = build_plan(NEED, catalog=unavailable) + assert plan.verdict == 'unroutable' and plan.lane_id is None + assert not plan.commands + + +def test_pair_provisioning_routes_to_custody_not_survey(catalog): + need = "provision a Scaleway object storage backup publisher key pair into OpenBao custody" + plan = build_plan(need, catalog=catalog) + assert plan.lane_id == "openbao-api-key" + assert plan.verdict == "founder_required" + assert plan.founder_act.kind == "paste_once_provision" + assert not any("plan-survey" in command for command in plan.commands) + + +def test_missing_custody_frontdoor_fails_closed(catalog): + unavailable = Catalog(catalog.path, [e for e in catalog.entries if e.id != "openbao-api-key"]) + assert build_plan("provision OpenBao backup key pair", catalog=unavailable).verdict == "unroutable" diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 72fa6c7..065f09a 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -652,6 +652,8 @@ def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch): "warden.proxy.subprocess.run", lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite stdout guard")), ) + # This case tests the human stdout guard; the agent guard has its own tests. + monkeypatch.setattr("warden.access.agent_read_boundary_identity", lambda: "") # CliRunner captures stdout (not a tty), so the guard trips without --unsafe-stdout. r = runner.invoke(app, ["access", "whynot-design-npm-publish", "--fetch"]) assert r.exit_code == 6 diff --git a/wiki/playbooks/openbao-structure-survey.md b/wiki/playbooks/openbao-structure-survey.md index 62f17b5..c245386 100644 --- a/wiki/playbooks/openbao-structure-survey.md +++ b/wiki/playbooks/openbao-structure-survey.md @@ -2,11 +2,11 @@ Catalog: `openbao-structure-survey` (status `active`, pointer only) Owner: `ops-mason` — `scripts/plan-survey.py`, `docs/construction-plan-format.md` § 2 -Routing record: `WARDEN-WP-0042`; owner work: `ops-mason/MASON-WP-0008` +Routing records: `WARDEN-WP-0042`, `WARDEN-WP-0043`; owner change: `ops-mason/MASON-IN-0004` (2026-10-07) ops-warden routes this need and does nothing else. It holds no survey code and no OpenBao token; the survey runs under ops-mason's own scoped -`ops-mason-build` grant, which reads structure metadata and is denied every +`ops-mason-survey` read-only grant, which reads structure metadata and is denied every secret value. ## Worker checklist @@ -22,3 +22,5 @@ secret value. Anything the grant refuses is ops-mason's to resolve with the founder; do not reach for a broader session through ops-warden. + +The builder grant was retired by MASON-IN-0004. Surveys cannot apply policies or auth roles; reviewed structure changes belong to railiance-platform's delegated metadata applier.