config: [agent] allow_paths static pre-allow

This commit is contained in:
2026-09-09 22:19:23 +08:00
parent d17c80b07e
commit 22f52a0bd2
5 changed files with 89 additions and 1 deletions
+6
View File
@@ -10,6 +10,9 @@ from msgspec import Struct, field
# ``https://api.deepseek.com/anthropic``)
type DeepSeekConvention = Literal["", "openai", "anthropic", "responses"]
# Static directory pre-allow mode for ``[agent].allow_paths`` (path → mode).
type AllowPathMode = Literal["read", "write", "exec"]
# Built-in persona when ``[agent].system_prompt`` is omitted.
# Set ``system_prompt = ""`` to omit the persona block only.
# Override with a multi-line TOML literal (prefer ''' so nested " is fine).
@@ -111,6 +114,9 @@ class AgentConfig(Struct, omit_defaults=True):
parallel_tools: bool = True
confirm_destructive: bool = True
path_denylist: list[str] = field(default_factory=list)
# Static directory pre-allow (resolved path → mode); never prompts.
# Denylist still applies, and the path must exist when chat starts.
allow_paths: dict[str, AllowPathMode] = field(default_factory=dict)
max_context_tokens: int = 200_000
# How to inject todo stack nags into model context (see agent/todo_nag.py).
+1
View File
@@ -8,6 +8,7 @@ from .access import get_directory_access_confirm_hook as get_directory_access_co
from .access import grant_process_access as grant_process_access
from .access import grant_session_access as grant_session_access
from .access import request_directory_access as request_directory_access
from .access import set_config_access as set_config_access
from .access import set_directory_access_confirm_hook as set_directory_access_confirm_hook
from .catalog import ToolCatalog as ToolCatalog
from .catalog import ToolSource as ToolSource
+27 -1
View File
@@ -30,7 +30,7 @@ from plyngent.tools.workspace import (
)
if TYPE_CHECKING:
from collections.abc import Callable
from collections.abc import Callable, Mapping
from plyngent.tools.context import InstanceState, SessionState
@@ -122,6 +122,32 @@ def active_grant_count() -> int:
return total
def set_config_access(entries: Mapping[str, str]) -> tuple[list[Path], list[str]]:
"""Install static TOML pre-allow entries on the bound instance policy.
Replaces any previous config grants. Returns ``(applied roots, skipped
keys)``; entries are skipped when the mode is unknown or the path does not
exist (the host warns about skipped keys).
"""
policy = require_bound_instance().workspace
policy.config_allow.clear()
applied: list[Path] = []
skipped: list[str] = []
for raw_path, raw_mode in entries.items():
mode = parse_access_mode(raw_mode)
resolved: Path | None
try:
resolved = Path(raw_path).expanduser().resolve()
except OSError:
resolved = None
if mode is None or resolved is None or not resolved.exists():
skipped.append(raw_path)
continue
policy.config_allow[resolved] = mode
applied.append(resolved)
return applied, skipped
def _resolve_target(path: str) -> Path | str:
"""Resolve a requested path; return a model-facing error string on failure."""
try: