1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Bind generated clients to product version

This commit is contained in:
Andraxion 2026-07-29 15:47:08 -04:00
parent 47ac329947
commit dc9500e50e
5 changed files with 40 additions and 0 deletions

View file

@ -173,6 +173,7 @@
"required": [ "required": [
"status", "status",
"schema_version", "schema_version",
"docforge_version",
"operation", "operation",
"action", "action",
"client", "client",
@ -194,6 +195,11 @@
"properties": { "properties": {
"status": { "const": "ok" }, "status": { "const": "ok" },
"schema_version": { "const": 1 }, "schema_version": { "const": 1 },
"docforge_version": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"operation": { "const": "adapter_client.configure" }, "operation": { "const": "adapter_client.configure" },
"action": { "enum": ["preview", "write"] }, "action": { "enum": ["preview", "write"] },
"client": { "enum": ["codex", "claude", "openclaw"] }, "client": { "enum": ["codex", "claude", "openclaw"] },

View file

@ -215,6 +215,7 @@
"required": [ "required": [
"status", "status",
"schema_version", "schema_version",
"docforge_version",
"operation", "operation",
"action", "action",
"client", "client",
@ -232,6 +233,11 @@
"properties": { "properties": {
"status": { "const": "ok" }, "status": { "const": "ok" },
"schema_version": { "const": 1 }, "schema_version": { "const": 1 },
"docforge_version": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"operation": { "const": "client.configure" }, "operation": { "const": "client.configure" },
"action": { "enum": ["preview", "write"] }, "action": { "enum": ["preview", "write"] },
"client": { "enum": ["codex", "claude", "openclaw"] }, "client": { "enum": ["codex", "claude", "openclaw"] },

View file

@ -16,6 +16,7 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Literal, cast from typing import Literal, cast
from ._version import __version__ as DOCFORGE_VERSION
from .adapter_launcher import ( from .adapter_launcher import (
AdapterLauncherV1, AdapterLauncherV1,
AdapterSourceAvailabilityV1, AdapterSourceAvailabilityV1,
@ -617,6 +618,8 @@ def _validate_configuration_result(
*, *,
trusted_descriptor: ProjectDescriptor | None = None, trusted_descriptor: ProjectDescriptor | None = None,
) -> None: ) -> None:
if result.get("docforge_version") != DOCFORGE_VERSION:
raise AssertionError("Generated client product version drifted")
artifact = cast(dict[str, object], result["artifact"]) artifact = cast(dict[str, object], result["artifact"])
binding = cast(dict[str, object], result["binding"]) binding = cast(dict[str, object], result["binding"])
policy = cast(dict[str, object], result["effective_policy"]) policy = cast(dict[str, object], result["effective_policy"])
@ -797,6 +800,7 @@ def _validate_configuration_result(
expected_hash = document_hash( expected_hash = document_hash(
{ {
"schema_version": 1, "schema_version": 1,
"docforge_version": result["docforge_version"],
"client": result["client"], "client": result["client"],
"server_name": result["server_name"], "server_name": result["server_name"],
"project": project, "project": project,
@ -1041,6 +1045,7 @@ def generate_client_configuration(
plan_hash = document_hash( plan_hash = document_hash(
{ {
"schema_version": 1, "schema_version": 1,
"docforge_version": DOCFORGE_VERSION,
"client": selected_client, "client": selected_client,
"server_name": selected_name, "server_name": selected_name,
"project": project_binding, "project": project_binding,
@ -1061,6 +1066,7 @@ def generate_client_configuration(
result: dict[str, object] = { result: dict[str, object] = {
"status": "ok", "status": "ok",
"schema_version": 1, "schema_version": 1,
"docforge_version": DOCFORGE_VERSION,
"operation": "client.configure", "operation": "client.configure",
"action": "write" if output is not None else "preview", "action": "write" if output is not None else "preview",
"client": selected_client, "client": selected_client,
@ -1278,6 +1284,7 @@ def generate_adapter_client_configuration(
result: dict[str, object] = { result: dict[str, object] = {
"status": "ok", "status": "ok",
"schema_version": 1, "schema_version": 1,
"docforge_version": DOCFORGE_VERSION,
"operation": "adapter_client.configure", "operation": "adapter_client.configure",
"action": "write" if output is not None else "preview", "action": "write" if output is not None else "preview",
"client": selected_client, "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"]) artifact = cast(dict[str, object], result["artifact"])
return { return {
"schema_version": 1, "schema_version": 1,
"docforge_version": result["docforge_version"],
"client": result["client"], "client": result["client"],
"server_name": result["server_name"], "server_name": result["server_name"],
"project": result["project"], "project": result["project"],
@ -1375,6 +1383,8 @@ def _validate_adapter_configuration_result(
launcher: AdapterLauncherV1, launcher: AdapterLauncherV1,
source_availability: AdapterSourceAvailabilityV1, source_availability: AdapterSourceAvailabilityV1,
) -> None: ) -> None:
if result.get("docforge_version") != DOCFORGE_VERSION:
raise AssertionError("Generated adapter client product version drifted")
validate_adapter_launcher(project, launcher) validate_adapter_launcher(project, launcher)
descriptor = project.descriptor descriptor = project.descriptor
project_binding = cast(dict[str, object], result["project"]) project_binding = cast(dict[str, object], result["project"])

View file

@ -19,6 +19,7 @@ from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client from mcp.client.stdio import stdio_client
from referencing import Registry, Resource from referencing import Registry, Resource
from docforge._version import __version__
from docforge.adapter_contract import AdapterNode, AdapterProject, AdapterProjection from docforge.adapter_contract import AdapterNode, AdapterProject, AdapterProjection
from docforge.adapter_launcher import AdapterLauncherV1 from docforge.adapter_launcher import AdapterLauncherV1
from docforge.client_config import ( from docforge.client_config import (
@ -188,6 +189,7 @@ class AdapterLauncherTests(unittest.TestCase):
) )
self.assertEqual(first, second) self.assertEqual(first, second)
ADAPTER_CONFIGURATION_VALIDATOR.validate(first) ADAPTER_CONFIGURATION_VALIDATOR.validate(first)
self.assertEqual(__version__, first["docforge_version"])
self.assertEqual(launcher.launcher_hash, first["launcher_hash"]) self.assertEqual(launcher.launcher_hash, first["launcher_hash"])
self.assertEqual( self.assertEqual(
launcher.launcher_hash, launcher.launcher_hash,
@ -265,6 +267,15 @@ class AdapterLauncherTests(unittest.TestCase):
AdapterLauncherV1(**payload) # type: ignore[arg-type] AdapterLauncherV1(**payload) # type: ignore[arg-type]
result = generate_adapter_client_configuration(project, launcher, "codex") 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") result["binding"]["args"].append("--arbitrary")
with self.assertRaisesRegex(AssertionError, "Generated adapter client"): with self.assertRaisesRegex(AssertionError, "Generated adapter client"):
_validate_adapter_configuration_result( _validate_adapter_configuration_result(

View file

@ -19,6 +19,7 @@ from jsonschema import Draft202012Validator
from mcp import ClientSession, StdioServerParameters from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client from mcp.client.stdio import stdio_client
from docforge._version import __version__
from docforge.changeset_contract import document_hash from docforge.changeset_contract import document_hash
from docforge.cli import _parser, _run, main from docforge.cli import _parser, _run, main
from docforge.client_config import ( from docforge.client_config import (
@ -103,6 +104,7 @@ class ClientIntegrationTests(unittest.TestCase):
second = generate_client_configuration(project, client, no_ast=True) second = generate_client_configuration(project, client, no_ast=True)
self.assertEqual(first, second) self.assertEqual(first, second)
Draft202012Validator(CONFIGURATION_SCHEMA).validate(first) Draft202012Validator(CONFIGURATION_SCHEMA).validate(first)
self.assertEqual(__version__, first["docforge_version"])
Draft202012Validator(POLICY_SCHEMA).validate(first["effective_policy"]) Draft202012Validator(POLICY_SCHEMA).validate(first["effective_policy"])
self.assertEqual("read", first["binding"]["capability_mode"]) self.assertEqual("read", first["binding"]["capability_mode"])
self.assertEqual( self.assertEqual(
@ -251,6 +253,11 @@ class ClientIntegrationTests(unittest.TestCase):
validator.validate(result) validator.validate(result)
_validate_configuration_result(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 ( for field, value in (
("schema_version", 1), ("schema_version", 1),
("manual", "on-demand"), ("manual", "on-demand"),