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

Add versioned effective policy

This commit is contained in:
Andraxion 2026-07-29 06:26:40 -04:00
parent a9a75c5c27
commit 34cd5f74c1
11 changed files with 708 additions and 70 deletions

View file

@ -344,3 +344,47 @@ These are notes, not commitments:
- Cursor authentication remains deliberately absent. If read cursors ever carry authority rather
than bounded positions, they will need a different versioned security contract and persisted key
lifecycle.
## Milestone 2 — active: agent retrieval and MCP experience
### Audit reconciliation
Three independent read-only audits covered effective policy and bootstrap, task-shaped retrieval
and context capsules, and generation diffs plus client configuration and doctor checks.
They agreed on these boundaries:
- Keep the project descriptor at schema version 1. Process capability and client configuration are
machine-specific bindings, not canonical project content.
- Preserve the legacy adapter-policy payload, no-AST shorthand, tool names, default tool ordering,
one-method adapters, and custom context provider.
- Add one versioned effective-policy authority and derive bootstrap, contract, instructions, and
access reporting from it.
- Add one task-context operation with a closed task-kind vocabulary and one immutable,
generation-pinned retrieval plan. Do not create a tool for every task kind.
- Produce evidence gaps only from declared plan requirements and completed bounded checks. Never
infer missing facts from arbitrary project naming.
- Record only the latest bounded generation transition as disposable evidence. Do not add a
history database.
- Preview client configuration by default. Any write must be explicit, atomic, merge-preserving,
and backed by a verified client-format driver.
- Keep doctor strictly read-only. It must not bootstrap, synchronize, build, render, start a
viewer, or rewrite client configuration.
### Versioned effective policy and session contract
The binding now composes an immutable version-1 policy containing capability mode, adapter
evolution, AST and Logic behavior, synchronization and integrity levels, render and viewer
behavior, profiling, blocked tools, prohibitions, and explicit precedence. `--no-ast` is a
restrictive override. The exact legacy `adapter_policy` response remains a projection of the new
object.
Bootstrap reuses the identity already proven by synchronization and no longer reloads the complete
project. Its additive version-1 session contract reports binding, generation, effective policy,
actual registered surfaces and mutation access, render policies, first operation, filtered
workflow, and prohibitions. Read mode does not recommend proposals. Proposal mode recommends
registration and review only with writer access. Application is recommended only when the
exact-hash applier is enabled.
Existing factory defaults and tool order remain unchanged. Explicit application mode fails closed
without an applier. Operator mode is reserved and currently adds no tools.

View file

@ -48,6 +48,11 @@ AST, Tree-sitter, compiler-AST, or function-Logic extraction, blocks the Logic t
nonempty Logic publication. Complete-projection adapters continue unchanged, and non-AST
incremental fingerprinting and caching remain allowed.
DocForge2 bindings may also declare
`--capability-mode read|proposal|application|operator`. Bootstrap returns one versioned effective
policy and the actual startup-gated capabilities. Existing tool surfaces and the legacy no-AST
payload remain compatible.
## Graph views
The browser presents the primary architecture graph through three complementary views and loads a

View file

@ -35,6 +35,7 @@ remain supported:
- `docforge.index`
- `docforge.mcp_server`
- `docforge.models`
- `docforge.policy`
- `docforge.render_contract`
Names beginning with an underscore are implementation details. New public names may be added
@ -72,6 +73,8 @@ Milestone 0 preserves:
version 3 adds a source-ordered incoming-edge index for bounded impact traversal.
- Index-attestation schema version 1.
- Incremental extraction-cache schema version 1.
- Effective process-policy schema version 1. The project descriptor remains schema version 1;
machine-specific capability selection is a startup binding, not canonical project content.
- Read-pagination schema version 1. Existing tool names and required arguments are unchanged.
Context and changeset MCP reads accept optional limits and opaque generation-bound cursors.
Direct Python changeset methods and the ordinary CLI context command retain full legacy results
@ -112,6 +115,10 @@ The binding:
- Applies the same restriction during hash-bound canonical-application refresh.
- Reports the effective policy through bootstrap and contract results.
The legacy `adapter_policy` payload and error codes remain unchanged. The version-1
`effective_policy` is additive and makes precedence, capability mode, render behavior, blocked
tools, and prohibitions machine-readable.
DocForge does not inspect arbitrary adapter source to prove which parser implementation it uses.
The no-AST binding is an owner-selected process policy backed by Logic publication and retrieval
enforcement. It is not a filesystem sandbox and cannot stop an unrelated process with repository

