mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-25 00:02:25 +02:00
fix(approvals): seal approved bash/python and file writes in the agent workspace when no workspace is set (#6705)
#6683 made an exact approval seal a launch boundary (and a filesystem root) in the request's workspace, but only when a workspace is bound. Agent mode with no folder picked is the default, and there the approval still sealed nothing, so "Allow" always ended in "Approved action has no sealed process/job identity" (bash/python) or "Approved filesystem action has no sealed resource identity" (write_file). In Docker every agent turn carries the backend runtime context, so bash and write_file always ask for approval and never ran. With no workspace, seal the root in the agent workspace, which is where agent_cwd() already runs bash/python in that case. Inherited (child) authorities still cannot widen. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
98a16242c6
commit
f8e043e8ca
+10
-4
@@ -425,9 +425,13 @@ class ToolApprovalStore:
|
|||||||
# approval exists to admit an ungranted one. Seal its launch
|
# approval exists to admit an ungranted one. Seal its launch
|
||||||
# boundary now, in the request's workspace, the same way an
|
# boundary now, in the request's workspace, the same way an
|
||||||
# ungranted filesystem action gets its root below (#6635).
|
# ungranted filesystem action gets its root below (#6635).
|
||||||
|
# With no workspace bound, bash/python run in the agent
|
||||||
|
# workspace (agent_cwd), so the launch is sealed there.
|
||||||
roots = request_authority.resource_roots
|
roots = request_authority.resource_roots
|
||||||
if not roots and workspace:
|
if not roots:
|
||||||
roots = (FilesystemRoot.seal(workspace, owner=_normalized_owner(owner)),)
|
from src.tool_execution import default_agent_workspace
|
||||||
|
roots = (FilesystemRoot.seal(workspace or default_agent_workspace(),
|
||||||
|
owner=_normalized_owner(owner)),)
|
||||||
if len(roots) == 1:
|
if len(roots) == 1:
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
process_authority = replace(
|
process_authority = replace(
|
||||||
@@ -455,8 +459,10 @@ class ToolApprovalStore:
|
|||||||
if tool_name in NATIVE_FILESYSTEM_TOOLS and backend_operation is not None and isinstance(backend_operation.resource, NativeBackendResource):
|
if tool_name in NATIVE_FILESYSTEM_TOOLS and backend_operation is not None and isinstance(backend_operation.resource, NativeBackendResource):
|
||||||
try:
|
try:
|
||||||
roots = request_authority.resource_roots if request_authority is not None else ()
|
roots = request_authority.resource_roots if request_authority is not None else ()
|
||||||
if not roots and workspace and (request_authority is None or not request_authority.inherited):
|
if not roots and (request_authority is None or not request_authority.inherited):
|
||||||
roots = (FilesystemRoot.seal(workspace, owner=_normalized_owner(owner)),)
|
from src.tool_execution import default_agent_workspace
|
||||||
|
roots = (FilesystemRoot.seal(workspace or default_agent_workspace(),
|
||||||
|
owner=_normalized_owner(owner)),)
|
||||||
resource_operation = resolve_filesystem_operation(
|
resource_operation = resolve_filesystem_operation(
|
||||||
ExactOperation.normalize(tool_name, content), roots=roots, workspace=workspace or "",
|
ExactOperation.normalize(tool_name, content), roots=roots, workspace=workspace or "",
|
||||||
request_id=request_authority.request_id if request_authority is not None else "")
|
request_id=request_authority.request_id if request_authority is not None else "")
|
||||||
|
|||||||
@@ -1221,6 +1221,12 @@ def agent_cwd() -> str:
|
|||||||
if vetted is None:
|
if vetted is None:
|
||||||
raise RuntimeError("active workspace is not a safe real directory")
|
raise RuntimeError("active workspace is not a safe real directory")
|
||||||
return vetted
|
return vetted
|
||||||
|
return default_agent_workspace()
|
||||||
|
|
||||||
|
|
||||||
|
def default_agent_workspace() -> str:
|
||||||
|
"""The agent workspace under the data dir, where subprocesses run when no
|
||||||
|
workspace is bound."""
|
||||||
resolved = os.path.realpath(_AGENT_WORKDIR)
|
resolved = os.path.realpath(_AGENT_WORKDIR)
|
||||||
if resolved not in _agent_readable_data_subdirs() or vet_workspace(resolved) != resolved:
|
if resolved not in _agent_readable_data_subdirs() or vet_workspace(resolved) != resolved:
|
||||||
raise RuntimeError("agent workspace is not a safe real directory")
|
raise RuntimeError("agent workspace is not a safe real directory")
|
||||||
|
|||||||
@@ -79,3 +79,69 @@ async def test_inherited_authority_still_cannot_widen_to_ungranted_bash(tmp_path
|
|||||||
assert pending.process_operation is None
|
assert pending.process_operation is None
|
||||||
_, result = await _dispatch(child, "bash", "printf approved-run", exact)
|
_, result = await _dispatch(child, "bash", "printf approved-run", exact)
|
||||||
assert result.get("blocked") is True
|
assert result.get("blocked") is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def agent_workspace():
|
||||||
|
import os
|
||||||
|
from src.constants import AGENT_WORKSPACE_DIR
|
||||||
|
os.makedirs(AGENT_WORKSPACE_DIR, exist_ok=True)
|
||||||
|
return os.path.realpath(AGENT_WORKSPACE_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def test_approval_without_a_workspace_seals_a_launch_in_the_agent_workspace(agent_workspace):
|
||||||
|
# Agent mode with no folder picked is the default. bash already runs in the
|
||||||
|
# agent workspace there (agent_cwd), so that is where its approval seals.
|
||||||
|
authority = _authority("", "get_workspace")
|
||||||
|
assert authority.resource_roots == ()
|
||||||
|
|
||||||
|
pending, _ = _approve(authority, "bash", "pwd")
|
||||||
|
|
||||||
|
bound = pending.process_operation
|
||||||
|
assert bound is not None
|
||||||
|
assert bound.launch is not None
|
||||||
|
assert bound.launch.scope.root.path == agent_workspace
|
||||||
|
|
||||||
|
|
||||||
|
async def test_approved_bash_without_a_workspace_runs_in_the_agent_workspace(agent_workspace):
|
||||||
|
authority = _authority("", "get_workspace")
|
||||||
|
_, exact = _approve(authority, "bash", "pwd")
|
||||||
|
|
||||||
|
description, result = await _dispatch(authority, "bash", "pwd", exact)
|
||||||
|
|
||||||
|
assert result.get("failure_kind") != "resource_identity_denied", result
|
||||||
|
assert result.get("exit_code") == 0, result
|
||||||
|
containment = result.get("containment") or {}
|
||||||
|
assert containment.get("workspace", agent_workspace) == agent_workspace, containment
|
||||||
|
|
||||||
|
|
||||||
|
async def test_inherited_authority_without_a_workspace_still_cannot_widen(agent_workspace):
|
||||||
|
from dataclasses import replace
|
||||||
|
child = replace(_authority("", "get_workspace"), inherited=True)
|
||||||
|
pending, exact = _approve(child, "bash", "pwd")
|
||||||
|
|
||||||
|
assert pending.process_operation is None
|
||||||
|
_, result = await _dispatch(child, "bash", "pwd", exact)
|
||||||
|
assert result.get("blocked") is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_approved_write_without_a_workspace_lands_in_the_agent_workspace(agent_workspace):
|
||||||
|
# write_file had the same gap: with no workspace the approval sealed no
|
||||||
|
# root, so "approve" ended in "no sealed resource identity".
|
||||||
|
import os
|
||||||
|
target = "approved-write-6635.txt"
|
||||||
|
content = f"{target}\nhello"
|
||||||
|
authority = _authority("", "get_workspace")
|
||||||
|
pending, exact = _approve(authority, "write_file", content)
|
||||||
|
assert pending.resource_operation is not None
|
||||||
|
|
||||||
|
_, result = await _dispatch(authority, "write_file", content, exact)
|
||||||
|
|
||||||
|
assert result.get("failure_kind") != "resource_identity_denied", result
|
||||||
|
written = os.path.join(agent_workspace, target)
|
||||||
|
try:
|
||||||
|
with open(written) as fh:
|
||||||
|
assert fh.read().strip() == "hello"
|
||||||
|
finally:
|
||||||
|
if os.path.exists(written):
|
||||||
|
os.remove(written)
|
||||||
|
|||||||
Reference in New Issue
Block a user