Add versioned effective policy
This commit is contained in:
parent
a9a75c5c27
commit
34cd5f74c1
11 changed files with 708 additions and 70 deletions
|
|
@ -18,6 +18,7 @@ from .errors import DocForgeError
|
|||
from .index import ProjectIndex
|
||||
from .models import IncrementalStateProject, ProjectService, RuntimeValidatedProject
|
||||
from .pagination import canonical_hash, decode_cursor, page_limit, page_receipt
|
||||
from .policy import CapabilityMode, capability_mode, compose_effective_policy
|
||||
from .project import Project, project_root_fingerprint
|
||||
from .rendering import RenderService
|
||||
from .telemetry import request, stage
|
||||
|
|
@ -129,50 +130,87 @@ class DocForgeService:
|
|||
binding_metadata: Mapping[str, object] | None = None,
|
||||
no_ast: bool = False,
|
||||
diagnostics: bool = False,
|
||||
capability_mode_name: str | None = None,
|
||||
) -> None:
|
||||
self.project = project
|
||||
self.index = ProjectIndex(self.project, allow_logic=not no_ast)
|
||||
self.changesets = ChangesetStore(self.project, proposal_writer)
|
||||
default_mode: CapabilityMode = (
|
||||
"application" if canonical_applier is not None else "proposal"
|
||||
)
|
||||
selected_mode = capability_mode(capability_mode_name, default=default_mode)
|
||||
application_enabled = (
|
||||
canonical_applier_id is not None
|
||||
and canonical_applier is not None
|
||||
and selected_mode in {"application", "operator"}
|
||||
)
|
||||
self.policy = compose_effective_policy(
|
||||
selected_mode=selected_mode,
|
||||
capability_source=("factory_default" if capability_mode_name is None else "explicit"),
|
||||
no_ast=no_ast,
|
||||
diagnostics=diagnostics,
|
||||
render_configured=project.descriptor.render is not None,
|
||||
application_enabled=application_enabled,
|
||||
)
|
||||
self.index = ProjectIndex(self.project, allow_logic=not self.policy.no_ast)
|
||||
self.changesets = ChangesetStore(
|
||||
self.project,
|
||||
proposal_writer if selected_mode != "read" else None,
|
||||
)
|
||||
self.rendering = RenderService(self.project, self.changesets)
|
||||
self.application = CanonicalApplicationService(
|
||||
self.project,
|
||||
applier_id=canonical_applier_id,
|
||||
applier=canonical_applier,
|
||||
applier_id=canonical_applier_id if application_enabled else None,
|
||||
applier=canonical_applier if application_enabled else None,
|
||||
index=self.index,
|
||||
)
|
||||
self.visualization = ViewerManagerClient(self.index)
|
||||
self.context_provider = context_provider
|
||||
self.binding_metadata = dict(binding_metadata or {})
|
||||
self.no_ast = no_ast
|
||||
self.no_ast = self.policy.no_ast
|
||||
self.diagnostics = diagnostics
|
||||
self.tool_surface = tool_surface or (
|
||||
*ALL_TOOLS,
|
||||
*(APPLICATION_TOOLS if self.application.enabled else ()),
|
||||
default_surface = (
|
||||
READ_TOOLS
|
||||
if selected_mode == "read"
|
||||
else (
|
||||
*ALL_TOOLS,
|
||||
*(
|
||||
APPLICATION_TOOLS
|
||||
if self.application.enabled and selected_mode in {"application", "operator"}
|
||||
else ()
|
||||
),
|
||||
)
|
||||
)
|
||||
self.tool_surface = tool_surface or default_surface
|
||||
|
||||
def adapter_policy(self) -> dict[str, object]:
|
||||
"""Return the immutable adapter-evolution policy for this MCP binding."""
|
||||
|
||||
if not self.no_ast:
|
||||
return {
|
||||
"mode": "standard",
|
||||
"ast_analysis": "allowed",
|
||||
"logic_projection": "allowed",
|
||||
"incremental_extraction": "allowed",
|
||||
"adapter_rewrite": "not_requested",
|
||||
}
|
||||
return self.policy.adapter_policy()
|
||||
|
||||
def capabilities(self) -> dict[str, object]:
|
||||
"""Return the registered surfaces separately from startup-bound authority."""
|
||||
|
||||
proposal_access = self.changesets.access()
|
||||
application_access = self.application.access()
|
||||
return {
|
||||
"mode": "preserve-no-ast",
|
||||
"ast_analysis": "forbidden",
|
||||
"logic_projection": "forbidden",
|
||||
"incremental_extraction": "allowed",
|
||||
"adapter_rewrite": "forbidden",
|
||||
"blocked_tools": ["docforge_get_logic"],
|
||||
"instruction": (
|
||||
"Preserve the existing adapter extraction strategy. Do not add Python AST, "
|
||||
"Tree-sitter, compiler-AST, or function-Logic extraction. Non-AST incremental "
|
||||
"fingerprinting and caching remain allowed."
|
||||
),
|
||||
"schema_version": 1,
|
||||
"mode": self.policy.capability_mode,
|
||||
"registered_tools": list(self.tool_surface),
|
||||
"read": {
|
||||
"enabled": True,
|
||||
"tools": [tool for tool in READ_TOOLS if tool in self.tool_surface],
|
||||
},
|
||||
"proposal": {
|
||||
"surface_enabled": any(tool in self.tool_surface for tool in PROPOSAL_TOOLS),
|
||||
"mutation_access": proposal_access,
|
||||
},
|
||||
"application": {
|
||||
"surface_enabled": any(tool in self.tool_surface for tool in APPLICATION_TOOLS),
|
||||
"mutation_access": application_access,
|
||||
},
|
||||
"operator": {
|
||||
"enabled": self.policy.capability_mode == "operator",
|
||||
"tools": [],
|
||||
},
|
||||
}
|
||||
|
||||
def invoke(
|
||||
|
|
@ -507,15 +545,15 @@ class DocForgeService:
|
|||
def bootstrap(self) -> dict[str, object]:
|
||||
def operation() -> dict[str, object]:
|
||||
synchronized = self.index.synchronize()
|
||||
snapshot = self.project.load()
|
||||
root = snapshot.descriptor.root
|
||||
descriptor = self.project.descriptor
|
||||
root = descriptor.root
|
||||
binding = {
|
||||
"project_root": str(root),
|
||||
"descriptor_path": str(snapshot.descriptor.descriptor_path),
|
||||
"adapter": snapshot.descriptor.adapter,
|
||||
"cache_root": str(snapshot.descriptor.cache_root),
|
||||
"index_path": str(snapshot.descriptor.index_path),
|
||||
"changeset_root": str(snapshot.descriptor.changeset_root),
|
||||
"descriptor_path": str(descriptor.descriptor_path),
|
||||
"adapter": descriptor.adapter,
|
||||
"cache_root": str(descriptor.cache_root),
|
||||
"index_path": str(descriptor.index_path),
|
||||
"changeset_root": str(descriptor.changeset_root),
|
||||
**self.binding_metadata,
|
||||
"adapter_policy": self.adapter_policy(),
|
||||
}
|
||||
|
|
@ -523,11 +561,19 @@ class DocForgeService:
|
|||
"docforge_get_context or targeted read tools",
|
||||
"make and verify one coherent implementation slice",
|
||||
"docforge_sync",
|
||||
"docforge_register_changes",
|
||||
"docforge_get_changeset_diff",
|
||||
"docforge_apply_changeset",
|
||||
"docforge_bootstrap",
|
||||
]
|
||||
proposal_access = self.changesets.access()
|
||||
application_access = self.application.access()
|
||||
if proposal_access["enabled"] and "docforge_register_changes" in self.tool_surface:
|
||||
recommended_workflow.extend(
|
||||
(
|
||||
"docforge_register_changes",
|
||||
"docforge_get_changeset_diff",
|
||||
)
|
||||
)
|
||||
if application_access["enabled"] and "docforge_apply_changeset" in self.tool_surface:
|
||||
recommended_workflow.append("docforge_apply_changeset")
|
||||
recommended_workflow.append("docforge_bootstrap")
|
||||
if self.no_ast:
|
||||
recommended_workflow.insert(
|
||||
1,
|
||||
|
|
@ -536,19 +582,55 @@ class DocForgeService:
|
|||
"compiler-AST, or function-Logic extraction"
|
||||
),
|
||||
)
|
||||
if descriptor.profiles:
|
||||
recommended_first_operation: dict[str, object] = {
|
||||
"tool": "docforge_get_context",
|
||||
"arguments": {"profile": descriptor.profiles[0].profile_id},
|
||||
"reason": "Begin with one configured bounded context profile.",
|
||||
}
|
||||
else:
|
||||
recommended_first_operation = {
|
||||
"tool": "docforge_project_info",
|
||||
"arguments": dict[str, object](),
|
||||
"reason": "Confirm the fixed binding before targeted retrieval.",
|
||||
}
|
||||
capabilities = self.capabilities()
|
||||
effective_policy = self.policy.as_dict()
|
||||
session_contract: dict[str, object] = {
|
||||
"schema_version": 1,
|
||||
"binding": binding,
|
||||
"generation": {
|
||||
"revision": synchronized["revision"],
|
||||
"source_hash": synchronized["source_hash"],
|
||||
"freshness": "current",
|
||||
},
|
||||
"effective_policy": effective_policy,
|
||||
"capabilities": capabilities,
|
||||
"render_policies": {
|
||||
"manual": effective_policy["manual_render"],
|
||||
"graph": effective_policy["graph_render"],
|
||||
"live_viewer": effective_policy["live_viewer"],
|
||||
},
|
||||
"recommended_first_operation": recommended_first_operation,
|
||||
"recommended_workflow": recommended_workflow,
|
||||
"prohibitions": effective_policy["prohibitions"],
|
||||
}
|
||||
return {
|
||||
"status": "ok",
|
||||
"project_id": snapshot.descriptor.project_id,
|
||||
"project_id": descriptor.project_id,
|
||||
"project_root_fingerprint": project_root_fingerprint(root),
|
||||
"title": snapshot.descriptor.title,
|
||||
"adapter": snapshot.descriptor.adapter,
|
||||
"revision": snapshot.revision,
|
||||
"source_hash": snapshot.source_hash,
|
||||
"title": descriptor.title,
|
||||
"adapter": descriptor.adapter,
|
||||
"revision": synchronized["revision"],
|
||||
"source_hash": synchronized["source_hash"],
|
||||
"binding": binding,
|
||||
"canonical_paths": [str(path) for path in snapshot.descriptor.content_roots],
|
||||
"canonical_paths": [str(path) for path in descriptor.content_roots],
|
||||
"adapter_policy": self.adapter_policy(),
|
||||
"proposal_access": self.changesets.access(),
|
||||
"canonical_application_access": self.application.access(),
|
||||
"effective_policy": effective_policy,
|
||||
"capabilities": capabilities,
|
||||
"session_contract": session_contract,
|
||||
"proposal_access": proposal_access,
|
||||
"canonical_application_access": application_access,
|
||||
"synchronization": synchronized["synchronization"],
|
||||
"recommended_workflow": recommended_workflow,
|
||||
}
|
||||
|
|
@ -606,6 +688,8 @@ class DocForgeService:
|
|||
"Canonical project files own facts; DocForge results are derived."
|
||||
),
|
||||
"adapter_policy": self.adapter_policy(),
|
||||
"effective_policy": self.policy.as_dict(),
|
||||
"capabilities": self.capabilities(),
|
||||
"canonical_paths": [
|
||||
*(relative(path) for path in snapshot.descriptor.content_roots),
|
||||
*(relative(path) for path in snapshot.descriptor.authority_files),
|
||||
|
|
@ -638,14 +722,20 @@ class DocForgeService:
|
|||
],
|
||||
"allowed_tools": list(self.tool_surface),
|
||||
"excluded_operations": list(
|
||||
EXCLUDED_OPERATIONS
|
||||
+ (
|
||||
("canonical_writes", "canonical_changeset_application")
|
||||
if not self.application.enabled
|
||||
else ()
|
||||
dict.fromkeys(
|
||||
EXCLUDED_OPERATIONS
|
||||
+ (
|
||||
("canonical_writes", "canonical_changeset_application")
|
||||
if not self.application.enabled
|
||||
else ()
|
||||
)
|
||||
+ (
|
||||
READ_ONLY_EXCLUDED_OPERATIONS
|
||||
if self.policy.capability_mode == "read"
|
||||
else ()
|
||||
)
|
||||
+ tuple(self.policy.prohibitions)
|
||||
)
|
||||
+ (READ_ONLY_EXCLUDED_OPERATIONS if self.tool_surface == READ_TOOLS else ())
|
||||
+ (("adapter_ast_upgrade", "function_logic_extraction") if self.no_ast else ())
|
||||
),
|
||||
"proposal_access": self.changesets.access(),
|
||||
"canonical_application_access": self.application.access(),
|
||||
|
|
@ -907,19 +997,20 @@ class DocForgeService:
|
|||
|
||||
|
||||
def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMCP:
|
||||
capability = (
|
||||
"Read validated documentation for exactly one configured project."
|
||||
if read_only
|
||||
else (
|
||||
"Read validated documentation and write isolated proposal changesets and previews for "
|
||||
"exactly one configured project"
|
||||
+ (
|
||||
", with hash-bound canonical application enabled."
|
||||
if service.application.enabled
|
||||
else "."
|
||||
)
|
||||
)
|
||||
)
|
||||
capability = {
|
||||
"read": "Read validated documentation for exactly one configured project.",
|
||||
"proposal": (
|
||||
"Read validated documentation and use startup-gated isolated proposal changesets and "
|
||||
"previews for exactly one configured project."
|
||||
),
|
||||
"application": (
|
||||
"Read validated documentation, use startup-gated isolated proposals, and apply one "
|
||||
"exact validated changeset hash for exactly one configured project."
|
||||
),
|
||||
"operator": (
|
||||
"Operate the fixed validated documentation binding for exactly one configured project."
|
||||
),
|
||||
}[service.policy.capability_mode]
|
||||
server = FastMCP(
|
||||
"DocForge",
|
||||
instructions=(
|
||||
|
|
@ -1492,6 +1583,7 @@ def create_server(
|
|||
canonical_applier_id: str | None = None,
|
||||
no_ast: bool = False,
|
||||
diagnostics: bool = False,
|
||||
capability_mode: str | None = None,
|
||||
) -> FastMCP:
|
||||
project = Project.open(project_root)
|
||||
return create_project_server(
|
||||
|
|
@ -1507,6 +1599,7 @@ def create_server(
|
|||
},
|
||||
no_ast=no_ast,
|
||||
diagnostics=diagnostics,
|
||||
capability_mode=capability_mode,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1520,6 +1613,7 @@ def create_project_server(
|
|||
binding_metadata: Mapping[str, object] | None = None,
|
||||
no_ast: bool = False,
|
||||
diagnostics: bool = False,
|
||||
capability_mode: str | None = None,
|
||||
) -> FastMCP:
|
||||
"""Create the full fixed MCP surface for one explicitly configured project service."""
|
||||
|
||||
|
|
@ -1532,8 +1626,12 @@ def create_project_server(
|
|||
binding_metadata=binding_metadata,
|
||||
no_ast=no_ast,
|
||||
diagnostics=diagnostics,
|
||||
capability_mode_name=capability_mode,
|
||||
)
|
||||
return _create_bound_server(
|
||||
service,
|
||||
read_only=service.policy.capability_mode == "read",
|
||||
)
|
||||
return _create_bound_server(service, read_only=False)
|
||||
|
||||
|
||||
def create_read_only_server(
|
||||
|
|
@ -1543,6 +1641,7 @@ def create_read_only_server(
|
|||
binding_metadata: Mapping[str, object] | None = None,
|
||||
no_ast: bool = False,
|
||||
diagnostics: bool = False,
|
||||
capability_mode: str | None = None,
|
||||
) -> FastMCP:
|
||||
"""Create an adapter-capable MCP server exposing only the fixed read tool surface."""
|
||||
|
||||
|
|
@ -1553,7 +1652,14 @@ def create_read_only_server(
|
|||
binding_metadata=binding_metadata,
|
||||
no_ast=no_ast,
|
||||
diagnostics=diagnostics,
|
||||
capability_mode_name="read" if capability_mode is None else capability_mode,
|
||||
)
|
||||
if service.policy.capability_mode != "read":
|
||||
raise DocForgeError(
|
||||
"invalid_capability_mode",
|
||||
"Read-only server factory accepts only read capability mode",
|
||||
capability_mode=service.policy.capability_mode,
|
||||
)
|
||||
return _create_bound_server(service, read_only=True)
|
||||
|
||||
|
||||
|
|
@ -1575,6 +1681,11 @@ def main() -> None:
|
|||
action="store_true",
|
||||
help="Attach bounded request-local stage timings and counters",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--capability-mode",
|
||||
choices=("read", "proposal", "application", "operator"),
|
||||
help="Expose the versioned project-bound capability surface",
|
||||
)
|
||||
arguments = parser.parse_args()
|
||||
create_server(
|
||||
arguments.project_root,
|
||||
|
|
@ -1582,6 +1693,7 @@ def main() -> None:
|
|||
canonical_applier_id=arguments.canonical_applier,
|
||||
no_ast=arguments.no_ast,
|
||||
diagnostics=arguments.diagnostics,
|
||||
capability_mode=arguments.capability_mode,
|
||||
).run(transport="stdio")
|
||||
|
||||
|
||||
|
|
|
|||
166
src/docforge/policy.py
Normal file
166
src/docforge/policy.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
"""Versioned immutable policy composition for one project-bound server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from .errors import DocForgeError
|
||||
|
||||
CapabilityMode = Literal["read", "proposal", "application", "operator"]
|
||||
CAPABILITY_MODES: tuple[CapabilityMode, ...] = (
|
||||
"read",
|
||||
"proposal",
|
||||
"application",
|
||||
"operator",
|
||||
)
|
||||
|
||||
POLICY_PRECEDENCE = (
|
||||
"core_safety",
|
||||
"explicit_binding",
|
||||
"no_ast_shorthand",
|
||||
"resource_availability",
|
||||
)
|
||||
|
||||
|
||||
def capability_mode(value: str | None, *, default: CapabilityMode) -> CapabilityMode:
|
||||
"""Validate one additive capability-mode selection."""
|
||||
|
||||
selected = default if value is None else value
|
||||
if selected not in CAPABILITY_MODES:
|
||||
raise DocForgeError(
|
||||
"invalid_capability_mode",
|
||||
"Capability mode is unsupported",
|
||||
capability_mode=selected,
|
||||
allowed=list(CAPABILITY_MODES),
|
||||
)
|
||||
return selected # type: ignore[return-value]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EffectivePolicyV1:
|
||||
"""One fully composed process policy shared by every public projection."""
|
||||
|
||||
capability_mode: CapabilityMode
|
||||
capability_source: Literal["factory_default", "explicit"]
|
||||
adapter_evolution: Literal["allowed", "preserve"]
|
||||
ast_analysis: Literal["allowed", "forbidden"]
|
||||
logic_indexing: Literal["full", "off"]
|
||||
synchronization: Literal["automatic"]
|
||||
integrity: Literal["validated"]
|
||||
manual_render: Literal["auto", "explicit", "disabled"]
|
||||
graph_render: Literal["disabled"]
|
||||
live_viewer: Literal["on-demand"]
|
||||
profiling: Literal["enabled", "disabled"]
|
||||
blocked_tools: tuple[str, ...]
|
||||
prohibitions: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def no_ast(self) -> bool:
|
||||
return self.ast_analysis == "forbidden"
|
||||
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"capability_mode": self.capability_mode,
|
||||
"capability_source": self.capability_source,
|
||||
"adapter_evolution": self.adapter_evolution,
|
||||
"ast_analysis": self.ast_analysis,
|
||||
"logic_indexing": self.logic_indexing,
|
||||
"synchronization": self.synchronization,
|
||||
"integrity": self.integrity,
|
||||
"manual_render": self.manual_render,
|
||||
"graph_render": self.graph_render,
|
||||
"live_viewer": self.live_viewer,
|
||||
"profiling": self.profiling,
|
||||
"blocked_tools": list(self.blocked_tools),
|
||||
"prohibitions": list(self.prohibitions),
|
||||
"precedence": list(POLICY_PRECEDENCE),
|
||||
}
|
||||
|
||||
def adapter_policy(self) -> dict[str, object]:
|
||||
"""Preserve the exact legacy adapter-policy projection."""
|
||||
|
||||
if not self.no_ast:
|
||||
return {
|
||||
"mode": "standard",
|
||||
"ast_analysis": "allowed",
|
||||
"logic_projection": "allowed",
|
||||
"incremental_extraction": "allowed",
|
||||
"adapter_rewrite": "not_requested",
|
||||
}
|
||||
return {
|
||||
"mode": "preserve-no-ast",
|
||||
"ast_analysis": "forbidden",
|
||||
"logic_projection": "forbidden",
|
||||
"incremental_extraction": "allowed",
|
||||
"adapter_rewrite": "forbidden",
|
||||
"blocked_tools": ["docforge_get_logic"],
|
||||
"instruction": (
|
||||
"Preserve the existing adapter extraction strategy. Do not add Python AST, "
|
||||
"Tree-sitter, compiler-AST, or function-Logic extraction. Non-AST incremental "
|
||||
"fingerprinting and caching remain allowed."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def compose_effective_policy(
|
||||
*,
|
||||
selected_mode: CapabilityMode,
|
||||
capability_source: Literal["factory_default", "explicit"],
|
||||
no_ast: bool,
|
||||
diagnostics: bool,
|
||||
render_configured: bool,
|
||||
application_enabled: bool,
|
||||
) -> EffectivePolicyV1:
|
||||
"""Compose fixed defaults with restrictive compatibility shorthands."""
|
||||
|
||||
if selected_mode == "application" and not application_enabled:
|
||||
raise DocForgeError(
|
||||
"capability_unavailable",
|
||||
"Application capability requires a startup-bound canonical applier",
|
||||
capability_mode=selected_mode,
|
||||
required="canonical_applier",
|
||||
)
|
||||
prohibitions = [
|
||||
"arbitrary_file_access",
|
||||
"arbitrary_renderer_execution",
|
||||
"shell_execution",
|
||||
"git_mutation",
|
||||
"deployment",
|
||||
"publication",
|
||||
"project_switching",
|
||||
]
|
||||
blocked_tools: tuple[str, ...] = ()
|
||||
if no_ast:
|
||||
prohibitions.extend(
|
||||
(
|
||||
"adapter_ast_upgrade",
|
||||
"tree_sitter_upgrade",
|
||||
"compiler_ast_upgrade",
|
||||
"function_logic_extraction",
|
||||
)
|
||||
)
|
||||
blocked_tools = ("docforge_get_logic",)
|
||||
manual_render: Literal["auto", "explicit", "disabled"]
|
||||
if not render_configured:
|
||||
manual_render = "disabled"
|
||||
elif application_enabled and selected_mode in {"application", "operator"}:
|
||||
manual_render = "auto"
|
||||
else:
|
||||
manual_render = "explicit"
|
||||
return EffectivePolicyV1(
|
||||
capability_mode=selected_mode,
|
||||
capability_source=capability_source,
|
||||
adapter_evolution="preserve" if no_ast else "allowed",
|
||||
ast_analysis="forbidden" if no_ast else "allowed",
|
||||
logic_indexing="off" if no_ast else "full",
|
||||
synchronization="automatic",
|
||||
integrity="validated",
|
||||
manual_render=manual_render,
|
||||
graph_render="disabled",
|
||||
live_viewer="on-demand",
|
||||
profiling="enabled" if diagnostics else "disabled",
|
||||
blocked_tools=blocked_tools,
|
||||
prohibitions=tuple(prohibitions),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue