feat(runtime): bind native filesystem operations to resource identities

This commit is contained in:
Alexandre Teixeira
2026-10-02 01:19:32 +01:00
parent 7aa891e5e6
commit ba3e631d58
8 changed files with 1393 additions and 15 deletions
+33 -7
View File
@@ -11,6 +11,7 @@ from pathlib import Path
import re
from uuid import uuid4
from src.agent_runtime.resources import FilesystemRoot, intersect_roots
from src.tool_policy import ToolPolicy, build_effective_tool_policy
from src.turn_contract import (
FAMILY_TOOLS, canonical_tool, requested_capabilities,
@@ -111,6 +112,9 @@ class RequestAuthority:
block_all: bool = False
disable_mcp: bool = False
inherited: bool = False
# None is only the trusted constructor's instruction to seal a workspace.
# Persisted/child authorities always carry an explicit tuple, including ().
resource_roots: tuple[FilesystemRoot, ...] | None = None
def __post_init__(self):
if (not isinstance(self.request_id, str) or not self.request_id
@@ -122,10 +126,23 @@ class RequestAuthority:
or any(not isinstance(n, str) or canonical_tool(n) != n for n in self.denied)
or any(type(v) is not bool for v in (self.block_all, self.disable_mcp, self.inherited))):
raise ValueError("Malformed request authority")
if self.resource_roots is None:
roots = ()
if self.workspace:
try:
roots = (FilesystemRoot.seal(self.workspace, owner=self.owner),)
except (OSError, ValueError, RuntimeError):
pass # An unresolved workspace grants no filesystem root.
object.__setattr__(self, "resource_roots", roots)
if (not isinstance(self.resource_roots, tuple)
or any(not isinstance(r, FilesystemRoot) or (r.owner and r.owner != self.owner)
for r in self.resource_roots)):
raise ValueError("Malformed request resource roots")
@classmethod
def empty(cls, *, owner=None, session_id=None, workspace=None):
return cls(uuid4().hex, _owner(owner), str(session_id or ""), str(workspace or ""))
return cls(uuid4().hex, _owner(owner), str(session_id or ""), str(workspace or ""),
resource_roots=())
def bound_to(self, *, owner=None, session_id=None, workspace=None):
return (self.owner == _owner(owner) and self.session_id == str(session_id or "")
@@ -150,12 +167,15 @@ class RequestAuthority:
if not isinstance(child, RequestAuthority):
raise TypeError("Child authority must be server-owned RequestAuthority")
grants = []
roots = ()
if (self.owner, self.session_id, self.workspace) == (child.owner, child.session_id, child.workspace):
theirs = {g.tool: g for g in child.grants}
grants = [g.intersect(theirs[g.tool]) for g in self.grants if g.tool in theirs]
roots = intersect_roots(self.resource_roots, child.resource_roots)
return replace(self, grants=tuple(grants), denied=self.denied | child.denied,
block_all=self.block_all or child.block_all,
disable_mcp=self.disable_mcp or child.disable_mcp, inherited=True)
disable_mcp=self.disable_mcp or child.disable_mcp, inherited=True,
resource_roots=roots)
def continuation(self, *, owner=None, session_id=None):
"""A server continuation may rebind a session, never change owner/grants."""
@@ -164,18 +184,19 @@ class RequestAuthority:
return replace(self, session_id=str(session_id or ""), inherited=True)
def to_dict(self):
return {"version": 1, "request_id": self.request_id, "owner": self.owner,
return {"version": 2, "request_id": self.request_id, "owner": self.owner,
"session_id": self.session_id, "workspace": self.workspace,
"grants": [{"tool": g.tool,
"actions": None if g.actions is None else sorted(g.actions),
"inputs": None if g.inputs is None else sorted(g.inputs)} for g in self.grants],
"denied": sorted(self.denied), "block_all": self.block_all,
"disable_mcp": self.disable_mcp, "inherited": self.inherited}
"disable_mcp": self.disable_mcp, "inherited": self.inherited,
"resource_roots": [r.to_dict() for r in self.resource_roots]}
@classmethod
def from_dict(cls, value):
if (not isinstance(value, dict) or type(value.get("version")) is not int
or value["version"] != 1):
or value["version"] not in {1, 2}):
raise ValueError("Unsupported authority snapshot")
def limits(value):
if value is None:
@@ -183,10 +204,14 @@ class RequestAuthority:
if not isinstance(value, list) or any(not isinstance(v, str) for v in value):
raise ValueError("Malformed authority limits")
return frozenset(value)
roots = value["resource_roots"] if value["version"] == 2 else []
if not isinstance(roots, list):
raise ValueError("Malformed request resource snapshot")
return cls(value["request_id"], value["owner"], value["session_id"], value["workspace"],
tuple(OperationGrant(g["tool"], limits(g["actions"]), limits(g["inputs"]))
for g in value["grants"]), limits(value["denied"]),
value["block_all"], value["disable_mcp"], value["inherited"])
value["block_all"], value["disable_mcp"], value["inherited"],
tuple(FilesystemRoot.from_dict(r) for r in roots))
_BROWSER_READ_ACTIONS = frozenset({"open", "navigate", "snapshot", "text", "read", "find",
@@ -397,7 +422,8 @@ def seal_task_authority(prompt, task_type, action, *, owner=None, parent_authori
parent = RequestAuthority.empty(owner=owner)
if parent is not None:
authority = parent.intersect(replace(authority, session_id=parent.session_id,
workspace=parent.workspace))
workspace=parent.workspace,
resource_roots=parent.resource_roots))
return _json({"task_input": [prompt, task_type, action], "authority": authority.to_dict()})
+203
View File
@@ -0,0 +1,203 @@
"""Resolve native filesystem selectors once, after operation admission.
Resolution produces inert bindings; the dispatcher still owns authority,
TurnContract, security and approval gates. No remote filesystem is resolved here.
"""
from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
import json
import os
from src.agent_runtime.authority import ExactOperation
from src.agent_runtime.resources import FilesystemResource, FilesystemRoot
from src.path_confinement import canonical_root, confine
NATIVE_FILESYSTEM_TOOLS = frozenset({
"read_file", "write_file", "edit_file", "apply_patch", "ls", "glob", "grep",
})
@dataclass(frozen=True)
class ResourceBinding:
role: str
resource: FilesystemResource
def __post_init__(self):
if self.role not in {"source", "target", "destination", "search_root"} or not isinstance(self.resource, FilesystemResource):
raise ValueError("Malformed operation resource binding")
@dataclass(frozen=True)
class BoundFilesystemOperation:
operation: ExactOperation
execution_input: str
bindings: tuple[ResourceBinding, ...]
# Empty only for inert proposal resolution without an originating request.
request_id: str = ""
def __post_init__(self):
if (not isinstance(self.operation, ExactOperation)
or not isinstance(self.execution_input, str)
or not isinstance(self.bindings, tuple) or not self.bindings
or any(not isinstance(b, ResourceBinding) for b in self.bindings)):
raise ValueError("Malformed resource-bound operation")
if not isinstance(self.request_id, str) or any(c in self.request_id for c in ("\0", "\n", "\r")):
raise ValueError("Malformed resource operation request identity")
if (self.operation.action in {"move", "rename"}
and (len(self.bindings) != 2 or {b.role for b in self.bindings} != {"source", "destination"}
or len({b.resource.path for b in self.bindings}) != 2
or next(b for b in self.bindings if b.role == "source").resource.identity is None)):
raise ValueError("Move/rename must bind distinct source and destination")
def validate(self):
for binding in self.bindings:
binding.resource.validate()
def to_dict(self):
return {"request_id": self.request_id, "tool": self.operation.transport_tool, "input": self.operation.input,
"execution_input": self.execution_input,
"bindings": [{"role": b.role, "resource": b.resource.to_dict()} for b in self.bindings]}
def resolve_path(self, selector, *, search=False):
"""Consume declared canonical targets; permit bounded search descendants."""
if not isinstance(selector, str):
raise ValueError("Resource selector must be a string")
value = selector.strip()
for binding in self.bindings:
resource = binding.resource
if value == resource.path or (search and not value and binding.role == "search_root"):
resource.validate()
return resource.path
if not search:
for binding in self.bindings:
resource = binding.resource
if binding.role == "search_root" and resource.identity.kind == "directory":
resource.validate()
try:
path = confine(resource.path, value)
return FilesystemResource.resolve(resource.root, path).path
except (ValueError, OSError, RuntimeError):
continue
raise ValueError("Path is not declared by the resource-bound operation")
def _resolve(roots, selector, *, workspace, allow_missing):
if not isinstance(selector, str) or not selector.strip():
raise ValueError("Resource path is required and must be a string")
value = selector.strip()
# The virtual alias belongs to the request workspace, even when a child
# narrows its root to a subdirectory of that workspace.
if value == "/workspace" or value.startswith("/workspace/"):
if not workspace:
raise ValueError("Workspace alias has no server-owned workspace")
base = canonical_root(workspace)
value = base if value == "/workspace" else os.path.join(base, value[len("/workspace/"):])
elif not os.path.isabs(os.path.expanduser(value)):
if workspace:
value = os.path.join(canonical_root(workspace), value)
elif len(roots) == 1:
value = os.path.join(roots[0].path, value)
else:
raise ValueError("Relative resource path has no unambiguous server root")
for root in roots:
try:
return FilesystemResource.resolve(root, value, allow_missing=allow_missing)
except (ValueError, OSError, RuntimeError):
continue
boundary = "the workspace" if workspace else "the sealed roots"
raise ValueError(f"Resource path is outside {boundary}, sensitive, missing or changed")
def resolve_filesystem_operation(operation, *, roots, workspace="", request_id=""):
"""Server adapter. This does not grant the operation or authorize its roots."""
if not isinstance(operation, ExactOperation) or operation.tool not in NATIVE_FILESYSTEM_TOOLS:
raise ValueError("Operation has no native filesystem adapter")
if (not isinstance(roots, tuple) or not roots
or any(not isinstance(r, FilesystemRoot) for r in roots)):
raise ValueError("Native filesystem operation requires a sealed resource root")
content = operation.input
args = json.loads(content) if content.lstrip().startswith("{") else None
if args is not None and not isinstance(args, dict):
raise ValueError("Filesystem input must be an object")
bindings = []
def bind(selector, role, *, missing=False):
resource = _resolve(roots, selector, workspace=workspace, allow_missing=missing)
bindings.append(ResourceBinding(role, resource))
return resource.path
tool = operation.tool
if tool == "apply_patch":
from src.agent_tools.filesystem_tools import _parse_agent_patch
if args is None:
patch = content
else:
variants = [args[k] for k in ("patch_text", "patchText", "patch") if k in args]
if not variants or any(not isinstance(p, str) or p != variants[0] for p in variants):
raise ValueError("Patch requires one unambiguous patch_text")
patch = variants[0]
ops = _parse_agent_patch(patch)
paths = [bind(op["path"], "destination" if op["kind"] == "add" else "target",
missing=op["kind"] == "add") for op in ops]
objects = [b.resource.identity for b in bindings if b.resource.identity is not None]
if len(set(paths)) != len(paths) or len(set(objects)) != len(objects):
raise ValueError("Patch targets resolve to the same resource")
path_iter = iter(paths)
lines = patch.replace("\r\n", "\n").replace("\r", "\n").split("\n")
for i, line in enumerate(lines):
for marker in ("*** Add File: ", "*** Update File: ", "*** Delete File: "):
if line.startswith(marker):
lines[i] = marker + next(path_iter)
break
execution_input = json.dumps({"patch_text": "\n".join(lines)}, sort_keys=True)
else:
search = tool in {"ls", "glob", "grep"}
if args is None:
if tool == "write_file":
path, _, body = content.partition("\n")
args = {"path": path.strip(), "content": body}
elif tool == "edit_file":
raise ValueError("edit_file requires a JSON object")
elif tool in {"glob", "grep"}:
args = {"pattern": content.strip()}
else:
args = {"path": content.split("\n", 1)[0].strip()}
selector = args.get("path", "" if search else None)
if search and selector == "":
if workspace:
selector = canonical_root(workspace)
elif len(roots) == 1:
selector = roots[0].path
else:
raise ValueError("Search root is unresolved")
args["path"] = bind(selector, "search_root" if search else
"source" if tool == "read_file" else "destination" if tool == "write_file" else "target",
missing=tool == "write_file")
execution_input = json.dumps(args, sort_keys=True, allow_nan=False)
bound = BoundFilesystemOperation(operation, execution_input, tuple(bindings), request_id)
bound.validate()
return bound
_ACTIVE: ContextVar[BoundFilesystemOperation | None] = ContextVar("resource_operation", default=None)
def active_resource_operation():
return _ACTIVE.get()
@contextmanager
def bind_resource_operation(operation):
if operation is not None and not isinstance(operation, BoundFilesystemOperation):
raise TypeError("Resource operation must be server-owned")
if operation is not None:
operation.validate()
token = _ACTIVE.set(operation)
try:
yield operation
finally:
_ACTIVE.reset(token)
+302
View File
@@ -0,0 +1,302 @@
"""Inert server-owned resource identities, independent of operation authority.
Filesystem observations detect replacement; they are not held kernel handles or
content/effect evidence. Other producers must supply their own incarnations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from enum import Enum
import os
from pathlib import Path
import stat
from src.agent_runtime.path_policy import _is_sensitive_path
from src.path_confinement import canonical_root, confine
def _text(value, label, *, optional=False):
if (not isinstance(value, str) or (not value and not optional)
or any(c in value for c in ("\0", "\n", "\r"))):
raise ValueError(f"Invalid resource {label}")
def _absolute(value):
_text(value, "path")
if not os.path.isabs(value) or os.path.normpath(value) != value:
raise ValueError("Resource path must be canonical and absolute")
def _control_plane_path(path):
# Execution snapshots/receipts are server state, even if a workspace root
# contains the data directory. A writable user file cannot mint authority.
from src.constants import BG_JOBS_DIR, BG_JOBS_FILE, CONTAINMENT_STATE_FILE
if path in {canonical_root(BG_JOBS_FILE), canonical_root(CONTAINMENT_STATE_FILE)}:
return True
return (Path(path).is_relative_to(canonical_root(BG_JOBS_DIR))
and path.endswith(".authority.json"))
class FilesystemScope(str, Enum):
WORKSPACE = "workspace"
SCRATCH = "scratch"
EXTERNAL = "external"
PRIVATE = "private"
class ResourceIdentityError(ValueError):
"""An observed execution resource has changed or cannot be resolved."""
@dataclass(frozen=True)
class FileObjectIdentity:
device: int
inode: int
kind: str
def __post_init__(self):
if (type(self.device) is not int or self.device < 0
or type(self.inode) is not int or self.inode <= 0
or self.kind not in {"file", "directory"}):
raise ValueError("Malformed filesystem object identity")
@classmethod
def observe(cls, path):
info = os.stat(path, follow_symlinks=False)
kind = ("file" if stat.S_ISREG(info.st_mode) else
"directory" if stat.S_ISDIR(info.st_mode) else None)
if kind is None:
raise ValueError("Filesystem resource must be a regular file or directory")
return cls(info.st_dev, info.st_ino, kind)
@dataclass(frozen=True)
class FilesystemRoot:
path: str
scope: FilesystemScope
identity: FileObjectIdentity
owner: str = ""
def __post_init__(self):
_absolute(self.path)
_text(self.owner, "owner", optional=True)
if (not isinstance(self.scope, FilesystemScope)
or not isinstance(self.identity, FileObjectIdentity)
or self.identity.kind != "directory"
or os.path.dirname(self.path) == self.path
or _is_sensitive_path(self.path)
or (self.scope is FilesystemScope.PRIVATE and not self.owner)):
raise ValueError("Malformed filesystem root identity")
@classmethod
def seal(cls, path, *, scope=FilesystemScope.WORKSPACE, owner=""):
root = canonical_root(path)
return cls(root, scope, FileObjectIdentity.observe(root), owner)
def validate(self):
try:
if canonical_root(self.path) != self.path or FileObjectIdentity.observe(self.path) != self.identity:
raise ResourceIdentityError("Filesystem root identity changed")
except (OSError, RuntimeError) as error:
raise ResourceIdentityError("Filesystem root identity is unresolved") from error
def to_dict(self):
return {**asdict(self), "scope": self.scope.value}
@classmethod
def from_dict(cls, value):
if not isinstance(value, dict) or set(value) != {"path", "scope", "identity", "owner"}:
raise ValueError("Malformed filesystem root snapshot")
return cls(value["path"], FilesystemScope(value["scope"]),
FileObjectIdentity(**value["identity"]), value["owner"])
@dataclass(frozen=True)
class PathObservation:
path: str
identity: FileObjectIdentity
def __post_init__(self):
_absolute(self.path)
if not isinstance(self.identity, FileObjectIdentity) or self.identity.kind != "directory":
raise ValueError("Malformed filesystem ancestor identity")
@dataclass(frozen=True)
class FilesystemResource:
root: FilesystemRoot
path: str
identity: FileObjectIdentity | None
ancestors: tuple[PathObservation, ...]
def __post_init__(self):
_absolute(self.path)
if (not isinstance(self.root, FilesystemRoot)
or not Path(self.path).is_relative_to(self.root.path)
or (self.identity is not None and not isinstance(self.identity, FileObjectIdentity))
or not isinstance(self.ancestors, tuple)
or any(not isinstance(a, PathObservation) for a in self.ancestors)
or not self.ancestors
or self.ancestors[0] != PathObservation(self.root.path, self.root.identity)):
raise ValueError("Malformed filesystem resource identity")
parent = Path(self.root.path)
expected = [str(parent)]
for part in Path(self.path).relative_to(self.root.path).parts[:-1]:
parent /= part
expected.append(str(parent))
if ([a.path for a in self.ancestors] != expected[:len(self.ancestors)]
or (self.identity is not None and len(self.ancestors) != len(expected))):
raise ValueError("Malformed filesystem ancestor chain")
@classmethod
def resolve(cls, root, selector, *, allow_missing=False):
root.validate()
# Only this server-owned workspace root supplies the virtual alias.
if not isinstance(selector, str):
raise ValueError("Resource path must be a string")
value = selector.strip()
if root.scope is FilesystemScope.WORKSPACE:
if value == "/workspace":
value = root.path
elif value.startswith("/workspace/"):
value = os.path.join(root.path, value[len("/workspace/"):])
path = confine(root.path, value)
if _is_sensitive_path(path) or _control_plane_path(path):
raise ValueError("Resource path is sensitive")
ancestors = [PathObservation(root.path, root.identity)]
relative = Path(path).relative_to(root.path)
parent = Path(root.path)
missing_parent = False
for part in relative.parts[:-1]:
parent /= part
try:
observed = FileObjectIdentity.observe(parent)
except FileNotFoundError:
missing_parent = True
break
ancestors.append(PathObservation(str(parent), observed))
try:
identity = None if missing_parent else FileObjectIdentity.observe(path)
except FileNotFoundError:
identity = None
if identity is None and not allow_missing:
raise ValueError("Filesystem resource is unresolved or missing")
return cls(root, path, identity, tuple(ancestors))
def validate(self):
try:
if self.resolve(self.root, self.path, allow_missing=self.identity is None) != self:
raise ResourceIdentityError("Filesystem resource identity changed")
except (ValueError, OSError, RuntimeError) as error:
raise ResourceIdentityError("Filesystem resource identity changed or is unresolved") from error
def to_dict(self):
return asdict(self)
def intersect_roots(parent, child):
"""Keep the narrower root only when the observed parent's identity agrees."""
result = []
for left in parent:
for right in child:
if (left.scope, left.owner) != (right.scope, right.owner):
continue
if left == right:
result.append(left)
continue
try:
if Path(right.path).is_relative_to(left.path):
# A newly sealed child may not renew a replaced parent root.
left.validate()
right.validate()
result.append(right)
elif Path(left.path).is_relative_to(right.path):
left.validate()
right.validate()
result.append(left)
except (OSError, ValueError, RuntimeError):
continue
return tuple(dict.fromkeys(result))
@dataclass(frozen=True)
class ProcessResource:
namespace: str
incarnation: str
owner: str
pid: int
start_token: str
job_id: str = ""
containment_id: str = ""
namespace_pid: int | None = None
namespace_start_token: str = ""
def __post_init__(self):
for name in ("namespace", "incarnation", "owner", "start_token"):
_text(getattr(self, name), name)
for name in ("job_id", "containment_id", "namespace_start_token"):
_text(getattr(self, name), name, optional=True)
if (type(self.pid) is not int or self.pid <= 0
or (self.namespace_pid is not None and
(type(self.namespace_pid) is not int or self.namespace_pid <= 0))
or bool(self.namespace_pid) != bool(self.namespace_start_token)):
raise ValueError("Malformed process resource identity")
@dataclass(frozen=True)
class BrowserProducer:
namespace: str
owner: str
thread_id: str
session_id: str
incarnation: str
def __post_init__(self):
for name in ("namespace", "owner", "thread_id", "session_id", "incarnation"):
_text(getattr(self, name), name)
@dataclass(frozen=True)
class BrowserPageResource:
producer: BrowserProducer
page_id: str
navigation_generation: int
observed_url: str
def __post_init__(self):
if (not isinstance(self.producer, BrowserProducer)
or type(self.navigation_generation) is not int or self.navigation_generation < 0):
raise ValueError("Malformed browser page identity")
_text(self.page_id, "page")
_text(self.observed_url, "observed URL")
@dataclass(frozen=True)
class ExternalResource:
namespace: str
endpoint_id: str
server_id: str
tool_id: str
incarnation: str
external: bool = True
def __post_init__(self):
for name in ("namespace", "endpoint_id", "server_id", "tool_id", "incarnation"):
_text(getattr(self, name), name)
if self.external is not True:
raise ValueError("External resource cannot attest local containment")
@dataclass(frozen=True)
class OwnedResource:
namespace: str
owner: str
thread_id: str
collection: str
record_id: str
revision: str = ""
def __post_init__(self):
for name in ("namespace", "owner", "thread_id", "collection", "record_id"):
_text(getattr(self, name), name)
_text(self.revision, "revision", optional=True)
+27 -1
View File
@@ -15,7 +15,7 @@ import secrets
import threading
import time
from dataclasses import dataclass, field
from typing import Any
from typing import Any, TYPE_CHECKING
from src.tool_approval_scopes import (
CHAT_SESSION_APPROVAL_DECISION,
@@ -27,6 +27,9 @@ from src.tool_approval_scopes import (
from src.tool_capabilities import ToolCapabilities, capabilities_for_action
from src.agent_runtime.authority import RequestAuthority
if TYPE_CHECKING:
from src.agent_runtime.resource_binding import BoundFilesystemOperation
DEFAULT_APPROVAL_TTL_SECONDS = 10 * 60
DEFAULT_MAX_PENDING_APPROVALS = 2048
@@ -119,6 +122,7 @@ def _binding_payload(
effects: tuple[str, ...],
result_integrity: str,
request_authority: RequestAuthority | None = None,
resource_operation=None,
) -> dict[str, Any]:
return {
"owner": _normalized_owner(owner),
@@ -140,6 +144,7 @@ def _binding_payload(
"effects": list(effects),
"result_integrity": str(result_integrity),
"request_authority": request_authority.to_dict() if request_authority is not None else None,
"resource_operation": resource_operation.to_dict() if resource_operation is not None else None,
}
@@ -169,6 +174,8 @@ class PendingToolApproval:
# is never displayed or treated as authorization for the sealed action.
request_text: str = ""
request_authority: RequestAuthority | None = None
# Server-resolved targets at proposal time; never read from the approval UI.
resource_operation: BoundFilesystemOperation | None = None
def public_payload(self, *, reason: str | None = None) -> dict[str, Any]:
return {
@@ -278,6 +285,7 @@ class ExactToolApproval:
effects=effects,
result_integrity=result_integrity,
request_authority=self.pending.request_authority,
resource_operation=self.pending.resource_operation,
)
return _canonical_digest(expected) == self.pending.digest
@@ -366,6 +374,22 @@ class ToolApprovalStore:
if request_authority is not None and not isinstance(request_authority, RequestAuthority):
raise TypeError("Approval authority must be server-owned RequestAuthority")
now = time.time()
from src.agent_runtime.authority import ExactOperation
from src.agent_runtime.resource_binding import NATIVE_FILESYSTEM_TOOLS, resolve_filesystem_operation
from src.agent_runtime.resources import FilesystemRoot
resource_operation = None
if tool_name in NATIVE_FILESYSTEM_TOOLS:
try:
roots = request_authority.resource_roots if request_authority is not None else ()
if not roots and workspace:
roots = (FilesystemRoot.seal(workspace, owner=_normalized_owner(owner)),)
resource_operation = resolve_filesystem_operation(
ExactOperation.normalize(tool_name, content), roots=roots, workspace=workspace or "",
request_id=request_authority.request_id if request_authority is not None else "")
except (ValueError, TypeError, OSError, RuntimeError):
# An unresolved proposal may be displayed, but it cannot execute
# after approval by reconstructing its targets at claim time.
pass
effects = tuple(sorted(effect.value for effect in capabilities.effects))
result_integrity = capabilities.result_integrity.value
payload = _binding_payload(
@@ -384,6 +408,7 @@ class ToolApprovalStore:
effects=effects,
result_integrity=result_integrity,
request_authority=request_authority,
resource_operation=resource_operation,
)
pending = PendingToolApproval(
approval_id=secrets.token_urlsafe(32),
@@ -408,6 +433,7 @@ class ToolApprovalStore:
continuation_query=payload["continuation_query"],
request_text=str(request_text or ""),
request_authority=request_authority,
resource_operation=resource_operation,
)
with self._lock:
self._purge_expired_locked(now)
+75 -4
View File
@@ -19,7 +19,7 @@ import secrets
import sys
import time
from contextlib import contextmanager
from dataclasses import dataclass
from dataclasses import dataclass, replace
from typing import Any, Awaitable, Callable, Dict, Iterator, Optional, Tuple
@@ -43,6 +43,12 @@ from src.constants import (
)
from src.path_confinement import canonical_root, confine, is_inside
from src.tool_utils import _truncate, get_mcp_manager
from src.tool_types import ToolBlock
from src.agent_runtime.resource_binding import (
NATIVE_FILESYSTEM_TOOLS, active_resource_operation, bind_resource_operation,
resolve_filesystem_operation,
)
from src.agent_runtime.resources import ResourceIdentityError
class _MissingToolSecurityContext:
@@ -830,6 +836,9 @@ def _resolve_tool_path(raw_path: str) -> str:
When a workspace is active for this turn, paths are confined to it instead
of the default allowlist (see _resolve_tool_path_in_workspace).
"""
resource_operation = active_resource_operation()
if resource_operation is not None:
return resource_operation.resolve_path(raw_path)
ws = get_active_workspace()
if ws:
return _resolve_tool_path_in_workspace(ws, raw_path)
@@ -972,6 +981,9 @@ def _resolve_search_root(raw_path: str) -> str:
primary root (project data dir) and a supplied path is confined by the
global allowlist + sensitive-file policy.
"""
resource_operation = active_resource_operation()
if resource_operation is not None:
return resource_operation.resolve_path(raw_path, search=True)
raw = (raw_path or "").strip()
ws = get_active_workspace()
if ws:
@@ -1237,6 +1249,7 @@ async def _direct_fallback(
"disabled_tools": frozenset(disabled_tools or ()),
"tool_policy": tool_policy,
"request_authority": active_request_authority(),
"resource_operation": active_resource_operation(),
}
from src.agent_tools import TOOL_HANDLERS
@@ -1364,6 +1377,41 @@ async def execute_tool_block(
"exit_code": 1, "failure_kind": "turn_contract_denied",
}
# External executors require their own adapters. Local observations must
# never stand in for remote resource or containment identities.
execution_bridge = get_active_execution_bridge()
transport = operation.transport_tool
external_resource_call = (
(execution_bridge is not None and transport in execution_bridge.supported_tools)
or (transport in _ROUTED_BRIDGE_TOOLS and _client_bridge(client_runtime_context) is not None)
or (transport == "apply_patch" and _tui_host_bridge_patch_url(client_runtime_context))
)
resource_operation = None
if operation.tool in NATIVE_FILESYSTEM_TOOLS and not external_resource_call:
try:
roots = authority.resource_roots
approved_resource = exact_approval.pending.resource_operation if exact_approval is not None else None
if exact_approval is not None and approved_resource is None:
raise ValueError("Approved filesystem action has no sealed resource identity")
if exact_admission and not roots and approved_resource is not None:
# This single exact action can use only the roots sealed with
# its proposal. The request/child authority is never widened.
roots = tuple(dict.fromkeys(b.resource.root for b in approved_resource.bindings))
if any(r.owner and r.owner != authority.owner for r in roots):
raise ValueError("Filesystem resource owner differs from request authority")
resource_operation = resolve_filesystem_operation(
operation, roots=roots, workspace=authority.workspace, request_id=authority.request_id)
if approved_resource is not None:
if approved_resource.request_id and approved_resource.request_id != authority.request_id:
raise ValueError("Approved resource belongs to another request")
if replace(resource_operation, request_id=approved_resource.request_id) != approved_resource:
raise ValueError("Approved filesystem resource identity changed")
except (ValueError, TypeError, OSError, RuntimeError) as error:
return f"{transport}: BLOCKED", {
"error": str(error), "exit_code": 1, "blocked": True,
"failure_kind": "resource_identity_denied",
}
approval_claimed = False
if exact_approval is not None:
if (
@@ -1450,9 +1498,9 @@ async def execute_tool_block(
token = _active_workspace.set(workspace or None)
try:
with bind_request_authority(authority):
with bind_request_authority(authority), bind_resource_operation(resource_operation):
output = await _execute_tool_block_impl(
block,
ToolBlock(transport, resource_operation.execution_input) if resource_operation is not None else block,
session_id=session_id,
disabled_tools=disabled_tools,
owner=owner,
@@ -1483,6 +1531,11 @@ async def execute_tool_block(
getattr(block, "content", None),
)
return output
except ResourceIdentityError as error:
return f"{transport}: BLOCKED", {
"error": str(error), "exit_code": 1, "blocked": True,
"failure_kind": "resource_identity_denied",
}
finally:
_active_workspace.reset(token)
@@ -1614,6 +1667,7 @@ async def _execute_tool_block_impl(
bridge_owns_tool = (
execution_bridge is not None
and tool in execution_bridge.supported_tools
and active_resource_operation() is None
)
# Public-owner restrictions protect tools executed by this deployment.
@@ -1673,7 +1727,8 @@ async def _execute_tool_block_impl(
},
)
if tool in _ROUTED_BRIDGE_TOOLS and _client_bridge(client_runtime_context) is not None:
if (active_resource_operation() is None and tool in _ROUTED_BRIDGE_TOOLS
and _client_bridge(client_runtime_context) is not None):
return await dispatched(_route_tool_via_bridge(tool, content, session_id, client_runtime_context))
# Background execution: a `bash` block whose first line is the `#!bg`
@@ -1719,6 +1774,22 @@ async def _execute_tool_block_impl(
from src.ai_interaction import do_generate_image
desc = "generate_image"
result = await dispatched(do_generate_image(content, session_id=session_id, owner=owner))
elif (tool in NATIVE_FILESYSTEM_TOOLS
and (active_resource_operation() is not None or tool != "apply_patch"
or not _tui_host_bridge_patch_url(client_runtime_context))):
if active_resource_operation() is None:
return f"{tool}: BLOCKED", {
"error": "Native filesystem dispatch has no bound resource operation",
"exit_code": 1, "blocked": True, "failure_kind": "resource_identity_denied",
}
# Backend selection is pinned. MCP connection availability cannot
# redirect an admitted native resource to a different filesystem.
original = active_resource_operation().operation.input
desc = f"{tool}: {original.split(chr(10))[0][:80]}"
result = await dispatched(_direct_fallback(tool, content, owner=owner, session_id=session_id)) \
or {"error": f"{tool}: execution failed", "exit_code": 1}
if tool == "edit_file":
desc = result.get("output") or result.get("error") or "edit_file"
elif tool in _MCP_TOOL_MAP:
first_line = content.split(chr(10))[0][:80]
desc = f"{tool}: {first_line}"