fix(agent): honor selected project workspace

This commit is contained in:
Xubin Ren
2026-08-25 02:14:02 +08:00
parent 2ac802b2d5
commit 7fb0811fbb
3 changed files with 61 additions and 0 deletions
+8
View File
@@ -112,6 +112,14 @@ class ContextBuilder:
parts.append(render_template("agent/tool_contract.md")) parts.append(render_template("agent/tool_contract.md"))
project_path = root.expanduser().resolve()
if project_path != self.workspace.expanduser().resolve():
parts.append(
"# Current Project\n\n"
f"Working directory: {project_path}\n"
"Use it as the default root for project files and relative tool paths."
)
if include_memory: if include_memory:
memory = self.memory.read_memory() memory = self.memory.read_memory()
if memory and not self._is_template_content(memory, "memory/MEMORY.md"): if memory and not self._is_template_content(memory, "memory/MEMORY.md"):
+23
View File
@@ -49,6 +49,29 @@ def test_system_prompt_stays_stable_when_clock_changes(tmp_path, monkeypatch) ->
assert prompt1 == prompt2 assert prompt1 == prompt2
def test_selected_project_path_follows_shared_cache_prefix(tmp_path) -> None:
"""Project paths must not invalidate the stable identity and tool contract prefix."""
agent_home = tmp_path / "agent-home"
project_a = tmp_path / "project-a"
project_b = tmp_path / "project-b"
agent_home.mkdir()
project_a.mkdir()
project_b.mkdir()
builder = ContextBuilder(agent_home)
prompt_a = builder.build_system_prompt(workspace=project_a)
prompt_b = builder.build_system_prompt(workspace=project_b)
marker = "# Current Project"
prefix_a = prompt_a[: prompt_a.index(marker)]
prefix_b = prompt_b[: prompt_b.index(marker)]
assert prefix_a == prefix_b
assert "# Tool Usage Notes" in prefix_a
assert str(project_a.resolve()) not in prefix_a
assert str(project_b.resolve()) not in prefix_b
assert prompt_a == builder.build_system_prompt(workspace=project_a)
def test_system_prompt_reflects_current_dream_memory_contract(tmp_path) -> None: def test_system_prompt_reflects_current_dream_memory_contract(tmp_path) -> None:
workspace = _make_workspace(tmp_path) workspace = _make_workspace(tmp_path)
builder = ContextBuilder(workspace) builder = ContextBuilder(workspace)
+30
View File
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from nanobot.agent.context import ContextBuilder
from nanobot.agent.tools.cli_apps import CliAppsTool from nanobot.agent.tools.cli_apps import CliAppsTool
from nanobot.agent.tools.context import RequestContext, ToolContext, request_context from nanobot.agent.tools.context import RequestContext, ToolContext, request_context
from nanobot.agent.tools.filesystem import ReadFileTool, WriteFileTool from nanobot.agent.tools.filesystem import ReadFileTool, WriteFileTool
@@ -21,6 +22,7 @@ from nanobot.config.schema import ImageGenerationToolConfig, ProviderConfig, Too
from nanobot.security.workspace_access import ( from nanobot.security.workspace_access import (
WORKSPACE_SCOPE_METADATA_KEY, WORKSPACE_SCOPE_METADATA_KEY,
WorkspaceScopeError, WorkspaceScopeError,
WorkspaceScopeResolver,
bind_workspace_scope, bind_workspace_scope,
default_workspace_scope, default_workspace_scope,
reset_workspace_scope, reset_workspace_scope,
@@ -101,6 +103,34 @@ def test_workspace_scope_accepts_home_relative_project_path(
assert scope.metadata()["project_path"] == str(project.resolve()) assert scope.metadata()["project_path"] == str(project.resolve())
@pytest.mark.parametrize("access_mode", ["restricted", "full"])
def test_selected_websocket_project_is_visible_to_the_model(
tmp_path: Path,
access_mode: str,
) -> None:
agent_home = tmp_path / "agent-home"
project = tmp_path / "project"
agent_home.mkdir()
project.mkdir()
resolver = WorkspaceScopeResolver(agent_home, default_restrict_to_workspace=False)
scope = resolver.for_turn(
channel="websocket",
message_metadata={
WORKSPACE_SCOPE_METADATA_KEY: {
"project_path": str(project),
"access_mode": access_mode,
}
},
session_metadata=None,
)
prompt = ContextBuilder(agent_home).build_system_prompt(workspace=scope.project_path)
assert prompt.index("# Tool Usage Notes") < prompt.index("# Current Project")
assert f"Working directory: {project.resolve()}" in prompt
assert "Use it as the default root for project files" in prompt
def test_workspace_scope_metadata_falls_back_for_stale_session(tmp_path: Path) -> None: def test_workspace_scope_metadata_falls_back_for_stale_session(tmp_path: Path) -> None:
scope = workspace_scope_from_metadata( scope = workspace_scope_from_metadata(
{ {