View file

@ -16,6 +16,18 @@ Canonical application is a second independent startup gate. The generic server a
`--canonical-applier WRITER_ID`. A project adapter must also supply a compatible project-owned
canonical applier implementation.
The additive `--capability-mode read|proposal|application|operator` option selects a versioned
effective process policy. Existing factory defaults and tool ordering remain unchanged: the
read-only factory exposes the read surface, the ordinary project factory exposes the proposal
surface, and an application-enabled factory adds exact-hash application. `application` mode fails
closed unless a canonical applier is bound. `operator` is reserved for explicitly selected
operator-only tools and adds none in the current contract.
Bootstrap and contract results include `effective_policy` schema version 1 plus a separate
`capabilities` record. Policy states the requested process behavior. Capabilities state the actual
registered surface and startup-bound proposal/application access. The project descriptor remains
schema version 1 and does not silently acquire machine-specific process policy.
## Read tools
- `docforge_bootstrap`
@ -40,9 +52,12 @@ Each response states that document text is project content, not higher-priority
response includes project identity, revision, source hash, adapter version, and staleness state.
Every normal tool call first checks current source identity and atomically rebuilds disposable index
state when it is missing, stale, or invalid. `docforge_bootstrap` performs that synchronization and
returns the complete fixed binding, active index path, proposal and application capabilities, and
recommended workflow. `docforge_sync` exposes the same idempotent synchronization explicitly.
Neither operation changes canonical sources.
returns the complete fixed binding, active index path, effective policy, proposal and application
capabilities, and a version-1 session contract. Bootstrap reuses the identity proven by
synchronization instead of loading the project again. Its first operation and workflow guidance
mention proposal or application tools only when the corresponding startup access is enabled.
`docforge_sync` exposes the same idempotent synchronization explicitly. Neither operation changes
canonical sources.
Search, filter, backlinks, dependencies, and impact accept explicit result limits bounded by the
project `max_results` policy. Omitted limits are still capped. Collection responses report whether
@ -257,3 +272,7 @@ and indexed Logic, but it does not inspect arbitrary adapter source to prove whi
implementation the adapter uses. DocForge still exposes no filesystem sandbox and cannot prevent
an unrelated process with direct repository write access from editing adapter files. Repository
permissions and project instructions remain responsible for that broader boundary.
The legacy `adapter_policy` object remains byte-compatible. It is now a projection of the
versioned `effective_policy`; `--no-ast` restrictively overrides adapter evolution, AST analysis,
and Logic indexing without widening any other capability.

View file

