1081 lines
38 KiB
Python
1081 lines
38 KiB
Python
"""Deterministic, explicit client-configuration plans for DocForge MCP."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import secrets
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
from collections.abc import Callable
|
|
from contextlib import suppress
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Literal, cast
|
|
|
|
from .changeset_contract import document_hash
|
|
from .errors import DocForgeError
|
|
from .models import ProjectDescriptor, ProjectService
|
|
from .policy import CapabilityMode, compose_effective_policy
|
|
from .project import Project, project_root_fingerprint, validate_descriptor_binding
|
|
from .projection_policy import compose_projection_policy
|
|
|
|
ClientName = Literal["codex", "claude", "openclaw"]
|
|
CLIENT_NAMES: tuple[ClientName, ...] = ("codex", "claude", "openclaw")
|
|
MAX_CLIENT_FRAGMENT_BYTES = 1_000_000
|
|
GENERATED_CAPABILITY_MODES: tuple[CapabilityMode, ...] = (
|
|
"read",
|
|
"proposal",
|
|
"application",
|
|
)
|
|
_SERVER_NAME = re.compile(r"[a-z0-9][a-z0-9_-]{0,63}")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _FileIdentity:
|
|
device: int
|
|
inode: int
|
|
mode: int
|
|
size: int
|
|
mtime_ns: int
|
|
ctime_ns: int
|
|
uid: int
|
|
link_count: int
|
|
|
|
|
|
def _file_identity(status: os.stat_result) -> _FileIdentity:
|
|
return _FileIdentity(
|
|
device=status.st_dev,
|
|
inode=status.st_ino,
|
|
mode=status.st_mode,
|
|
size=status.st_size,
|
|
mtime_ns=status.st_mtime_ns,
|
|
ctime_ns=status.st_ctime_ns,
|
|
uid=status.st_uid,
|
|
link_count=status.st_nlink,
|
|
)
|
|
|
|
|
|
def _client_name(value: str) -> ClientName:
|
|
if value not in CLIENT_NAMES:
|
|
raise DocForgeError(
|
|
"unsupported_client",
|
|
"Client configuration target is unsupported",
|
|
client=value,
|
|
allowed=list(CLIENT_NAMES),
|
|
)
|
|
return value
|
|
|
|
|
|
def _capability_mode(value: str) -> CapabilityMode:
|
|
if value not in GENERATED_CAPABILITY_MODES:
|
|
raise DocForgeError(
|
|
"invalid_capability_mode",
|
|
"Generated configuration supports read, proposal, or application mode",
|
|
capability_mode=value,
|
|
allowed=list(GENERATED_CAPABILITY_MODES),
|
|
)
|
|
return value
|
|
|
|
|
|
def _bounded_seconds(value: int, *, field: str, maximum: int) -> int:
|
|
if type(value) is not int or value < 1 or value > maximum:
|
|
raise DocForgeError(
|
|
"invalid_timeout",
|
|
"Client timeout is outside the supported range",
|
|
field=field,
|
|
minimum=1,
|
|
maximum=maximum,
|
|
)
|
|
return value
|
|
|
|
|
|
def _default_server_name(project_id: str, fingerprint: str) -> str:
|
|
prefix = re.sub(r"[^a-z0-9_-]+", "-", project_id.lower()).strip("-_")
|
|
prefix = prefix or "project"
|
|
suffix = f"-{fingerprint}"
|
|
available = 64 - len("docforge-") - len(suffix)
|
|
return f"docforge-{prefix[:available]}{suffix}"
|
|
|
|
|
|
def _validated_server_name(value: str | None, *, project_id: str, fingerprint: str) -> str:
|
|
selected = value or _default_server_name(project_id, fingerprint)
|
|
if _SERVER_NAME.fullmatch(selected) is None:
|
|
raise DocForgeError(
|
|
"invalid_server_name",
|
|
"Generated server name must be a stable lowercase client identifier",
|
|
pattern=_SERVER_NAME.pattern,
|
|
maximum_length=64,
|
|
)
|
|
return selected
|
|
|
|
|
|
def _toml_string(value: str) -> str:
|
|
return json.dumps(value, ensure_ascii=False)
|
|
|
|
|
|
def _toml_array(values: list[str]) -> str:
|
|
return "[" + ", ".join(_toml_string(value) for value in values) + "]"
|
|
|
|
|
|
def _artifact(
|
|
client: ClientName,
|
|
*,
|
|
server_name: str,
|
|
command: str,
|
|
arguments: list[str],
|
|
startup_timeout: int,
|
|
tool_timeout: int,
|
|
) -> tuple[str, str, str | None]:
|
|
if client == "codex":
|
|
content = "\n".join(
|
|
(
|
|
f'[mcp_servers."{server_name}"]',
|
|
f"command = {_toml_string(command)}",
|
|
f"args = {_toml_array(arguments)}",
|
|
"env = {}",
|
|
f"startup_timeout_sec = {startup_timeout}",
|
|
f"tool_timeout_sec = {tool_timeout}",
|
|
"",
|
|
)
|
|
)
|
|
return "codex-toml-fragment-v1", content, None
|
|
if client == "openclaw":
|
|
content = (
|
|
json.dumps(
|
|
{
|
|
"mcp": {
|
|
"servers": {
|
|
server_name: {
|
|
"args": arguments,
|
|
"command": command,
|
|
"connectTimeout": startup_timeout,
|
|
"env": {},
|
|
"supportsParallelToolCalls": False,
|
|
"timeout": tool_timeout,
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
+ "\n"
|
|
)
|
|
return "openclaw-json-fragment-v1", content, None
|
|
content = (
|
|
json.dumps(
|
|
{
|
|
"mcpServers": {
|
|
server_name: {
|
|
"args": arguments,
|
|
"command": command,
|
|
"env": {},
|
|
}
|
|
}
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
+ "\n"
|
|
)
|
|
return (
|
|
"claude-json-fragment-v1",
|
|
content,
|
|
"Claude per-server timeout representation is not yet verified.",
|
|
)
|
|
|
|
|
|
def _signature(
|
|
directory_fd: int,
|
|
name: str,
|
|
) -> _FileIdentity | None:
|
|
try:
|
|
status = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
|
|
except FileNotFoundError:
|
|
return None
|
|
except OSError as error:
|
|
raise DocForgeError(
|
|
"unsafe_output",
|
|
"Configuration output cannot be inspected safely",
|
|
) from error
|
|
if stat.S_ISLNK(status.st_mode) or not stat.S_ISREG(status.st_mode):
|
|
raise DocForgeError(
|
|
"unsafe_output",
|
|
"Configuration output must be a regular file and not a symbolic link",
|
|
)
|
|
return _file_identity(status)
|
|
|
|
|
|
def _parent_binding_current(path: Path, directory_fd: int) -> bool:
|
|
try:
|
|
before = path.lstat()
|
|
resolved = path.resolve(strict=True)
|
|
after = path.lstat()
|
|
opened = os.fstat(directory_fd)
|
|
return (
|
|
not stat.S_ISLNK(before.st_mode)
|
|
and stat.S_ISDIR(before.st_mode)
|
|
and resolved == path
|
|
and (before.st_dev, before.st_ino, before.st_mode)
|
|
== (after.st_dev, after.st_ino, after.st_mode)
|
|
== (opened.st_dev, opened.st_ino, opened.st_mode)
|
|
)
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _require_parent_binding(path: Path, directory_fd: int) -> None:
|
|
if not _parent_binding_current(path, directory_fd):
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output parent changed during publication",
|
|
)
|
|
|
|
|
|
def _bound_parent(path: Path) -> tuple[Path, int]:
|
|
absolute = Path(os.path.abspath(path.expanduser()))
|
|
parent = absolute.parent
|
|
try:
|
|
parent_status = parent.lstat()
|
|
resolved = parent.resolve(strict=True)
|
|
except OSError as error:
|
|
raise DocForgeError(
|
|
"invalid_output",
|
|
"Configuration output parent does not exist",
|
|
) from error
|
|
if (
|
|
stat.S_ISLNK(parent_status.st_mode)
|
|
or not stat.S_ISDIR(parent_status.st_mode)
|
|
or resolved != parent
|
|
or absolute.name in {"", ".", ".."}
|
|
):
|
|
raise DocForgeError(
|
|
"unsafe_output",
|
|
"Configuration output parent must be one real non-symlinked directory",
|
|
)
|
|
try:
|
|
directory_fd = os.open(
|
|
parent,
|
|
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
|
)
|
|
except OSError as error:
|
|
raise DocForgeError(
|
|
"unsafe_output",
|
|
"Configuration output parent cannot be opened safely",
|
|
) from error
|
|
opened = os.fstat(directory_fd)
|
|
if opened.st_dev != parent_status.st_dev or opened.st_ino != parent_status.st_ino:
|
|
with suppress(OSError):
|
|
os.close(directory_fd)
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output parent changed while it was opened",
|
|
)
|
|
return absolute, directory_fd
|
|
|
|
|
|
def _read_existing(
|
|
directory_fd: int,
|
|
name: str,
|
|
signature: _FileIdentity,
|
|
) -> bytes:
|
|
if signature.size > MAX_CLIENT_FRAGMENT_BYTES:
|
|
raise DocForgeError(
|
|
"output_oversized",
|
|
"Existing configuration output exceeds the bounded fragment limit",
|
|
maximum_bytes=MAX_CLIENT_FRAGMENT_BYTES,
|
|
)
|
|
try:
|
|
descriptor = os.open(
|
|
name,
|
|
os.O_RDONLY | os.O_NOFOLLOW,
|
|
dir_fd=directory_fd,
|
|
)
|
|
except OSError as error:
|
|
raise DocForgeError(
|
|
"unsafe_output",
|
|
"Configuration output cannot be opened safely",
|
|
) from error
|
|
try:
|
|
opened = os.fstat(descriptor)
|
|
opened_signature = _file_identity(opened)
|
|
if opened_signature != signature:
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output changed while it was opened",
|
|
)
|
|
remaining = MAX_CLIENT_FRAGMENT_BYTES + 1
|
|
chunks: list[bytes] = []
|
|
while remaining:
|
|
chunk = os.read(descriptor, min(65_536, remaining))
|
|
if not chunk:
|
|
break
|
|
chunks.append(chunk)
|
|
remaining -= len(chunk)
|
|
raw = b"".join(chunks)
|
|
finally:
|
|
with suppress(OSError):
|
|
os.close(descriptor)
|
|
if len(raw) > MAX_CLIENT_FRAGMENT_BYTES or _signature(directory_fd, name) != signature:
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output changed while it was read",
|
|
)
|
|
return raw
|
|
|
|
|
|
def _private_existing(identity: _FileIdentity) -> bool:
|
|
return (
|
|
identity.uid == os.geteuid()
|
|
and stat.S_IMODE(identity.mode) & 0o077 == 0
|
|
and identity.link_count == 1
|
|
)
|
|
|
|
|
|
def _rollback_link(
|
|
directory_fd: int,
|
|
name: str,
|
|
expected: _FileIdentity,
|
|
) -> bool:
|
|
try:
|
|
current = _signature(directory_fd, name)
|
|
if current is None:
|
|
return True
|
|
if current.device != expected.device or current.inode != expected.inode:
|
|
return True
|
|
os.unlink(name, dir_fd=directory_fd)
|
|
return True
|
|
except (DocForgeError, OSError):
|
|
return False
|
|
|
|
|
|
def _rollback_and_sync(
|
|
directory_fd: int,
|
|
name: str,
|
|
expected: _FileIdentity,
|
|
) -> bool:
|
|
if not _rollback_link(directory_fd, name, expected):
|
|
return False
|
|
try:
|
|
os.fsync(directory_fd)
|
|
except OSError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _atomic_write(
|
|
path: Path,
|
|
content: str,
|
|
*,
|
|
validate_binding: Callable[[], None],
|
|
) -> tuple[str, str, str | None, Path | None]:
|
|
target, directory_fd = _bound_parent(path)
|
|
encoded = content.encode("utf-8")
|
|
if len(encoded) > MAX_CLIENT_FRAGMENT_BYTES:
|
|
with suppress(OSError):
|
|
os.close(directory_fd)
|
|
raise DocForgeError(
|
|
"output_oversized",
|
|
"Generated configuration fragment exceeds the bounded limit",
|
|
maximum_bytes=MAX_CLIENT_FRAGMENT_BYTES,
|
|
)
|
|
temporary_name = f".docforge-client-{secrets.token_hex(12)}"
|
|
temporary_created = False
|
|
committed = False
|
|
linked_identity: _FileIdentity | None = None
|
|
try:
|
|
_require_parent_binding(target.parent, directory_fd)
|
|
before = _signature(directory_fd, target.name)
|
|
if before is not None:
|
|
if not _private_existing(before):
|
|
raise DocForgeError(
|
|
"unsafe_output",
|
|
(
|
|
"Existing configuration fragment must be owned by the current user, "
|
|
"private, and singly linked"
|
|
),
|
|
)
|
|
existing = _read_existing(directory_fd, target.name, before)
|
|
if existing == encoded:
|
|
validate_binding()
|
|
_require_parent_binding(target.parent, directory_fd)
|
|
current = _signature(directory_fd, target.name)
|
|
if (
|
|
current != before
|
|
or current is None
|
|
or not _private_existing(current)
|
|
or _read_existing(directory_fd, target.name, current) != encoded
|
|
):
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output changed before unchanged publication was confirmed",
|
|
)
|
|
validate_binding()
|
|
_require_parent_binding(target.parent, directory_fd)
|
|
return "unchanged", "not_applicable", None, target
|
|
raise DocForgeError(
|
|
"output_conflict",
|
|
"Configuration fragment already exists with different content",
|
|
existing_sha256=hashlib.sha256(existing).hexdigest(),
|
|
generated_sha256=hashlib.sha256(encoded).hexdigest(),
|
|
)
|
|
|
|
try:
|
|
temporary_fd = os.open(
|
|
temporary_name,
|
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
|
0o600,
|
|
dir_fd=directory_fd,
|
|
)
|
|
except OSError as error:
|
|
raise DocForgeError(
|
|
"output_publication_failed",
|
|
"Configuration fragment temporary file could not be created",
|
|
) from error
|
|
temporary_created = True
|
|
try:
|
|
with os.fdopen(temporary_fd, "wb", closefd=True) as handle:
|
|
handle.write(encoded)
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
except OSError as error:
|
|
raise DocForgeError(
|
|
"output_publication_failed",
|
|
"Configuration fragment temporary file could not be written durably",
|
|
) from error
|
|
temporary_identity = _signature(directory_fd, temporary_name)
|
|
if temporary_identity is None:
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration fragment temporary file disappeared before publication",
|
|
)
|
|
if _signature(directory_fd, target.name) is not None:
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output appeared before atomic publication",
|
|
)
|
|
_require_parent_binding(target.parent, directory_fd)
|
|
validate_binding()
|
|
try:
|
|
os.link(
|
|
temporary_name,
|
|
target.name,
|
|
src_dir_fd=directory_fd,
|
|
dst_dir_fd=directory_fd,
|
|
follow_symlinks=False,
|
|
)
|
|
except FileExistsError as error:
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output appeared during atomic publication",
|
|
) from error
|
|
except OSError as error:
|
|
raise DocForgeError(
|
|
"output_publication_failed",
|
|
"Configuration fragment could not be published atomically",
|
|
) from error
|
|
linked_identity = temporary_identity
|
|
try:
|
|
validate_binding()
|
|
except DocForgeError:
|
|
if _rollback_link(directory_fd, target.name, temporary_identity):
|
|
raise
|
|
committed = True
|
|
return (
|
|
"created",
|
|
"unconfirmed",
|
|
"publication_binding_unconfirmed",
|
|
None,
|
|
)
|
|
linked = _signature(directory_fd, target.name)
|
|
if (
|
|
linked is None
|
|
or linked.device != temporary_identity.device
|
|
or linked.inode != temporary_identity.inode
|
|
or _read_existing(directory_fd, target.name, linked) != encoded
|
|
or not _parent_binding_current(target.parent, directory_fd)
|
|
):
|
|
if _rollback_link(directory_fd, target.name, temporary_identity):
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output changed during atomic publication",
|
|
)
|
|
committed = True
|
|
return (
|
|
"created",
|
|
"unconfirmed",
|
|
"publication_location_unconfirmed",
|
|
None,
|
|
)
|
|
durability = "confirmed"
|
|
warning: str | None = None
|
|
try:
|
|
os.unlink(temporary_name, dir_fd=directory_fd)
|
|
temporary_created = False
|
|
published = _signature(directory_fd, target.name)
|
|
if (
|
|
published is None
|
|
or not _private_existing(published)
|
|
or published.device != temporary_identity.device
|
|
or published.inode != temporary_identity.inode
|
|
or _read_existing(directory_fd, target.name, published) != encoded
|
|
or not _parent_binding_current(target.parent, directory_fd)
|
|
):
|
|
if _rollback_link(directory_fd, target.name, temporary_identity):
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output changed after atomic publication",
|
|
)
|
|
committed = True
|
|
return (
|
|
"created",
|
|
"unconfirmed",
|
|
"publication_location_unconfirmed",
|
|
None,
|
|
)
|
|
try:
|
|
validate_binding()
|
|
except DocForgeError:
|
|
if _rollback_link(directory_fd, target.name, temporary_identity):
|
|
raise
|
|
committed = True
|
|
return (
|
|
"created",
|
|
"unconfirmed",
|
|
"publication_binding_unconfirmed",
|
|
None,
|
|
)
|
|
os.fsync(directory_fd)
|
|
except OSError:
|
|
durability = "unconfirmed"
|
|
warning = "publication_durability_unconfirmed"
|
|
try:
|
|
validate_binding()
|
|
except DocForgeError:
|
|
if _rollback_and_sync(directory_fd, target.name, temporary_identity):
|
|
raise
|
|
committed = True
|
|
return (
|
|
"created",
|
|
"unconfirmed",
|
|
"publication_binding_unconfirmed",
|
|
None,
|
|
)
|
|
try:
|
|
published = _signature(directory_fd, target.name)
|
|
publication_current = (
|
|
published is not None
|
|
and _private_existing(published)
|
|
and published.device == temporary_identity.device
|
|
and published.inode == temporary_identity.inode
|
|
and _read_existing(directory_fd, target.name, published) == encoded
|
|
and _parent_binding_current(target.parent, directory_fd)
|
|
)
|
|
except DocForgeError:
|
|
publication_current = False
|
|
if not publication_current:
|
|
if _rollback_and_sync(directory_fd, target.name, temporary_identity):
|
|
raise DocForgeError(
|
|
"output_changed",
|
|
"Configuration output changed before publication was finalized",
|
|
)
|
|
committed = True
|
|
return (
|
|
"created",
|
|
"unconfirmed",
|
|
"publication_location_unconfirmed",
|
|
None,
|
|
)
|
|
committed = True
|
|
return "created", durability, warning, target
|
|
except Exception:
|
|
if not committed and linked_identity is not None:
|
|
_rollback_link(directory_fd, target.name, linked_identity)
|
|
if not committed and temporary_created:
|
|
with suppress(OSError):
|
|
os.unlink(temporary_name, dir_fd=directory_fd)
|
|
raise
|
|
finally:
|
|
with suppress(OSError):
|
|
os.close(directory_fd)
|
|
|
|
|
|
def _validate_configuration_result(
|
|
result: dict[str, object],
|
|
*,
|
|
trusted_descriptor: ProjectDescriptor | None = None,
|
|
) -> None:
|
|
artifact = cast(dict[str, object], result["artifact"])
|
|
binding = cast(dict[str, object], result["binding"])
|
|
policy = cast(dict[str, object], result["effective_policy"])
|
|
projection_policy = cast(dict[str, object], result["projection_policy"])
|
|
projection_availability = cast(
|
|
dict[str, object],
|
|
result["projection_availability"],
|
|
)
|
|
project = cast(dict[str, object], result["project"])
|
|
if trusted_descriptor is None:
|
|
try:
|
|
bound_descriptor = Project.open(cast(str, project["project_root"])).descriptor
|
|
except (DocForgeError, KeyError, TypeError) as error:
|
|
raise AssertionError(
|
|
"Generated client project binding cannot be independently validated"
|
|
) from error
|
|
else:
|
|
bound_descriptor = trusted_descriptor
|
|
if (
|
|
project["project_id"] != bound_descriptor.project_id
|
|
or project["project_root"] != str(bound_descriptor.root)
|
|
or project["project_root_fingerprint"] != project_root_fingerprint(bound_descriptor.root)
|
|
or project["adapter"] != bound_descriptor.adapter
|
|
or project["descriptor_hash"] != bound_descriptor.descriptor_hash
|
|
):
|
|
raise AssertionError("Generated client project binding drifted")
|
|
content = cast(str, artifact["content"])
|
|
if artifact["content_sha256"] != hashlib.sha256(content.encode("utf-8")).hexdigest():
|
|
raise AssertionError("Generated client content hash drifted")
|
|
timeouts = cast(dict[str, object], binding["timeouts"])
|
|
expected_format, expected_content, _ = _artifact(
|
|
cast(ClientName, result["client"]),
|
|
server_name=cast(str, result["server_name"]),
|
|
command=cast(str, binding["command"]),
|
|
arguments=cast(list[str], binding["args"]),
|
|
startup_timeout=cast(int, timeouts["startup_seconds"]),
|
|
tool_timeout=cast(int, timeouts["tool_seconds"]),
|
|
)
|
|
if artifact["format"] != expected_format or content != expected_content:
|
|
raise AssertionError("Generated client artifact drifted from its binding")
|
|
adapter_policy = cast(dict[str, object], binding["adapter_policy"])
|
|
render_policy = cast(dict[str, object], binding["render_policy"])
|
|
arguments = cast(list[str], binding["args"])
|
|
prefix = [
|
|
"-I",
|
|
"-m",
|
|
"docforge.mcp_server",
|
|
"--project-root",
|
|
cast(str, project["project_root"]),
|
|
"--capability-mode",
|
|
cast(str, binding["capability_mode"]),
|
|
]
|
|
if arguments[:7] != prefix:
|
|
raise AssertionError("Generated client arguments drifted from their binding")
|
|
remaining = arguments[7:]
|
|
no_ast_argument = "--no-ast" in arguments
|
|
if no_ast_argument:
|
|
if remaining[-1:] != ["--no-ast"] or arguments.count("--no-ast") != 1:
|
|
raise AssertionError("Generated no-AST argument layout drifted")
|
|
remaining = remaining[:-1]
|
|
projection_arguments: dict[str, str] = {}
|
|
authority_arguments: list[str] = []
|
|
position = 0
|
|
projection_options = {
|
|
"--manual-render-policy": "manual",
|
|
"--portable-graph-policy": "portable_graph",
|
|
"--live-viewer-policy": "live_viewer",
|
|
}
|
|
while position < len(remaining):
|
|
option = remaining[position]
|
|
field = projection_options.get(option)
|
|
if field is None:
|
|
authority_arguments.append(option)
|
|
position += 1
|
|
continue
|
|
if position + 1 >= len(remaining) or option in projection_arguments:
|
|
raise AssertionError("Generated projection policy argument layout drifted")
|
|
value = remaining[position + 1]
|
|
projection_arguments[option] = value
|
|
if projection_policy[field] != value:
|
|
raise AssertionError("Generated projection policy argument drifted")
|
|
position += 2
|
|
remaining = authority_arguments
|
|
mode = binding["capability_mode"]
|
|
if (
|
|
(mode == "read" and remaining)
|
|
or (
|
|
mode == "proposal"
|
|
and (len(remaining) != 2 or remaining[0] != "--proposal-writer" or not remaining[1])
|
|
)
|
|
or (
|
|
mode == "application"
|
|
and (
|
|
len(remaining) != 4
|
|
or remaining[0] != "--proposal-writer"
|
|
or remaining[2] != "--canonical-applier"
|
|
or not remaining[1]
|
|
or remaining[1] != remaining[3]
|
|
)
|
|
)
|
|
):
|
|
raise AssertionError("Generated authority argument layout drifted")
|
|
expected_projection_policy = compose_projection_policy(
|
|
manual=projection_arguments.get("--manual-render-policy"),
|
|
portable_graph=projection_arguments.get("--portable-graph-policy"),
|
|
live_viewer=projection_arguments.get("--live-viewer-policy"),
|
|
manual_configured=cast(bool, projection_availability["manual_configured"]),
|
|
portable_graph_configured=cast(
|
|
bool,
|
|
projection_availability["portable_graph_configured"],
|
|
),
|
|
application_enabled=cast(
|
|
bool,
|
|
projection_availability["application_enabled"],
|
|
),
|
|
live_viewer_available=cast(
|
|
bool,
|
|
projection_availability["live_viewer_available"],
|
|
),
|
|
)
|
|
if (
|
|
projection_policy != expected_projection_policy.as_dict()
|
|
or projection_availability["manual_configured"] != (render_policy["manual"] != "disabled")
|
|
or projection_availability["manual_configured"] != (bound_descriptor.render is not None)
|
|
or projection_availability["portable_graph_configured"]
|
|
!= (bound_descriptor.graph_render is not None)
|
|
or projection_availability["application_enabled"] != (mode == "application")
|
|
or projection_availability["live_viewer_available"] is not True
|
|
):
|
|
raise AssertionError("Generated projection policy drifted from its availability")
|
|
composed_policy = compose_effective_policy(
|
|
selected_mode=cast(CapabilityMode, mode),
|
|
capability_source="explicit",
|
|
no_ast=adapter_policy["mode"] == "preserve-no-ast",
|
|
diagnostics=False,
|
|
render_configured=render_policy["manual"] != "disabled",
|
|
application_enabled=mode == "application",
|
|
)
|
|
expected_policy = composed_policy.as_dict()
|
|
if (
|
|
policy != expected_policy
|
|
or adapter_policy != composed_policy.adapter_policy()
|
|
or binding["capability_mode"] != policy["capability_mode"]
|
|
or no_ast_argument != (adapter_policy["mode"] == "preserve-no-ast")
|
|
or render_policy["manual"] != policy["manual_render"]
|
|
or render_policy["graph"] != policy["graph_render"]
|
|
or render_policy["live_viewer"] != policy["live_viewer"]
|
|
or (
|
|
adapter_policy["mode"] == "preserve-no-ast"
|
|
and (
|
|
policy["adapter_evolution"] != "preserve"
|
|
or policy["ast_analysis"] != "forbidden"
|
|
or policy["logic_indexing"] != "off"
|
|
)
|
|
)
|
|
or (
|
|
adapter_policy["mode"] == "standard"
|
|
and (
|
|
policy["adapter_evolution"] != "allowed"
|
|
or policy["ast_analysis"] != "allowed"
|
|
or policy["logic_indexing"] != "full"
|
|
)
|
|
)
|
|
):
|
|
raise AssertionError("Generated client policy drifted from its binding")
|
|
if (
|
|
result["projection_policy_hash"]
|
|
!= hashlib.sha256(
|
|
json.dumps(
|
|
projection_policy,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
ensure_ascii=False,
|
|
).encode("utf-8")
|
|
).hexdigest()
|
|
):
|
|
raise AssertionError("Generated projection policy hash drifted")
|
|
expected_hash = document_hash(
|
|
{
|
|
"schema_version": 1,
|
|
"client": result["client"],
|
|
"server_name": result["server_name"],
|
|
"project": project,
|
|
"binding": binding,
|
|
"effective_policy": policy,
|
|
"projection_policy": projection_policy,
|
|
"projection_policy_hash": result["projection_policy_hash"],
|
|
"projection_availability": projection_availability,
|
|
"artifact_format": artifact["format"],
|
|
"artifact_content_sha256": artifact["content_sha256"],
|
|
}
|
|
)
|
|
if result["configuration_hash"] != expected_hash:
|
|
raise AssertionError("Generated client configuration hash drifted")
|
|
|
|
|
|
def generate_client_configuration(
|
|
project: ProjectService,
|
|
client: str,
|
|
*,
|
|
server_name: str | None = None,
|
|
capability_mode: str = "read",
|
|
proposal_writer: str | None = None,
|
|
canonical_applier: str | None = None,
|
|
no_ast: bool = False,
|
|
manual_render_policy: str | None = None,
|
|
portable_graph_policy: str | None = None,
|
|
live_viewer_policy: str | None = None,
|
|
startup_timeout: int = 30,
|
|
tool_timeout: int = 300,
|
|
output: Path | None = None,
|
|
) -> dict[str, object]:
|
|
"""Build one deterministic client fragment and optionally publish it explicitly."""
|
|
|
|
validate_descriptor_binding(project.descriptor)
|
|
selected_client = _client_name(client)
|
|
selected_mode = _capability_mode(capability_mode)
|
|
startup_seconds = _bounded_seconds(
|
|
startup_timeout,
|
|
field="startup_timeout",
|
|
maximum=3_600,
|
|
)
|
|
tool_seconds = _bounded_seconds(
|
|
tool_timeout,
|
|
field="tool_timeout",
|
|
maximum=86_400,
|
|
)
|
|
descriptor = project.descriptor
|
|
if descriptor.adapter != "generic":
|
|
raise DocForgeError(
|
|
"client_configuration_unavailable",
|
|
"Generic CLI configuration cannot reconstruct a project-owned adapter",
|
|
adapter=descriptor.adapter,
|
|
)
|
|
writer_ids = {writer.writer_id for writer in descriptor.proposal_writers}
|
|
if selected_mode == "read":
|
|
if proposal_writer is not None or canonical_applier is not None:
|
|
raise DocForgeError(
|
|
"invalid_capability_binding",
|
|
"Read configuration cannot bind proposal or application authority",
|
|
)
|
|
elif selected_mode == "proposal":
|
|
if proposal_writer is None or proposal_writer not in writer_ids:
|
|
raise DocForgeError(
|
|
"capability_unavailable",
|
|
"Proposal configuration requires a descriptor-declared writer",
|
|
required="proposal_writer",
|
|
)
|
|
if canonical_applier is not None:
|
|
raise DocForgeError(
|
|
"invalid_capability_binding",
|
|
"Proposal configuration cannot bind a canonical applier",
|
|
)
|
|
else:
|
|
if (
|
|
proposal_writer is None
|
|
or canonical_applier is None
|
|
or proposal_writer != canonical_applier
|
|
or proposal_writer not in writer_ids
|
|
):
|
|
raise DocForgeError(
|
|
"capability_unavailable",
|
|
"Application configuration requires one declared writer/applier identity",
|
|
required="matching_declared_writer_and_applier",
|
|
)
|
|
|
|
fingerprint = project_root_fingerprint(descriptor.root)
|
|
selected_name = _validated_server_name(
|
|
server_name,
|
|
project_id=descriptor.project_id,
|
|
fingerprint=fingerprint,
|
|
)
|
|
executable = Path(os.path.abspath(sys.executable))
|
|
try:
|
|
executable_status = executable.stat()
|
|
except OSError as error:
|
|
raise DocForgeError(
|
|
"client_configuration_unavailable",
|
|
"Current Python executable cannot be inspected",
|
|
) from error
|
|
if not stat.S_ISREG(executable_status.st_mode) or not os.access(executable, os.X_OK):
|
|
raise DocForgeError(
|
|
"client_configuration_unavailable",
|
|
"Current Python executable is not a runnable regular file",
|
|
)
|
|
try:
|
|
probe = subprocess.run(
|
|
[
|
|
str(executable),
|
|
"-I",
|
|
"-B",
|
|
"-c",
|
|
"import docforge.mcp_server",
|
|
],
|
|
check=False,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=10,
|
|
env=os.environ.copy(),
|
|
)
|
|
except (OSError, subprocess.SubprocessError) as error:
|
|
raise DocForgeError(
|
|
"client_configuration_unavailable",
|
|
"Current isolated Python executable could not be probed safely",
|
|
) from error
|
|
if probe.returncode != 0:
|
|
raise DocForgeError(
|
|
"client_configuration_unavailable",
|
|
"Current isolated Python executable cannot import docforge.mcp_server",
|
|
)
|
|
arguments = [
|
|
"-I",
|
|
"-m",
|
|
"docforge.mcp_server",
|
|
"--project-root",
|
|
str(descriptor.root),
|
|
"--capability-mode",
|
|
selected_mode,
|
|
]
|
|
if proposal_writer is not None:
|
|
arguments.extend(("--proposal-writer", proposal_writer))
|
|
if canonical_applier is not None:
|
|
arguments.extend(("--canonical-applier", canonical_applier))
|
|
policy = compose_effective_policy(
|
|
selected_mode=selected_mode,
|
|
capability_source="explicit",
|
|
no_ast=no_ast,
|
|
diagnostics=False,
|
|
render_configured=descriptor.render is not None,
|
|
application_enabled=canonical_applier is not None,
|
|
)
|
|
projection_policy = compose_projection_policy(
|
|
manual=manual_render_policy,
|
|
portable_graph=portable_graph_policy,
|
|
live_viewer=live_viewer_policy,
|
|
manual_configured=descriptor.render is not None,
|
|
portable_graph_configured=descriptor.graph_render is not None,
|
|
application_enabled=canonical_applier is not None,
|
|
)
|
|
default_projection_policy = compose_projection_policy(
|
|
manual_configured=descriptor.render is not None,
|
|
portable_graph_configured=descriptor.graph_render is not None,
|
|
application_enabled=canonical_applier is not None,
|
|
)
|
|
for option, selected, default in (
|
|
(
|
|
"--manual-render-policy",
|
|
projection_policy.manual,
|
|
default_projection_policy.manual,
|
|
),
|
|
(
|
|
"--portable-graph-policy",
|
|
projection_policy.portable_graph,
|
|
default_projection_policy.portable_graph,
|
|
),
|
|
(
|
|
"--live-viewer-policy",
|
|
projection_policy.live_viewer,
|
|
default_projection_policy.live_viewer,
|
|
),
|
|
):
|
|
if selected != default:
|
|
arguments.extend((option, selected))
|
|
if no_ast:
|
|
arguments.append("--no-ast")
|
|
artifact_format, content, warning = _artifact(
|
|
selected_client,
|
|
server_name=selected_name,
|
|
command=str(executable),
|
|
arguments=arguments,
|
|
startup_timeout=startup_seconds,
|
|
tool_timeout=tool_seconds,
|
|
)
|
|
if output is None:
|
|
validate_descriptor_binding(descriptor)
|
|
write_state = "not_requested"
|
|
durability = "not_applicable"
|
|
publication_warning = None
|
|
output_path = None
|
|
else:
|
|
validate_descriptor_binding(descriptor)
|
|
write_state, durability, publication_warning, published_path = _atomic_write(
|
|
output,
|
|
content,
|
|
validate_binding=lambda: validate_descriptor_binding(descriptor),
|
|
)
|
|
output_path = str(published_path) if published_path is not None else None
|
|
artifact = {
|
|
"format": artifact_format,
|
|
"content": content,
|
|
"content_sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
"output_path": output_path,
|
|
"write_state": write_state,
|
|
"durability": durability,
|
|
}
|
|
binding = {
|
|
"transport": "stdio",
|
|
"capability_mode": selected_mode,
|
|
"adapter_policy": policy.adapter_policy(),
|
|
"render_policy": {
|
|
"manual": policy.manual_render,
|
|
"graph": policy.graph_render,
|
|
"live_viewer": policy.live_viewer,
|
|
},
|
|
"command": str(executable),
|
|
"args": arguments,
|
|
"environment": {},
|
|
"timeouts": {
|
|
"startup_seconds": startup_seconds,
|
|
"tool_seconds": tool_seconds,
|
|
},
|
|
}
|
|
project_binding = {
|
|
"project_id": descriptor.project_id,
|
|
"project_root": str(descriptor.root),
|
|
"project_root_fingerprint": fingerprint,
|
|
"adapter": descriptor.adapter,
|
|
"descriptor_hash": descriptor.descriptor_hash,
|
|
}
|
|
policy_payload = policy.as_dict()
|
|
plan_hash = document_hash(
|
|
{
|
|
"schema_version": 1,
|
|
"client": selected_client,
|
|
"server_name": selected_name,
|
|
"project": project_binding,
|
|
"binding": binding,
|
|
"effective_policy": policy_payload,
|
|
"projection_policy": projection_policy.as_dict(),
|
|
"projection_policy_hash": projection_policy.policy_hash,
|
|
"projection_availability": {
|
|
"manual_configured": descriptor.render is not None,
|
|
"portable_graph_configured": descriptor.graph_render is not None,
|
|
"application_enabled": canonical_applier is not None,
|
|
"live_viewer_available": True,
|
|
},
|
|
"artifact_format": artifact_format,
|
|
"artifact_content_sha256": artifact["content_sha256"],
|
|
}
|
|
)
|
|
result: dict[str, object] = {
|
|
"status": "ok",
|
|
"schema_version": 1,
|
|
"operation": "client.configure",
|
|
"action": "write" if output is not None else "preview",
|
|
"client": selected_client,
|
|
"server_name": selected_name,
|
|
"project": project_binding,
|
|
"binding": binding,
|
|
"effective_policy": policy_payload,
|
|
"projection_policy": projection_policy.as_dict(),
|
|
"projection_policy_hash": projection_policy.policy_hash,
|
|
"projection_availability": {
|
|
"manual_configured": descriptor.render is not None,
|
|
"portable_graph_configured": descriptor.graph_render is not None,
|
|
"application_enabled": canonical_applier is not None,
|
|
"live_viewer_available": True,
|
|
},
|
|
"artifact": artifact,
|
|
"configuration_hash": plan_hash,
|
|
"warnings": [
|
|
*([] if warning is None else [{"code": "timeout_format_unverified"}]),
|
|
*([] if publication_warning is None else [{"code": publication_warning}]),
|
|
],
|
|
}
|
|
_validate_configuration_result(result, trusted_descriptor=descriptor)
|
|
return result
|