Skip to content

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:

  1. session-scoped host backends
  2. 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_path
  • path_outside_cwd
  • path_outside_workspace
  • command_cwd_outside_cwd
  • command_cwd_outside_workspace
  • command_external_paths
  • command_paths_outside_workspace

Each decision point resolves to:

  • allow
  • warn
  • deny

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:

  • disposition
  • message
  • headline
  • recommendation
  • risks
  • risk_codes
  • primary_risk
  • has_risks
  • should_warn
  • should_deny
  • summary_lines()

This split is deliberate:

  • evaluate_* is for UI, previews, approval cards, or dry-run decisions
  • enforce_* 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.

Use the same policy in two places:

  1. host backend enforcement
  2. 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 deny before 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