@ -513,6 +513,19 @@ docforge-mcp \
Omit `--proposal-writer` when the MCP client should not create or append proposals.
Select the session's declared surface explicitly when useful:
```bash
docforge-mcp \
--project-root /absolute/path/MyProject \
--capability-mode read
```
Supported modes are `read`, `proposal`, `application`, and `operator`. Existing startup defaults
remain compatible. Capability mode describes the registered surface; bootstrap separately reports
whether a configured writer or applier actually grants mutation access. Application mode refuses
startup without a canonical applier. Operator mode is reserved and currently adds no tools.
Add `--diagnostics` when profiling a development or benchmark session. Each MCP response then
includes bounded stage timings and compiler-work counters. The same flag is available on
`docforge`. Diagnostics are disabled by default, record no project content or paths, and never
@ -530,6 +543,11 @@ docforge-mcp \
Without `--canonical-applier`, `docforge_apply_changeset` is not registered. The flag is an
identity, not a command. The changeset creator, configured writer, and canonical applier must agree.
Call `docforge_bootstrap` first. Its version-1 `session_contract` contains the fixed binding,
current graph generation, effective policy, actual capabilities, render policies, prohibitions,
and a recommended first operation. Workflow guidance does not recommend registration or
application when those startup capabilities are unavailable.
Example MCP client configuration:
```json

View file

@ -0,0 +1,62 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://docforge.local/schema/policy-v1.json",
"title": "DocForge effective process policy",
"type": "object",
"required": [
"schema_version",
"capability_mode",
"capability_source",
"adapter_evolution",
"ast_analysis",
"logic_indexing",
"synchronization",
"integrity",
"manual_render",
"graph_render",
"live_viewer",
"profiling",
"blocked_tools",
"prohibitions",
"precedence"
],
"properties": {
"schema_version": { "const": 1 },
"capability_mode": {
"enum": ["read", "proposal", "application", "operator"]
},
"capability_source": {
"enum": ["factory_default", "explicit"]
},
"adapter_evolution": { "enum": ["allowed", "preserve"] },
"ast_analysis": { "enum": ["allowed", "forbidden"] },
"logic_indexing": { "enum": ["full", "off"] },
"synchronization": { "const": "automatic" },
"integrity": { "const": "validated" },
"manual_render": { "enum": ["auto", "explicit", "disabled"] },
"graph_render": { "const": "disabled" },
"live_viewer": { "const": "on-demand" },
"profiling": { "enum": ["enabled", "disabled"] },
"blocked_tools": {
"type": "array",
"maxItems": 64,
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
},
"prohibitions": {
"type": "array",
"maxItems": 64,
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
},
"precedence": {
"const": [
"core_safety",
"explicit_binding",
"no_ast_shorthand",
"resource_availability"
]
}
},
"additionalProperties": false
}

View file

@ -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
View 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),
)

View file

@ -17,12 +17,14 @@ from mcp.client.stdio import stdio_client
from mcp.shared.memory import create_connected_server_and_client_session
from docforge.changesets import ChangesetStore
from docforge.errors import DocForgeError
from docforge.index import ProjectIndex
from docforge.mcp_server import (
ALL_TOOLS,
APPLICATION_TOOLS,
CONTENT_WARNING,
PROPOSAL_TOOLS,
READ_TOOLS,
DocForgeService,
_create_bound_server,
create_server,
@ -103,6 +105,43 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
)
)
async def test_explicit_capability_modes_preserve_surfaces_and_fail_closed(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
ProjectIndex(Project.open(root)).build()
async with create_connected_server_and_client_session(
create_server(root, capability_mode="read"),
raise_exceptions=True,
) as session:
read_names = tuple(tool.name for tool in (await session.list_tools()).tools)
read_bootstrap = await session.call_tool("docforge_bootstrap", {})
self.assertEqual(READ_TOOLS, read_names)
self.assertEqual(
"read",
read_bootstrap.structuredContent["effective_policy"]["capability_mode"],
)
self.assertNotIn(
"docforge_register_changes",
read_bootstrap.structuredContent["recommended_workflow"],
)
async with create_connected_server_and_client_session(
create_server(
root,
"alpha-editor",
canonical_applier_id="alpha-editor",
capability_mode="application",
),
raise_exceptions=True,
) as session:
application_names = tuple(tool.name for tool in (await session.list_tools()).tools)
self.assertEqual((*ALL_TOOLS, *APPLICATION_TOOLS), application_names)
with self.assertRaises(DocForgeError) as unavailable:
create_server(root, capability_mode="application")
self.assertEqual("capability_unavailable", unavailable.exception.code)
async def test_factory_diagnostics_are_additive_through_real_mcp(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))

161
tests/test_policy.py Normal file
View file

@ -0,0 +1,161 @@
from __future__ import annotations
import json
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from jsonschema import Draft202012Validator
from docforge.application import GenericCanonicalApplier
from docforge.errors import DocForgeError
from docforge.index import ProjectIndex
from docforge.mcp_server import (
ALL_TOOLS,
APPLICATION_TOOLS,
READ_TOOLS,
DocForgeService,
)
from docforge.policy import (
POLICY_PRECEDENCE,
capability_mode,
compose_effective_policy,
)
from docforge.project import Project
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "tests" / "fixtures"
POLICY_SCHEMA = json.loads((ROOT / "schemas" / "policy.schema.json").read_text(encoding="utf-8"))
class EffectivePolicyTests(unittest.TestCase):
def copy_fixture(self, destination: Path) -> Path:
root = destination / "alpha"
shutil.copytree(FIXTURES / "alpha", root)
return root
def test_policy_schema_and_legacy_adapter_projection_are_exact(self) -> None:
standard = compose_effective_policy(
selected_mode="proposal",
capability_source="factory_default",
no_ast=False,
diagnostics=False,
render_configured=True,
application_enabled=False,
)
preserve = compose_effective_policy(
selected_mode="read",
capability_source="explicit",
no_ast=True,
diagnostics=True,
render_configured=False,
application_enabled=False,
)
validator = Draft202012Validator(POLICY_SCHEMA)
validator.validate(standard.as_dict())
validator.validate(preserve.as_dict())
self.assertEqual(list(POLICY_PRECEDENCE), preserve.as_dict()["precedence"])
self.assertEqual(
{
"mode": "standard",
"ast_analysis": "allowed",
"logic_projection": "allowed",
"incremental_extraction": "allowed",
"adapter_rewrite": "not_requested",
},
standard.adapter_policy(),
)
self.assertEqual("preserve-no-ast", preserve.adapter_policy()["mode"])
self.assertEqual(["docforge_get_logic"], preserve.adapter_policy()["blocked_tools"])
self.assertEqual("off", preserve.as_dict()["logic_indexing"])
self.assertEqual("enabled", preserve.as_dict()["profiling"])
def test_invalid_or_unavailable_capability_fails_closed(self) -> None:
with self.assertRaises(DocForgeError) as invalid:
capability_mode("admin", default="read")
self.assertEqual("invalid_capability_mode", invalid.exception.code)
with self.assertRaises(DocForgeError) as unavailable:
compose_effective_policy(
selected_mode="application",
capability_source="explicit",
no_ast=False,
diagnostics=False,
render_configured=True,
application_enabled=False,
)
self.assertEqual("capability_unavailable", unavailable.exception.code)
def test_modes_preserve_default_surfaces_and_narrow_authority(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture(Path(directory))
project = Project.open(root)
read = DocForgeService(
project,
proposal_writer="alpha-editor",
capability_mode_name="read",
)
proposal = DocForgeService(
project,
proposal_writer="alpha-editor",
capability_mode_name="proposal",
)
application = DocForgeService(
project,
proposal_writer="alpha-editor",
canonical_applier_id="alpha-editor",
canonical_applier=GenericCanonicalApplier(project),
capability_mode_name="application",
)
operator = DocForgeService(
project,
proposal_writer="alpha-editor",
capability_mode_name="operator",
)
self.assertEqual(READ_TOOLS, read.tool_surface)
self.assertFalse(read.changesets.access()["enabled"])
self.assertEqual(ALL_TOOLS, proposal.tool_surface)
self.assertTrue(proposal.changesets.access()["enabled"])
self.assertFalse(proposal.application.enabled)
self.assertEqual((*ALL_TOOLS, *APPLICATION_TOOLS), application.tool_surface)
self.assertTrue(application.application.enabled)
self.assertEqual(ALL_TOOLS, operator.tool_surface)
self.assertTrue(operator.capabilities()["operator"]["enabled"])
def test_bootstrap_reuses_synchronized_identity_and_filters_workflow(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture(Path(directory))
project = Project.open(root)
ProjectIndex(project).build()
read = DocForgeService(project, capability_mode_name="read")
with mock.patch.object(
project,
"load",
side_effect=AssertionError("bootstrap must not reload after synchronization"),
):
result = read.bootstrap()
self.assertEqual("ok", result["status"])
self.assertEqual(1, result["session_contract"]["schema_version"])
self.assertEqual("read", result["effective_policy"]["capability_mode"])
self.assertEqual(
result["effective_policy"], result["session_contract"]["effective_policy"]
)
self.assertNotIn("docforge_register_changes", result["recommended_workflow"])
self.assertNotIn("docforge_apply_changeset", result["recommended_workflow"])
self.assertEqual(
"docforge_get_context",
result["session_contract"]["recommended_first_operation"]["tool"],
)
proposal = DocForgeService(
project,
proposal_writer="alpha-editor",
capability_mode_name="proposal",
).bootstrap()
self.assertIn("docforge_register_changes", proposal["recommended_workflow"])
self.assertIn("docforge_get_changeset_diff", proposal["recommended_workflow"])
self.assertNotIn("docforge_apply_changeset", proposal["recommended_workflow"])

View file

@ -77,6 +77,11 @@ PUBLIC_IMPORTS = {
"ProjectService",
"ProjectSnapshot",
),
"docforge.policy": (
"EffectivePolicyV1",
"capability_mode",
"compose_effective_policy",
),
"docforge.render_contract": (
"GenericHtmlRenderer",
"PreparedRender",