NK-WP-0048: {param:env} format spec; check follow-through/rollback templates
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Answers ops-warden's observation on the literal KEYCAPE_<CLIENT_ID>_CLIENT_SECRET.
The type adopts the spec once ops-warden's planner renders it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 469893@bnt-lap001
Assistant-Session: 49296f15-2171-4137-b972-2443bab95edb
This commit is contained in:
tegwick 2026-10-09 19:02:56 +02:00
parent 502907a2d3
commit 850907a987
3 changed files with 61 additions and 1 deletions

View file

@ -171,3 +171,28 @@ def test_forgejo_reference_case_is_accepted_and_matches_the_example():
assert {b["role"] for b in case["spec"]["acceptance"]["by"]} == {"owner", "founder"}
assert msgs(v.validate_case(case, TYPE, require_implemented=False)) == []
assert msgs(v.validate_case(case, as_implemented())) == []
def test_env_format_spec_renders_the_keycape_env_name():
# {client_id:env} gives the name keycape-client.py derives (env_name): upper
# case, '-' as '_'. The type adopts it once ops-warden's planner renders it
# too (ops-warden message 06303d1d); until then it keeps the literal form.
t = as_implemented()
for item in t["spec"]["follow_through"] + t["spec"]["rollback"]:
for key in ("entry_point", "action"):
if key in item:
item[key] = item[key].replace("KEYCAPE_<CLIENT_ID>_CLIENT_SECRET", "KEYCAPE_{client_id:env}_CLIENT_SECRET")
assert msgs(v.validate_type(t)) == []
issues, r = v.render_case(with_params(FORGEJO, client_id="my-forge"), t, DECLS)
assert msgs(issues) == []
steps = {s["id"]: s for s in r["follow_through"]}
assert "env KEYCAPE_MY_FORGE_CLIENT_SECRET from sso/keycape-my-forge-client" in steps["bind-env"]["entry_point"]
assert "<" not in steps["bind-env"]["entry_point"]
undo = [s for s in r["rollback"] if s["reverses"] == "bind-env"]
assert "KEYCAPE_MY_FORGE_CLIENT_SECRET" in undo[0]["action"]
def test_unknown_format_spec_is_rejected():
t = copy.deepcopy(TYPE)
t["spec"]["rollback"][0]["action"] += " {client_id:shout}"
assert any("unknown format spec" in m for m in msgs(v.validate_type(t)))

View file

@ -79,6 +79,15 @@ def placeholders(text: str) -> set[str]:
return {f[1] for f in string.Formatter().parse(text) if f[1]}
#: Format specs a template may apply to a parameter. ``env`` renders an
#: environment-variable fragment: upper case, ``-`` as ``_`` ({client_id:env}).
FORMAT_SPECS = {"", "env"}
def format_specs(text: str) -> set[str]:
return {f[2] or "" for f in string.Formatter().parse(text) if f[1]}
def _walk_strings(value: Any, path: str):
if isinstance(value, str):
yield path, value
@ -146,6 +155,9 @@ def validate_type(data: dict[str, Any], type_dir: Path | None = None) -> list[Is
_validate_materialization(spec["materialization"], params, issues)
_validate_verification(spec["verification"], params, issues)
_validate_rollback(spec["rollback"], step_ids, spec["materialization"], issues)
for section in ("follow_through", "rollback"):
if isinstance(spec[section], list):
_validate_templates(spec[section], f"type.spec.{section}", params, issues)
_validate_acceptance(spec["acceptance"], issues)
for section in ("materialization", "follow_through", "verification", "rollback"):
for i, item in enumerate(spec[section] if isinstance(spec[section], list) else []):
@ -241,6 +253,8 @@ def _validate_templates(items: Any, path: str, params: dict[str, dict], issues:
for name in placeholders(text):
if name not in params:
issues.append(err(spath, f"template uses unknown parameter {{{name}}}"))
for spec in sorted(format_specs(text) - FORMAT_SPECS):
issues.append(err(spath, f"template uses unknown format spec {spec!r}"))
def _validate_materialization(items: Any, params: dict[str, dict], issues: list[Issue]) -> None:
@ -446,6 +460,15 @@ def _applicable(items: list[dict], values: dict[str, Any]) -> list[dict]:
return [{k: v for k, v in i.items() if k != "when"} for i in items if holds(i.get("when"), values)]
class _Param(str):
"""A string parameter value that also renders the ``env`` format spec."""
def __format__(self, spec: str) -> str:
if spec == "env":
return self.upper().replace("-", "_")
return str.__format__(self, spec)
class _Blank(dict):
"""Template values where an optional parameter a case leaves out renders empty."""
@ -455,7 +478,8 @@ class _Blank(dict):
def _fill(value: Any, values: dict[str, Any]) -> Any:
if isinstance(value, str):
return value.format_map(_Blank({k: (",".join(map(str, v)) if isinstance(v, list) else v)
return value.format_map(_Blank({k: _Param(",".join(map(str, v)) if isinstance(v, list) else v)
if isinstance(v, (str, list)) else v
for k, v in values.items()}))
if isinstance(value, dict):
return {k: _fill(v, values) for k, v in value.items()}

View file

@ -362,6 +362,17 @@ the pod: `ensure_service_groups` and `revoke_service_groups` present, `/readyz`
deploy/identity-provisioner provisioner=forgejo.coulomb.social/coulomb/identity-provisioner@sha256:ffacd5d790a5b7800e0535f3aa95b7de52c7a68e4f575b9192c3e1622fa672c2`.
The pin in `sso-mfa/k8s/identity-provisioner/deployment.yaml` follows.
2026-10-09: ops-warden closed WARDEN-WP-0044-T01 and shipped `warden case plan`
(T02, message `06303d1d`); the accepted Forgejo case is refused until ops-mason
materializes it, and a materialized copy plans cleanly once
`keycape-client-secret-generate` exists. Its observation that `bind-env` carries
a literal `KEYCAPE_<CLIENT_ID>_CLIENT_SECRET`: the validator now renders a
`{param:env}` format spec (upper case, `-` as `_`, as `keycape-client.py`
derives the name) and rejects unknown specs, and follow-through and rollback
text is now checked for unknown placeholders too (patch-level, canon section 6).
The type keeps the literal until ops-warden's planner (`text.format`) renders the
spec as well; asked of ops-warden. 53 tests pass.
```task
id: NK-WP-0048-T07
status: wait