diff --git a/pep-stance.yaml b/pep-stance.yaml index 6d43550..370db6f 100644 --- a/pep-stance.yaml +++ b/pep-stance.yaml @@ -98,7 +98,7 @@ on_apply: # 2026-10-08: platform-supplier-credentials added as a not-applicable path pattern, # so routing not_applicable rose 16 -> 17. classification_coverage: - measured: "2026-10-08" + measured: "2026-10-09" attribution: self-measured signing_targets: resolved: 0 @@ -106,7 +106,7 @@ classification_coverage: not_applicable: 1 routing_lanes: resolved: 4 - unknown: 21 + unknown: 22 not_applicable: 17 verdict_caching: none diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index 709005a..e9d68ef 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -1324,7 +1324,7 @@ entries: workload_ref: applicability: applicable unknown_reason: "audit-core has not published an authoritative workload identity declaration." - need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS] + need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS, tenant-engine audit-core sender] owner_repo: ops-mason subsystem: OpenBao + audit-core warden_executes: false @@ -1335,8 +1335,14 @@ entries: delegation: mode: native intended_owner: ops-mason - reviewed: "2026-08-15" + reviewed: "2026-10-09" verified: unverified + # Per-sender projections. Each is verified on its own and none activates the + # registry lane. tenant-engine: KV platform/workloads/tenant-engine/audit-core-sender + # (v1), read policy workload-kv-read-tenant-engine-audit-core-sender-eso (exact + # read, no registry path), ESO-delivered; verified by ops-mason 2026-10-09 + # (MASON-WP-0007-T06, message e7e866bf). + consumers: [tenant-engine] auth_method: "in-cluster generated Secret audit-core-senders; later OpenBao KV via Mason wrap-migrate" path_template: "platform/workloads/audit-core/senders" policy_ref: "external-secrets-audit-core" @@ -1490,3 +1496,37 @@ entries: - "Re-mint the credential at the supplier, written to a mode-0600 file and never printed." - "Write it through a railiance-platform delegated credential change to platform/suppliers// (value from the file, never on argv); revoke the predecessor at the supplier." - "Verify with capabilities and metadata only (`bao token capabilities`, `bao kv metadata get`), then tell readers to re-fetch." + + - id: fluid-telegram-operator + title: fluid-telegram Hall of Helix operator KV lane (attended OIDC) + # Pointer only (MASON-WP-0005-T06, proposal 856bd6ba, 2026-10-09). Built by + # ops-mason as CCR-2026-0039; ops-mason verified the policy text and role: + # exact four-path matrix, role bound to fluid-telegram-operators at aal2, and + # 8 agent denies present. ops-mason proposed `applied-pending-verify`; the + # catalog has only draft/active, so it stays draft until the owner-run checks + # pass: member capabilities and non-member refusal + # (ops-mason scripts/verify-fluid-telegram-lane.sh), plus the salt + # create-once probe (FT-WP-0002). Then ops-warden activates it. The adapter + # runtime lane is separate (MASON-IN-0003): bot-token and redaction-salt only. + risk: high + workload_ref: + applicability: applicable + unknown_reason: "fluid-telegram has not published an authoritative workload identity declaration." + need_keywords: [fluid-telegram, hall-of-helix, telegram operator, telegram bot, mtproto, operator-session, bot-token, redaction-salt, fluid-telegram-operators] + owner_repo: ops-mason + subsystem: OpenBao netkingdom OIDC + platform KV (fluid-telegram campaign) + warden_executes: false + wiki_ref: wiki/playbooks/fluid-telegram-operator.md#worker-checklist + canon_ref: ops-mason/plans/fluid-telegram-operator-credential-lane.md + reviewed: "2026-10-09" + status: draft + delegation: + mode: native + intended_owner: ops-mason + reviewed: "2026-10-09" + verified: owner-confirmed + auth_method: "bao login -method=oidc -path=netkingdom role=fluid-telegram-hall-of-helix-workload-kv (members of fluid-telegram-operators, aal2, 15m token)" + path_template: "platform/workloads/coulomb/fluid-telegram/hall-of-helix/{operator-app,operator-session,bot-token,redaction-salt}" + exec_capable: false + resolvable: false + lane: login diff --git a/src/warden/case_plan.py b/src/warden/case_plan.py index d0a2293..15cb14b 100644 --- a/src/warden/case_plan.py +++ b/src/warden/case_plan.py @@ -13,6 +13,7 @@ parameter whose type declares ``sensitivity: secret`` is refused outright. """ from __future__ import annotations +import hashlib import re import string from dataclasses import dataclass, field @@ -56,10 +57,12 @@ class CasePlan: skipped: List[str] = field(default_factory=list) attended_sessions: List[str] = field(default_factory=list) verification: List[dict] = field(default_factory=list) + case_ref: Optional[str] = None def as_dict(self) -> dict: return { "case": self.case_id, + "case_ref": self.case_ref, "type": self.type_ref, "steps": [s.as_dict() for s in self.steps], "skipped": self.skipped, @@ -76,6 +79,15 @@ def load_case(path: Path) -> dict: return doc +def case_ref(case_id: str, case_bytes: bytes) -> str: + """``@sha256:`` over the case file's exact bytes. + + The reference an owner entry point records for a run (RPF-WP-0054-T01). The + owner re-derives it from the file rather than trusting the caller. + """ + return f"{case_id}@sha256:{hashlib.sha256(case_bytes).hexdigest()}" + + def default_type_path(case_path: Path, type_ref: str) -> Path: """net-kingdom layout: security-cases/.yaml beside security-case-types//type.yaml.""" type_id = type_ref.split("@", 1)[0] @@ -94,14 +106,31 @@ def _applies(when: Optional[str], params: Dict[str, str]) -> bool: return str(params[name]).lower() == expected.lower() +#: Format specs a template may apply, matching net-kingdom's validator +#: (``FORMAT_SPECS``): ``{client_id:env}`` is upper case with ``-`` as ``_``. +FORMAT_SPECS = ("", "env") + + +class _Param(str): + def __format__(self, spec: str) -> str: + if spec == "env": + return self.upper().replace("-", "_") + return str.__format__(self, spec) + + def _render(text: Optional[str], params: Dict[str, str], where: str) -> Optional[str]: if text is None: return None - names = {f for _, f, _, _ in string.Formatter().parse(text) if f} - missing = sorted(names - set(params)) + fields = [(f, spec or "", conv) for _, f, spec, conv in string.Formatter().parse(text) if f] + bad = sorted({f"{f}:{spec}" for f, spec, _ in fields if spec not in FORMAT_SPECS}) + if bad: + raise CaseError(f"{where}: unknown format spec in {bad}; only {{param:env}} is supported") + if any(conv for _, _, conv in fields): + raise CaseError(f"{where}: conversions such as !r are not supported in templates") + missing = sorted({f for f, _, _ in fields} - set(params)) if missing: raise CaseError(f"{where}: parameters {missing} are not set by the case") - return text.format(**params) + return text.format_map({k: _Param(v) for k, v in params.items()}) def plan_case(case_doc: dict, type_doc: dict, catalog: Optional[Catalog]) -> CasePlan: @@ -178,7 +207,10 @@ def _line(f: Finding) -> str: def format_plan(plan: CasePlan) -> str: labels = {"warden": "warden (caller proxy)", "attended": "ATTENDED", "owner": "owner"} - lines = [f"case {plan.case_id} ({plan.type_ref}): plan only, nothing executed", ""] + lines = [f"case {plan.case_id} ({plan.type_ref}): plan only, nothing executed"] + if plan.case_ref: + lines.append(f"case-ref: {plan.case_ref}") + lines.append("") for n, s in enumerate(plan.steps, 1): lane = f" lane={s.lane}" if s.lane else "" lines.append(f"{n}. {s.id} [{labels[s.mode]}] owner={s.owner}{lane} values={s.value_handling}") diff --git a/src/warden/cli.py b/src/warden/cli.py index 50569bc..09eca15 100644 --- a/src/warden/cli.py +++ b/src/warden/cli.py @@ -2255,13 +2255,14 @@ def case_plan( output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False, ) -> None: """Print a case's run (warden, attended, owner steps and verification); executes nothing (exit 1 if refused).""" - from warden.case_plan import CaseError, default_type_path, format_plan, load_case, plan_case + from warden.case_plan import CaseError, case_ref, default_type_path, format_plan, load_case, plan_case from warden.case_type import load_type try: case_doc = load_case(case_file) type_path = type_file or default_type_path(case_file, str((case_doc.get("spec") or {}).get("type", ""))) plan = plan_case(case_doc, load_type(type_path), _load_catalog()) + plan.case_ref = case_ref(plan.case_id, case_file.read_bytes()) except CaseError as exc: err.print(str(exc), markup=False) raise typer.Exit(1) @@ -2271,4 +2272,4 @@ def case_plan( if output_json: print(json.dumps(plan.as_dict(), indent=2)) else: - console.print(format_plan(plan), markup=False) + console.print(format_plan(plan), markup=False, soft_wrap=True) diff --git a/tests/test_case_plan.py b/tests/test_case_plan.py index 61b196b..88ba871 100644 --- a/tests/test_case_plan.py +++ b/tests/test_case_plan.py @@ -34,7 +34,8 @@ TYPE = { {"id": "mint", "owner": "o", "entry_point": "mint {client_id}", "auth": "caller", "lane": "key-cape-oidc-login", "warden_executes": True, "attended": False, "value_handling": "generated_in_custody", "when": "client_type == confidential"}, - {"id": "login", "owner": "o", "entry_point": "apply {client_id}", "auth": "operator", + {"id": "login", "owner": "o", "entry_point": "apply {client_id} env KEYCAPE_{client_id:env}_X", + "auth": "operator", "lane": "key-cape-oidc-login", "warden_executes": False, "attended": True, "value_handling": "none", "attended_justification": "live registry is a Secret"}, {"id": "handover", "owner": "{consumer}", "entry_point": "use {issuer}", "auth": "a", @@ -158,6 +159,21 @@ def test_refuse_secret_parameter(catalog): plan_case(_case(parameters=dict(CASE["spec"]["parameters"], pw="hunter2")), doc, catalog) +def test_env_format_spec_renders_like_keycape_client(catalog): + params = dict(CASE["spec"]["parameters"], client_id="my-app") + plan = plan_case(_case(parameters=params), _type(), catalog) + login = next(s for s in plan.steps if s.id == "login") + assert login.entry_point == "apply my-app env KEYCAPE_MY_APP_X" + + +@pytest.mark.parametrize("template", ["{client_id:upper}", "{client_id:>9}", "{client_id!r}"]) +def test_refuse_unknown_format_spec_or_conversion(catalog, template): + doc = _type() + doc["spec"]["follow_through"][0]["entry_point"] = template + with pytest.raises(CaseError, match="format spec|conversions"): + plan_case(_case(), doc, catalog) + + def test_refuse_unsupported_condition(catalog): doc = _type() doc["spec"]["follow_through"][0]["when"] = "client_type in (a, b)" @@ -173,6 +189,9 @@ def test_cli_plan_shows_attended_and_exit_codes(tmp_path): ok = runner.invoke(app, ["case", "plan", str(case_file), "--type", str(type_file)]) assert ok.exit_code == 0, ok.output assert "ATTENDED" in ok.output and "nothing executed" in ok.output + import hashlib + digest = hashlib.sha256(case_file.read_bytes()).hexdigest() + assert f"case-ref: c@sha256:{digest}" in ok.output case_file.write_text(yaml.safe_dump(_case(status="accepted"))) refused = runner.invoke(app, ["case", "plan", str(case_file), "--type", str(type_file)]) assert refused.exit_code == 1 diff --git a/tests/test_workload_join.py b/tests/test_workload_join.py index 24cd918..ecca720 100644 --- a/tests/test_workload_join.py +++ b/tests/test_workload_join.py @@ -13,7 +13,7 @@ def test_repo_catalog_uses_only_explicit_workload_references(): report = build(ROOT / "registry/routing/catalog.yaml", Path.home()) assert report["ok"] is True assert len(report["resolved"]) == 4 - assert len(report["unknown"]) == 21 + assert len(report["unknown"]) == 22 # 15 since 2026-09-21: the layer-declaration route is repository conformance # guidance, not a workload delivery lane, and the two NetKingdom SSO lanes # (c374d41) remain provider/control-plane credentials. Lanes added after @@ -24,6 +24,8 @@ def test_repo_catalog_uses_only_explicit_workload_references(): # under a founder-minted grant, not a workload credential. 17 since # 2026-10-08: platform-supplier-credentials (WARDEN-WP-0043) is a generic # supplier-account path pattern; concrete consumers keep their own lanes. + # unknown 22 since 2026-10-09: fluid-telegram-operator (MASON-WP-0005-T06) + # names fluid-telegram, which has published no workload declaration. assert len(report["not_applicable"]) == 17 assert {row["lane"] for row in report["resolved"]} == { "ops-warden-warden-sign-token", diff --git a/wiki/playbooks/audit-core-senders.md b/wiki/playbooks/audit-core-senders.md index 77c32cb..b3676be 100644 --- a/wiki/playbooks/audit-core-senders.md +++ b/wiki/playbooks/audit-core-senders.md @@ -15,5 +15,15 @@ First deploy mints sender tokens in-cluster into Secret `platform/workloads/audit-core/senders` is the later authority, filled by a Mason wrap-migrate — not by a founder `bao kv put`. +## Sender projections + +A sender that needs its own token gets an exact-path projection of it. That is a +separate reader of a single sender's token, never read access to the registry. +Each projection is verified on its own and does not activate the registry lane. + +| Sender | KV path | Reader | Verified | +| --- | --- | --- | --- | +| tenant-engine | `platform/workloads/tenant-engine/audit-core-sender` | policy `workload-kv-read-tenant-engine-audit-core-sender-eso`, role `tenant-engine-audit-core-sender-eso` (SA `tenant-engine/tenant-engine-eso`), ExternalSecret `tenant-engine/tenant-engine-audit-core-sender` | 2026-10-09, ops-mason MASON-WP-0007-T06 (metadata and policy text only) | + Never place a sender token, bearer, or `senders.json` value in Git, State Hub, logs, or chat. diff --git a/wiki/playbooks/fluid-telegram-operator.md b/wiki/playbooks/fluid-telegram-operator.md new file mode 100644 index 0000000..2e81897 --- /dev/null +++ b/wiki/playbooks/fluid-telegram-operator.md @@ -0,0 +1,26 @@ +# fluid-telegram operator lane (Hall of Helix) + +Catalog: `fluid-telegram-operator` (status `draft`, pointer only) +Owner: `ops-mason` (construction plan `ops-mason/plans/fluid-telegram-operator-credential-lane.md`, CCR-2026-0039) +Consumer: `fluid-telegram` attended provisioner (FT-WP-0002) + +## Worker checklist + +This page is a pointer. ops-warden does not issue, read, or hold these values. + +1. You must be a member of the LLDAP group `fluid-telegram-operators`, signed + in at aal2 (MFA). +2. Log in attended: + `bao login -no-print -method=oidc -path=netkingdom role=fluid-telegram-hall-of-helix-workload-kv`. + The token lives 15 minutes. Agents are denied on these paths. +3. Run the fluid-telegram provisioner per FT-WP-0002. It reads the operator + app entry, writes the MTProto session and bot token, and creates + `redaction-salt` once. +4. Verify with `bao token capabilities` only, never by reading a value. The + owner-run check is `ops-mason/scripts/verify-fluid-telegram-lane.sh`. + +The bot adapter's runtime read of `bot-token` and `redaction-salt` is a +separate lane (MASON-IN-0003). It never uses this attended role. + +Status: draft until the owner-run member and non-member checks and the salt +create-once probe pass. ops-warden then activates the entry. diff --git a/workplans/WARDEN-WP-0044-security-case-follow-through.md b/workplans/WARDEN-WP-0044-security-case-follow-through.md index 538de99..23e52d2 100644 --- a/workplans/WARDEN-WP-0044-security-case-follow-through.md +++ b/workplans/WARDEN-WP-0044-security-case-follow-through.md @@ -108,7 +108,7 @@ Tests: refuse an unaccepted or unmaterialized case; refuse a step whose catalog entry is missing or not reviewed; show attended steps explicitly. 2026-10-09: done. `warden case plan [--type] [--json]` -(`src/warden/case_plan.py`, 15 tests in `tests/test_case_plan.py`). It refuses: +(`src/warden/case_plan.py`, 14 tests in `tests/test_case_plan.py`). It refuses: - a case that is not `materialized`, or has no acceptance record - a type that is mismatched or not acknowledged - any `case check-type` finding, including a missing or unreviewed lane @@ -130,6 +130,16 @@ Observation for net-kingdom (not blocking): `bind-env` runs before `KEYCAPE__CLIENT_SECRET`, which is not a `{param}` placeholder, so it reaches the plan unrendered. +2026-10-09 follow-up: +- **Format specs.** `_render` supports net-kingdom's `{param:env}` spec (upper + case, `-` as `_`, the same as `keycape-client.py env_name`) and refuses any + other spec or `!conversion`. +- **Case reference.** The plan prints `case-ref: @sha256:` over the + case file's exact bytes. This is the reference proposed to railiance-platform + for `credential.py generate --case-ref` (RPF-WP-0054-T01); the broker + re-derives it from the file. +- 18 tests now. + ```task id: WARDEN-WP-0044-T03 status: wait