Host Backends And Projections
This page documents the host-backend and projection surface used by pydantic-acp.
If you are integrating langchain-acp, read LangChain ACP Projections and Event Projection Maps.
ACP Kit includes two small but important host-facing surfaces:
- session-scoped host backends
- projection maps
The first lets tools talk to the bound ACP client cleanly. The second makes ACP updates look better in the UI.
ClientFilesystemBackend
ClientFilesystemBackend is a thin adapter over ACP file APIs that automatically carries the active session id.
from pydantic_acp import ClientFilesystemBackend
backend = ClientFilesystemBackend(client=client, session=session)
response = await backend.read_text_file("notes/todo.txt")
print(response.content)
Supported methods:
read_text_file(...)write_text_file(...)
ClientTerminalBackend
ClientTerminalBackend does the same for ACP terminal operations:
from pydantic_acp import ClientTerminalBackend
backend = ClientTerminalBackend(client=client, session=session)
terminal = await backend.create_terminal("python", args=["-V"])
await backend.wait_for_terminal_exit(terminal.terminal_id)
output = await backend.terminal_output(terminal.terminal_id)
print(output.output)
Supported methods:
create_terminal(...)terminal_output(...)release_terminal(...)wait_for_terminal_exit(...)kill_terminal(...)
ClientHostContext
ClientHostContext groups both backends into one session-scoped object:
from pydantic_acp import ClientHostContext
host = ClientHostContext.from_session(client=client, session=session)
file_response = await host.filesystem.read_text_file("notes/workspace.md")
terminal = await host.terminal.create_terminal("python", args=["-V"])
This is the most ergonomic option inside a session-aware factory or AgentSource.
HostAccessPolicy
HostAccessPolicy adds a typed guardrail surface for host-backed file and terminal access.
from pydantic_acp import ClientHostContext, HostAccessPolicy
host = ClientHostContext.from_session(
client=client,
session=session,
access_policy=HostAccessPolicy(),
workspace_root=session.cwd,
)
What Problem It Solves
Host-backed integrations usually end up re-implementing the same decisions:
- should absolute paths be allowed
- should paths outside the active session cwd only warn or hard fail
- should workspace-root escapes be blocked
- should command cwd and command path arguments be treated with the same guardrail model
When that logic lives only in one downstream client, ACP-visible warnings and real execution policy drift apart. HostAccessPolicy gives ACP Kit one typed surface for both evaluation and enforcement.
Default policy behavior is conservative:
- absolute file paths warn
- paths outside the active session cwd warn
- paths outside the configured workspace root deny
- command cwd escapes and command path targets are evaluated with the same model
When the policy returns deny, the client backend raises PermissionError before the ACP request is sent.
Policy Shape
HostAccessPolicy currently controls seven decision points:
absolute_pathpath_outside_cwdpath_outside_workspacecommand_cwd_outside_cwdcommand_cwd_outside_workspacecommand_external_pathscommand_paths_outside_workspace
Each decision point resolves to:
allowwarndeny
You can also start from named presets:
from pydantic_acp import HostAccessPolicy
strict_policy = HostAccessPolicy.strict()
permissive_policy = HostAccessPolicy.permissive()
Use strict() when a coding agent should stay tightly inside the declared workspace. Use permissive() when the host still wants visibility into risk but does not want ACP Kit to deny as aggressively.
The evaluation objects are intentionally UI-friendly:
evaluation = strict_policy.evaluate_command(
'python',
args=['../scripts/build.py'],
session_cwd=session.cwd,
workspace_root=session.cwd,
)
print(evaluation.headline)
print(evaluation.message)
print(evaluation.recommendation)
This makes it easier for ACP clients and downstream integrations to render one consistent warning surface without rebuilding policy text manually.
Minimal verified path example:
from pathlib import Path
from pydantic_acp import HostAccessPolicy
policy = HostAccessPolicy.strict()
evaluation = policy.evaluate_path(
'../notes.txt',
session_cwd=Path('/workspace/app'),
workspace_root=Path('/workspace/app'),
)
assert evaluation.disposition == 'deny'
assert evaluation.should_deny
assert 'outside_cwd' in evaluation.risk_codes
Evaluation Surfaces
Path evaluation returns HostPathEvaluation. Command evaluation returns HostCommandEvaluation.
Both surfaces expose:
dispositionmessageheadlinerecommendationrisksrisk_codesprimary_riskhas_risksshould_warnshould_denysummary_lines()
This split is deliberate:
evaluate_*is for UI, previews, approval cards, or dry-run decisionsenforce_*is for actual blocking behavior before ACP host requests are sent
File And Command Evaluation Model
File access is evaluated against:
- the active session cwd
- the configured workspace root, if provided
- whether the original input path was absolute
Command access is evaluated against:
- the resolved command cwd
- the configured workspace root, if provided
- path-like command arguments such as
../file.py,/tmp/outside.txt, or--output=../dist/result.txt
The current command-path detection is intentionally heuristic. It is designed to catch obvious path targets and drive better guardrails or UI warnings, not to be a full shell parser.
Recommended Integration Pattern
Use the same policy in two places:
- host backend enforcement
- client-side projection or approval UX
That way:
- the warning a user sees
- and the rule that actually blocks execution
come from the same evaluation model.
Example:
policy = HostAccessPolicy.strict()
host = ClientHostContext.from_session(
client=client,
session=session,
access_policy=policy,
workspace_root=session.cwd,
)
evaluation = policy.evaluate_command(
'python',
args=['../scripts/build.py'],
session_cwd=session.cwd,
workspace_root=session.cwd,
)
Current Scope And Limits
HostAccessPolicy is intentionally narrow today.
It does:
- evaluate file paths
- evaluate command cwd and obvious path-like arguments
- return typed risk information
- enforce
denybefore ACP file or terminal requests are sent
It does not yet:
- rewrite or sanitize commands
- parse full shell syntax
- automatically wire itself through every runtime seam
- replace product-level approval UX
The current value is consistency: integrations can stop rebuilding one-off guardrail logic and use one native ACP Kit surface instead.
Projection Maps
Projection maps do not change tool execution. They change how ACP renders the resulting updates.
FileSystemProjectionMap
Use this for tool families that correspond to file reads, file writes, or shell commands:
from pydantic_acp import FileSystemProjectionMap
projection = FileSystemProjectionMap(
read_tool_names=frozenset({"mcp_repo_read_file", "mcp_host_read_workspace_file"}),
write_tool_names=frozenset({"mcp_host_write_workspace_file"}),
bash_tool_names=frozenset({"mcp_host_run_command"}),
)
This lets ACP clients render:
- read tools as diff-like previews
- write tools as file diffs
- shell tools as command previews or terminal references
Composing Projection Maps
Multiple projection maps can be combined:
from pydantic_acp import compose_projection_maps
projection_map = compose_projection_maps(filesystem_projection, hook_projection)
In practice, most setups pass them through AdapterConfig.projection_maps.
When To Use Host Backends
Use host backends when the ACP client should remain the authority for filesystem or shell access.
That is the right design for:
- editor integrations
- workspace-local coding agents
- security-reviewed command execution flows
- clients that want full visibility into shell creation and release