From c8c5fbabf83c290eeb8aac15f64996e05ccbd553 Mon Sep 17 00:00:00 2001 From: arpan Date: Fri, 4 Sep 2026 03:14:33 +0530 Subject: [PATCH] release: deprecate --principal-from-client-info, and harden the publish gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --principal-from-client-info now names its replacement wherever an operator meets it: the startup warning, the --help line, and a CHANGELOG Deprecated section. Removed in 0.3; use --principal-header. It is worth stating why it exists at all, because the reason expires rather than weakens. A v0.1 policy cannot address the principal — SPEC-v0.1 §3.2 refuses agent_eq and every other reserved name at load — so an unauthenticated principal misattributes evidence and cannot widen an outcome. v0.3's authority model makes the principal an authorization input, at which point a self-reported name cannot be one. The publish workflow's sdist gate installed only [dev], so the gateway, ACS, elicitation and OTel modules skipped on exactly the artifacts about to be uploaded — a weaker check than the one on main, guarding an upload to a version number that can never be reused. It installs the extras now. --- .github/workflows/publish.yml | 5 ++++- CHANGELOG.md | 12 ++++++++++++ src/ctrlrun/cli/main.py | 3 ++- src/ctrlrun/gateway/server.py | 6 ++++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 41ac2f35..22aae211 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -60,7 +60,10 @@ jobs: run: | mkdir -p /tmp/sdist && tar xzf dist/*.tar.gz -C /tmp/sdist --strip-components=1 cd /tmp/sdist - pip install ".[dev]" + # With the extras: without them the gateway, ACS, elicitation and OTel modules + # skip, and this gate guards an upload to a version number that can never be + # reused. It should be no weaker than the one on main. + pip install ".[dev,gateway,otel]" pytest -q - name: The quick start works from the wheel diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fcede8c..d31564e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,18 @@ Everything below ships. `pip install ctrlrun` still installs nothing but `pyyaml stable interface. The v0.1.0 schemas say otherwise, so the adapter ships. The no-claim rule is untouched. +### Deprecated + +- **`ctrlrun gateway --principal-from-client-info` — removed in 0.3. Use + `--principal-header`.** It takes the agent's name from `_meta["io.modelcontextprotocol/ + clientInfo"]`, which the MCP revision says is self-reported and *"SHOULD NOT"* be relied on + for security decisions. It is offerable in 0.2 only because of a fact that stops being true: + a v0.1 policy cannot address the principal at all (`SPEC-v0.1.md` §3.2 refuses `agent_eq` + and every other reserved name at load), so an unauthenticated principal misattributes + evidence and cannot widen an outcome. v0.3's authority model makes the principal an + authorization input, at which point a self-reported name cannot be one. The flag warns at + startup and its `--help` says so. + ### Removed - `SQLiteStateStore.journal`, and the `EventLog` class behind it. `JSONLEventSink` is that diff --git a/src/ctrlrun/cli/main.py b/src/ctrlrun/cli/main.py index ca565bfe..4670dc98 100644 --- a/src/ctrlrun/cli/main.py +++ b/src/ctrlrun/cli/main.py @@ -452,7 +452,8 @@ def _approval_dict(record: ApprovalRecord) -> dict[str, Any]: @click.option( "--principal-from-client-info", is_flag=True, - help="Take it from clientInfo, which the protocol does not verify. Removed in v0.3.", + help="DEPRECATED, removed in 0.3 — use --principal-header. Takes the agent from " + "clientInfo, which the protocol does not verify.", ) @click.option("--user-header", default=None, help="Take principal.user from this header.") @click.option("--environment", default="production", show_default=True) diff --git a/src/ctrlrun/gateway/server.py b/src/ctrlrun/gateway/server.py index afda860f..a3bec4e2 100644 --- a/src/ctrlrun/gateway/server.py +++ b/src/ctrlrun/gateway/server.py @@ -900,8 +900,10 @@ def serve_forever(gateway: Gateway) -> None: # address the principal at all, so an unauthenticated one misattributes evidence and # cannot widen an outcome. It is removed in v0.3. _LOG.warning( - "--principal-from-client-info trusts a self-reported name; it is attribution on " - "a receipt, never an authorization input, and it is removed in v0.3" + "--principal-from-client-info is DEPRECATED and will be removed in 0.3; use " + "--principal-header instead. It trusts a name the protocol does not verify, " + "which is attribution on a receipt and never an authorization input — and v0.3's " + "authority model makes the principal an authorization input (SPEC-v0.2 §6.5)" ) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start()