fix(agent): give file tools the agent workspace as root when no workspace is set (#6722)

* fix(agent): give file tools the agent workspace as root when no workspace is set (#6679)

With no workspace bound, bash/python run in data/agent_workspace, but
RequestAuthority sealed no filesystem root, so every native file tool
failed with "requires a sealed resource root", and an approved
write_file failed with "no sealed resource identity". Seal the agent
workspace instead, and resolve relative paths in _resolve_tool_path
against it rather than the server's cwd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: re-seal resource roots when a test swaps the authority owner (#6679)

A no-workspace authority now seals the agent workspace for its owner, so
replace(authority, owner="bob") copied alice's root and was rejected
before the approval-binding check the tests are about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agent): only seal a vetted agent workspace as the file-tool root (#6679)

Apply the agent_cwd() check before sealing data/agent_workspace: it must
be a real directory in the readable data subdirs, otherwise no root is
sealed. A symlinked workspace no longer redirects the file tools or the
bash/python launch scope. Tighten the dispatch test assertions to the
exact 'outside the sealed roots' message.

* refactor(agent): vet the no-workspace file root with default_agent_workspace (#6679)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grzegorz
2026-10-26 09:46:52 -06:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 9d0d3720f4
commit 1000b6582c
7 changed files with 109 additions and 15 deletions
+80 -4
View File
@@ -123,12 +123,88 @@ def test_blocks_listing_the_data_directory_itself():
_resolve_tool_path(DATA_DIR)
def test_blocks_app_state_reached_by_relative_path(monkeypatch):
"""The data directory is a relative hop from the checkout root, so
def test_blocks_app_state_reached_by_relative_path(tmp_path, monkeypatch):
"""The data directory is a relative hop from the agent workspace, so
confinement cannot depend on the model supplying an absolute path."""
monkeypatch.chdir(os.path.dirname(DATA_DIR))
current_execution = importlib.import_module("src.tool_execution")
data_dir = tmp_path / "data"
data_dir.mkdir()
workspace = _configure_test_data_tree(monkeypatch, data_dir)["AGENT_WORKSPACE_DIR"]
workspace.mkdir()
monkeypatch.setattr(current_execution, "_AGENT_WORKDIR", str(workspace))
with pytest.raises(ValueError, match="application state"):
_resolve_tool_path(os.path.join(os.path.basename(DATA_DIR), "sessions.json"))
current_execution._resolve_tool_path(os.path.join("..", "sessions.json"))
def test_relative_path_resolves_in_agent_workspace_not_server_cwd(tmp_path, monkeypatch):
"""With no workspace bound, bash runs in the agent workspace, so a
relative write_file/read_file path must land there too (#6679), not in
the server's cwd, which is outside the allowed roots."""
current_execution = importlib.import_module("src.tool_execution")
data_dir = tmp_path / "data"
data_dir.mkdir()
workspace = _configure_test_data_tree(monkeypatch, data_dir)["AGENT_WORKSPACE_DIR"]
workspace.mkdir()
monkeypatch.setattr(current_execution, "_AGENT_WORKDIR", str(workspace))
server_cwd = tmp_path / "checkout"
server_cwd.mkdir()
monkeypatch.chdir(server_cwd)
token = current_execution._active_workspace.set(None)
try:
assert current_execution._resolve_tool_path("test.txt") == os.path.join(
os.path.realpath(workspace), "test.txt"
)
assert current_execution._resolve_search_root("sub") == os.path.join(
os.path.realpath(workspace), "sub"
)
finally:
current_execution._active_workspace.reset(token)
def test_request_without_workspace_seals_the_agent_workspace(tmp_path, monkeypatch):
"""With no workspace, the native file tools get the folder bash/python
already run in (#6679) instead of no root at all, and a relative path
lands there while app state next to it stays out of reach."""
from src.agent_runtime.authority import ExactOperation, OperationGrant, RequestAuthority
from src.agent_runtime.resource_binding import resolve_filesystem_operation
current_execution = importlib.import_module("src.tool_execution")
data_dir = tmp_path / "data"
data_dir.mkdir()
workspace = _configure_test_data_tree(monkeypatch, data_dir)["AGENT_WORKSPACE_DIR"]
workspace.mkdir()
(data_dir / "sessions.json").write_text("{}", encoding="utf-8")
monkeypatch.setattr(current_execution, "_AGENT_WORKDIR", str(workspace))
authority = RequestAuthority("request", "alice", "s", "", (OperationGrant("write_file"),))
assert [r.path for r in authority.resource_roots] == [os.path.realpath(workspace)]
bound = resolve_filesystem_operation(
ExactOperation.normalize("write_file", "test.txt\nhello"), roots=authority.resource_roots)
assert bound.bindings[0].resource.path == os.path.join(os.path.realpath(workspace), "test.txt")
with pytest.raises(ValueError, match="outside the sealed roots"):
resolve_filesystem_operation(
ExactOperation.normalize("read_file", "../sessions.json"), roots=authority.resource_roots)
def test_symlinked_agent_workspace_is_not_sealed(tmp_path, monkeypatch):
"""A symlinked agent workspace must not become the file-tool root:
agent_cwd() and create_directories() refuse the same layout."""
from src.agent_runtime.authority import OperationGrant, RequestAuthority
current_execution = importlib.import_module("src.tool_execution")
data_dir = tmp_path / "data"
data_dir.mkdir()
workspace = _configure_test_data_tree(monkeypatch, data_dir)["AGENT_WORKSPACE_DIR"]
elsewhere = tmp_path / "elsewhere"
elsewhere.mkdir()
(elsewhere / "notes.txt").write_text("private", encoding="utf-8")
try:
workspace.symlink_to(elsewhere, target_is_directory=True)
except OSError:
pytest.skip("cannot create symlink")
monkeypatch.setattr(current_execution, "_AGENT_WORKDIR", str(workspace))
authority = RequestAuthority("request", "alice", "s", "", (OperationGrant("read_file"),))
assert authority.resource_roots == ()
def test_blocks_app_state_reached_through_a_symlink(tmp_path):