diff --git a/overrides/main.html b/overrides/main.html
index 1210646..0374e4a 100644
--- a/overrides/main.html
+++ b/overrides/main.html
@@ -10,12 +10,14 @@
#}
{% block extrahead %}
{{ super() }}
+ {# page.title is the nav label ("Home"); page.meta.title is the front matter. #}
+ {% set page_name = (page.meta.title if page and page.meta and page.meta.title else (page.title if page else None)) %}
{% set page_desc = page.meta.description if page and page.meta and page.meta.description else config.site_description %}
{% set page_url = page.canonical_url if page and page.canonical_url else config.site_url %}
-
+
@@ -24,7 +26,7 @@
-
+
diff --git a/src/cmcp_runtime/cli.py b/src/cmcp_runtime/cli.py
index 01a22cf..f0e2ab2 100644
--- a/src/cmcp_runtime/cli.py
+++ b/src/cmcp_runtime/cli.py
@@ -16,6 +16,31 @@
from cmcp_runtime.startup import RuntimeContext
+def _marker(preferred: str, fallback: str, stream: object) -> str:
+ """Return `preferred` if `stream` can encode it, else `fallback`.
+
+ A Windows console defaults to a legacy code page, and writing "✓" to
+ it raises UnicodeEncodeError. That killed `cmcp validate-config` on its
+ own success path: the config was valid, and the command still exited
+ non-zero with a codec traceback. The marker is decoration, so it degrades
+ rather than taking the command down with it.
+ """
+ encoding = getattr(stream, "encoding", None) or "ascii"
+ try:
+ preferred.encode(encoding)
+ except (UnicodeEncodeError, LookupError):
+ return fallback
+ return preferred
+
+
+def _ok() -> str:
+ return _marker("✓", "OK", sys.stdout)
+
+
+def _bad() -> str:
+ return _marker("✗", "ERROR", sys.stderr)
+
+
def build_server(ctx: RuntimeContext) -> MCPServer:
"""
Compose the running gateway from a validated RuntimeContext.
@@ -249,11 +274,12 @@ def validate_config(config: str) -> None:
try:
load_config(config)
- click.echo(f"✓ Config valid: {config}")
except Exception as exc:
- click.echo(f"✗ Config invalid: {exc}", err=True)
+ click.echo(f"{_bad()} Config invalid: {exc}", err=True)
raise SystemExit(1) from exc
+ click.echo(f"{_ok()} Config valid: {config}")
+
@main.command("validate-bundle")
@click.option("--bundle-path", required=True, type=click.Path(exists=True))
@@ -265,7 +291,7 @@ def validate_bundle(bundle_path: str, expected_hash: str) -> None:
try:
bundle = load_policy_bundle(bundle_path)
except Exception as exc:
- click.echo(f"✗ Bundle load error: {exc}", err=True)
+ click.echo(f"{_bad()} Bundle load error: {exc}", err=True)
raise SystemExit(1) from exc
bundle_hash = bundle.bundle_hash
@@ -274,10 +300,10 @@ def validate_bundle(bundle_path: str, expected_hash: str) -> None:
actual_hex = bundle_hash.removeprefix("sha256:")
if actual_hex == expected_hex:
- click.echo(f"✓ Bundle valid: {bundle_hash}")
+ click.echo(f"{_ok()} Bundle valid: {bundle_hash}")
else:
click.echo(
- f"✗ Bundle hash mismatch: expected {expected_hash}, got {bundle_hash}",
+ f"{_bad()} Bundle hash mismatch: expected {expected_hash}, got {bundle_hash}",
err=True,
)
raise SystemExit(1)
diff --git a/tests/unit/test_cli_output_encoding.py b/tests/unit/test_cli_output_encoding.py
new file mode 100644
index 0000000..6ced063
--- /dev/null
+++ b/tests/unit/test_cli_output_encoding.py
@@ -0,0 +1,128 @@
+"""
+Regression tests for CLI status markers on a legacy console.
+
+`cmcp validate-config` printed "✓ Config valid: ..." from inside the same
+try block that ran the validation. On a Windows console, whose default code
+page cannot encode that character, the echo raised UnicodeEncodeError, the
+`except Exception` caught it, and the command reported
+
+ ✗ Config invalid: 'charmap' codec can't encode character '✓' ...
+
+then exited 1. The config was valid. A user following the published quickstart
+was told their config was broken because the tool could not draw a tick.
+"""
+
+from __future__ import annotations
+
+import pytest
+from click.testing import CliRunner
+
+from cmcp_runtime.cli import _bad, _marker, _ok, main
+
+
+class _Stream:
+ """Minimal stand-in for sys.stdout with a fixed encoding."""
+
+ def __init__(self, encoding: str | None) -> None:
+ self.encoding = encoding
+
+
+@pytest.mark.parametrize(
+ ("encoding", "expected"),
+ [
+ ("utf-8", "✓"),
+ ("UTF-8", "✓"),
+ ("cp1252", "OK"),
+ ("ascii", "OK"),
+ ("cp437", "OK"),
+ (None, "OK"),
+ ("not-a-real-codec", "OK"),
+ ],
+)
+def test_marker_falls_back_when_the_stream_cannot_encode_it(encoding, expected):
+ assert _marker("✓", "OK", _Stream(encoding)) == expected
+
+
+def test_marker_never_raises_on_an_object_without_encoding():
+ assert _marker("✓", "OK", object()) == "OK"
+
+
+def test_ok_and_bad_return_something_printable():
+ assert _ok() in ("✓", "OK")
+ assert _bad() in ("✗", "ERROR")
+
+
+def _write_valid_config(tmp_path):
+ config = tmp_path / "cmcp-config.yaml"
+ config.write_text(
+ "attestation:\n"
+ " provider: auto\n"
+ " enforcement_mode: enforcing\n"
+ "policy_bundle_path: ./policies/\n"
+ "catalog_path: ./catalog.json\n"
+ 'listen_addr: "127.0.0.1:8443"\n',
+ encoding="utf-8",
+ )
+ return config
+
+
+def test_validate_config_succeeds_on_a_legacy_code_page(tmp_path):
+ """A valid config must validate where the tick cannot be drawn."""
+ config = _write_valid_config(tmp_path)
+ result = CliRunner(charset="cp1252").invoke(
+ main, ["validate-config", "--config", str(config)]
+ )
+ assert result.exit_code == 0, result.output
+ assert "Config valid" in result.output
+ assert "Config invalid" not in result.output
+
+
+def test_validate_config_keeps_the_tick_where_utf8_is_available(tmp_path):
+ config = _write_valid_config(tmp_path)
+ result = CliRunner(charset="utf-8").invoke(
+ main, ["validate-config", "--config", str(config)]
+ )
+ assert result.exit_code == 0, result.output
+ assert "✓ Config valid" in result.output
+
+
+def test_an_actually_invalid_config_still_fails(tmp_path):
+ """The fallback must not turn a real validation failure into a pass."""
+ config = tmp_path / "cmcp-config.yaml"
+ config.write_text("attestation: [this is not a mapping" + chr(10), encoding="utf-8")
+ result = CliRunner(charset="cp1252").invoke(
+ main, ["validate-config", "--config", str(config)]
+ )
+ assert result.exit_code == 1
+ assert "Config invalid" in result.output
+
+
+def test_a_failed_success_message_is_never_relabelled_as_invalid(tmp_path, monkeypatch):
+ """The success echo must sit outside the try that runs the validation.
+
+ A real Windows console raises UnicodeEncodeError from the write itself.
+ When the success echo lived inside the try, `except Exception` caught that
+ and printed "Config invalid: 'charmap' codec can't encode ...", so a valid
+ config was reported broken. Here the first echo raises and the test asserts
+ the command never claims the config is invalid.
+ """
+ import cmcp_runtime.cli as cli
+
+ config = _write_valid_config(tmp_path)
+ seen: list[str] = []
+ calls = {"n": 0}
+
+ def flaky_echo(message="", *args, **kwargs):
+ calls["n"] += 1
+ if calls["n"] == 1:
+ raise UnicodeEncodeError("charmap", "✓", 0, 1, "unmapped")
+ seen.append(str(message))
+
+ monkeypatch.setattr(cli.click, "echo", flaky_echo)
+
+ with pytest.raises(UnicodeEncodeError):
+ cli.validate_config.callback(config=str(config))
+
+ assert not any("Config invalid" in m for m in seen), (
+ "a printing failure was relabelled as a validation failure: %r" % seen
+ )