From dc9500e50e628f852b9baa868c950ecc7cb8cbd3 Mon Sep 17 00:00:00 2001 From: Andraxion Date: Wed, 29 Jul 2026 15:47:08 -0400 Subject: [PATCH] Bind generated clients to product version --- schemas/adapter-client-configuration.schema.json | 6 ++++++ schemas/client-configuration.schema.json | 6 ++++++ src/docforge/client_config.py | 10 ++++++++++ tests/test_adapter_launcher.py | 11 +++++++++++ tests/test_client_integration.py | 7 +++++++ 5 files changed, 40 insertions(+) diff --git a/schemas/adapter-client-configuration.schema.json b/schemas/adapter-client-configuration.schema.json index e316c93..abf0804 100644 --- a/schemas/adapter-client-configuration.schema.json +++ b/schemas/adapter-client-configuration.schema.json @@ -173,6 +173,7 @@ "required": [ "status", "schema_version", + "docforge_version", "operation", "action", "client", @@ -194,6 +195,11 @@ "properties": { "status": { "const": "ok" }, "schema_version": { "const": 1 }, + "docforge_version": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, "operation": { "const": "adapter_client.configure" }, "action": { "enum": ["preview", "write"] }, "client": { "enum": ["codex", "claude", "openclaw"] }, diff --git a/schemas/client-configuration.schema.json b/schemas/client-configuration.schema.json index 1cf7c6a..625d62e 100644 --- a/schemas/client-configuration.schema.json +++ b/schemas/client-configuration.schema.json @@ -215,6 +215,7 @@ "required": [ "status", "schema_version", + "docforge_version", "operation", "action", "client", @@ -232,6 +233,11 @@ "properties": { "status": { "const": "ok" }, "schema_version": { "const": 1 }, + "docforge_version": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, "operation": { "const": "client.configure" }, "action": { "enum": ["preview", "write"] }, "client": { "enum": ["codex", "claude", "openclaw"] }, diff --git a/src/docforge/client_config.py b/src/docforge/client_config.py index d02e493..7a435cc 100644 --- a/src/docforge/client_config.py +++ b/src/docforge/client_config.py @@ -16,6 +16,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Literal, cast +from ._version import __version__ as DOCFORGE_VERSION from .adapter_launcher import ( AdapterLauncherV1, AdapterSourceAvailabilityV1, @@ -617,6 +618,8 @@ def _validate_configuration_result( *, trusted_descriptor: ProjectDescriptor | None = None, ) -> None: + if result.get("docforge_version") != DOCFORGE_VERSION: + raise AssertionError("Generated client product version drifted") artifact = cast(dict[str, object], result["artifact"]) binding = cast(dict[str, object], result["binding"]) policy = cast(dict[str, object], result["effective_policy"]) @@ -797,6 +800,7 @@ def _validate_configuration_result( expected_hash = document_hash( { "schema_version": 1, + "docforge_version": result["docforge_version"], "client": result["client"], "server_name": result["server_name"], "project": project, @@ -1041,6 +1045,7 @@ def generate_client_configuration( plan_hash = document_hash( { "schema_version": 1, + "docforge_version": DOCFORGE_VERSION, "client": selected_client, "server_name": selected_name, "project": project_binding, @@ -1061,6 +1066,7 @@ def generate_client_configuration( result: dict[str, object] = { "status": "ok", "schema_version": 1, + "docforge_version": DOCFORGE_VERSION, "operation": "client.configure", "action": "write" if output is not None else "preview", "client": selected_client, @@ -1278,6 +1284,7 @@ def generate_adapter_client_configuration( result: dict[str, object] = { "status": "ok", "schema_version": 1, + "docforge_version": DOCFORGE_VERSION, "operation": "adapter_client.configure", "action": "write" if output is not None else "preview", "client": selected_client, @@ -1351,6 +1358,7 @@ def _adapter_configuration_hash_payload(result: dict[str, object]) -> dict[str, artifact = cast(dict[str, object], result["artifact"]) return { "schema_version": 1, + "docforge_version": result["docforge_version"], "client": result["client"], "server_name": result["server_name"], "project": result["project"], @@ -1375,6 +1383,8 @@ def _validate_adapter_configuration_result( launcher: AdapterLauncherV1, source_availability: AdapterSourceAvailabilityV1, ) -> None: + if result.get("docforge_version") != DOCFORGE_VERSION: + raise AssertionError("Generated adapter client product version drifted") validate_adapter_launcher(project, launcher) descriptor = project.descriptor project_binding = cast(dict[str, object], result["project"]) diff --git a/tests/test_adapter_launcher.py b/tests/test_adapter_launcher.py index e03df33..97b59b7 100644 --- a/tests/test_adapter_launcher.py +++ b/tests/test_adapter_launcher.py @@ -19,6 +19,7 @@ from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from referencing import Registry, Resource +from docforge._version import __version__ from docforge.adapter_contract import AdapterNode, AdapterProject, AdapterProjection from docforge.adapter_launcher import AdapterLauncherV1 from docforge.client_config import ( @@ -188,6 +189,7 @@ class AdapterLauncherTests(unittest.TestCase): ) self.assertEqual(first, second) ADAPTER_CONFIGURATION_VALIDATOR.validate(first) + self.assertEqual(__version__, first["docforge_version"]) self.assertEqual(launcher.launcher_hash, first["launcher_hash"]) self.assertEqual( launcher.launcher_hash, @@ -265,6 +267,15 @@ class AdapterLauncherTests(unittest.TestCase): AdapterLauncherV1(**payload) # type: ignore[arg-type] result = generate_adapter_client_configuration(project, launcher, "codex") + version_drift = json.loads(json.dumps(result)) + version_drift["docforge_version"] = "0.0.0" + with self.assertRaisesRegex(AssertionError, "product version"): + _validate_adapter_configuration_result( + version_drift, + project=project, + launcher=launcher, + source_availability=project_source_availability(version_drift), + ) result["binding"]["args"].append("--arbitrary") with self.assertRaisesRegex(AssertionError, "Generated adapter client"): _validate_adapter_configuration_result( diff --git a/tests/test_client_integration.py b/tests/test_client_integration.py index 539f089..84b7a88 100644 --- a/tests/test_client_integration.py +++ b/tests/test_client_integration.py @@ -19,6 +19,7 @@ from jsonschema import Draft202012Validator from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client +from docforge._version import __version__ from docforge.changeset_contract import document_hash from docforge.cli import _parser, _run, main from docforge.client_config import ( @@ -103,6 +104,7 @@ class ClientIntegrationTests(unittest.TestCase): second = generate_client_configuration(project, client, no_ast=True) self.assertEqual(first, second) Draft202012Validator(CONFIGURATION_SCHEMA).validate(first) + self.assertEqual(__version__, first["docforge_version"]) Draft202012Validator(POLICY_SCHEMA).validate(first["effective_policy"]) self.assertEqual("read", first["binding"]["capability_mode"]) self.assertEqual( @@ -251,6 +253,11 @@ class ClientIntegrationTests(unittest.TestCase): validator.validate(result) _validate_configuration_result(result) + version_drift = json.loads(json.dumps(result)) + version_drift["docforge_version"] = "0.0.0" + with self.assertRaisesRegex(AssertionError, "product version"): + _validate_configuration_result(version_drift) + for field, value in ( ("schema_version", 1), ("manual", "on-demand"),