fix(agent): preserve agent-owned state in project workspaces (#4945)
This commit is contained in:
@@ -51,6 +51,7 @@ class _FsTool(Tool):
|
||||
file_states: FileStates | None = None,
|
||||
restrict_to_workspace: bool | None = None,
|
||||
sandbox_restricts_workspace: bool = False,
|
||||
extra_read_allowed_files: list[Path] | None = None,
|
||||
):
|
||||
self._workspace = workspace
|
||||
self._allowed_dir = allowed_dir
|
||||
@@ -60,6 +61,7 @@ class _FsTool(Tool):
|
||||
*(extra_allowed_dirs or []),
|
||||
*(extra_read_allowed_dirs or []),
|
||||
]
|
||||
self._extra_read_allowed_files = list(extra_read_allowed_files or [])
|
||||
self._extra_write_allowed_dirs = list(extra_write_allowed_dirs or [])
|
||||
self._extra_write_allowed_files = list(extra_write_allowed_files or [])
|
||||
self._restrict_to_workspace = (
|
||||
@@ -78,17 +80,21 @@ class _FsTool(Tool):
|
||||
def create(cls, ctx: Any) -> Tool:
|
||||
from nanobot.agent.skills import BUILTIN_SKILLS_DIR
|
||||
|
||||
agent_workspace = Path(ctx.workspace)
|
||||
resolved_agent_workspace = agent_workspace.expanduser().resolve(strict=False)
|
||||
restrict = (
|
||||
ctx.config.restrict_to_workspace
|
||||
or ctx.config.exec.sandbox
|
||||
)
|
||||
sandbox_restricts = bool(ctx.config.exec.sandbox)
|
||||
allowed_dir = Path(ctx.workspace) if restrict else None
|
||||
extra_read = [BUILTIN_SKILLS_DIR]
|
||||
allowed_dir = agent_workspace if restrict else None
|
||||
# Agent-owned skills stay available from project scopes. History is a narrower
|
||||
# capability: expose only the append-only log, not the surrounding memory directory.
|
||||
return cls(
|
||||
workspace=Path(ctx.workspace),
|
||||
workspace=agent_workspace,
|
||||
allowed_dir=allowed_dir,
|
||||
extra_read_allowed_dirs=extra_read,
|
||||
extra_read_allowed_dirs=[BUILTIN_SKILLS_DIR, resolved_agent_workspace / "skills"],
|
||||
extra_read_allowed_files=[resolved_agent_workspace / "memory" / "history.jsonl"],
|
||||
file_states=ctx.file_state_store,
|
||||
restrict_to_workspace=ctx.config.restrict_to_workspace,
|
||||
sandbox_restricts_workspace=sandbox_restricts,
|
||||
@@ -119,16 +125,20 @@ class _FsTool(Tool):
|
||||
extra_allowed_files: list[Path] | None,
|
||||
*,
|
||||
include_media_dir: bool,
|
||||
extra_files_require_allowed_root: bool = False,
|
||||
) -> Path:
|
||||
access = current_tool_workspace(
|
||||
self._workspace,
|
||||
restrict_to_workspace=self._restrict_to_workspace,
|
||||
sandbox_restricts_workspace=self._sandbox_restricts_workspace,
|
||||
)
|
||||
allowed_root = self._effective_allowed_root(access.allowed_root)
|
||||
if extra_files_require_allowed_root and allowed_root is None:
|
||||
extra_allowed_files = None
|
||||
return resolve_workspace_path(
|
||||
path,
|
||||
access.project_path,
|
||||
self._effective_allowed_root(access.allowed_root),
|
||||
allowed_root,
|
||||
extra_allowed_dirs,
|
||||
extra_allowed_files,
|
||||
include_media_dir=include_media_dir,
|
||||
@@ -138,8 +148,9 @@ class _FsTool(Tool):
|
||||
return self._resolve_with_extra(
|
||||
path,
|
||||
self._extra_read_allowed_dirs,
|
||||
None,
|
||||
self._extra_read_allowed_files,
|
||||
include_media_dir=True,
|
||||
extra_files_require_allowed_root=True,
|
||||
)
|
||||
|
||||
def _resolve_write(self, path: str) -> Path:
|
||||
|
||||
@@ -283,6 +283,7 @@ class GrepTool(_SearchTool):
|
||||
|
||||
_MAX_RESULT_CHARS = 128_000
|
||||
_MAX_FILE_BYTES = 2_000_000
|
||||
_MAX_EXPLICIT_FILE_BYTES = 100_000_000
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -295,7 +296,8 @@ class GrepTool(_SearchTool):
|
||||
"Default output_mode is files_with_matches (file paths only); "
|
||||
"use content mode for matching lines with context. Prefer this "
|
||||
"over shell grep for ordinary workspace searches. "
|
||||
"Skips binary and files >2 MB. Supports glob/type filtering."
|
||||
"Binary and file-size limits are enforced by the tool; explicit file paths "
|
||||
"use a larger bounded limit than directory searches. Supports glob/type filtering."
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -456,6 +458,9 @@ class GrepTool(_SearchTool):
|
||||
counts: dict[str, int] = {}
|
||||
file_mtimes: dict[str, float] = {}
|
||||
root = target if target.is_dir() else target.parent
|
||||
max_file_bytes = (
|
||||
self._MAX_EXPLICIT_FILE_BYTES if target.is_file() else self._MAX_FILE_BYTES
|
||||
)
|
||||
|
||||
for file_path in self._iter_files(target):
|
||||
rel_path = file_path.relative_to(root).as_posix()
|
||||
@@ -464,8 +469,9 @@ class GrepTool(_SearchTool):
|
||||
if not _matches_type(file_path.name, type):
|
||||
continue
|
||||
|
||||
raw = file_path.read_bytes()
|
||||
if len(raw) > self._MAX_FILE_BYTES:
|
||||
with file_path.open("rb") as file:
|
||||
raw = file.read(max_file_bytes + 1)
|
||||
if len(raw) > max_file_bytes:
|
||||
skipped_large += 1
|
||||
continue
|
||||
if _is_binary(raw):
|
||||
|
||||
Reference in New Issue
Block a user