feat(runtime): bind remote and owned resources to authority

This commit is contained in:
Alexandre Teixeira
2026-10-02 03:00:46 +01:00
parent ba3e631d58
commit 571f685ad5
23 changed files with 2491 additions and 74 deletions
+51 -12
View File
@@ -11,7 +11,10 @@ from pathlib import Path
import re
from uuid import uuid4
from src.agent_runtime.resources import FilesystemRoot, intersect_roots
from src.agent_runtime.resources import (
FilesystemRoot, ExternalResource, NativeBackendResource, OwnedScope,
backend_from_dict, intersect_roots, seal_owned_scopes,
)
from src.tool_policy import ToolPolicy, build_effective_tool_policy
from src.turn_contract import (
FAMILY_TOOLS, canonical_tool, requested_capabilities,
@@ -115,6 +118,8 @@ class RequestAuthority:
# 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
backend_resources: tuple[ExternalResource | NativeBackendResource, ...] | None = None
owned_scopes: tuple[OwnedScope, ...] | None = None
def __post_init__(self):
if (not isinstance(self.request_id, str) or not self.request_id
@@ -138,11 +143,24 @@ class RequestAuthority:
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")
if self.backend_resources is None:
from src.agent_runtime.remote_resources import seal_backends
object.__setattr__(self, "backend_resources", seal_backends((g.tool for g in self.grants), owner=self.owner))
if self.owned_scopes is None:
object.__setattr__(self, "owned_scopes", seal_owned_scopes(
self.owner, self.session_id, (g.tool for g in self.grants)))
if (not isinstance(self.backend_resources, tuple)
or any(not isinstance(r, (ExternalResource, NativeBackendResource))
or (isinstance(r, ExternalResource) and r.owner and r.owner != self.owner) for r in self.backend_resources)
or not isinstance(self.owned_scopes, tuple)
or any(not isinstance(s, OwnedScope) or (s.owner, s.thread_id) != (self.owner, self.session_id)
for s in self.owned_scopes)):
raise ValueError("Malformed backend or owned resource scope")
@classmethod
def empty(cls, *, owner=None, session_id=None, workspace=None):
return cls(uuid4().hex, _owner(owner), str(session_id or ""), str(workspace or ""),
resource_roots=())
resource_roots=(), backend_resources=(), owned_scopes=())
def bound_to(self, *, owner=None, session_id=None, workspace=None):
return (self.owner == _owner(owner) and self.session_id == str(session_id or "")
@@ -168,35 +186,44 @@ class RequestAuthority:
raise TypeError("Child authority must be server-owned RequestAuthority")
grants = []
roots = ()
backends = ()
owned = ()
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)
backends = tuple(r for r in self.backend_resources if r in child.backend_resources)
owned = tuple(s for left in self.owned_scopes for right in child.owned_scopes
if (s := left.intersect(right)) is not None)
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,
resource_roots=roots)
resource_roots=roots, backend_resources=backends, owned_scopes=owned)
def continuation(self, *, owner=None, session_id=None):
"""A server continuation may rebind a session, never change owner/grants."""
if self.owner != _owner(owner):
return RequestAuthority.empty(owner=owner, session_id=session_id)
return replace(self, session_id=str(session_id or ""), inherited=True)
rebound = str(session_id or "")
return replace(self, session_id=rebound, inherited=True,
owned_scopes=tuple(replace(s, thread_id=rebound) for s in self.owned_scopes) if rebound else ())
def to_dict(self):
return {"version": 2, "request_id": self.request_id, "owner": self.owner,
return {"version": 3, "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,
"resource_roots": [r.to_dict() for r in self.resource_roots]}
"resource_roots": [r.to_dict() for r in self.resource_roots],
"backend_resources": [r.to_dict() for r in self.backend_resources],
"owned_scopes": [s.to_dict() for s in self.owned_scopes]}
@classmethod
def from_dict(cls, value):
if (not isinstance(value, dict) or type(value.get("version")) is not int
or value["version"] not in {1, 2}):
or value["version"] not in {1, 2, 3}):
raise ValueError("Unsupported authority snapshot")
def limits(value):
if value is None:
@@ -204,14 +231,19 @@ 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 []
roots = value["resource_roots"] if value["version"] >= 2 else []
if not isinstance(roots, list):
raise ValueError("Malformed request resource snapshot")
backends = value["backend_resources"] if value["version"] == 3 else []
owned = value["owned_scopes"] if value["version"] == 3 else []
if not isinstance(backends, list) or not isinstance(owned, list):
raise ValueError("Malformed request resource scope 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"],
tuple(FilesystemRoot.from_dict(r) for r in roots))
tuple(FilesystemRoot.from_dict(r) for r in roots),
tuple(backend_from_dict(r) for r in backends), tuple(OwnedScope.from_dict(s) for s in owned))
_BROWSER_READ_ACTIONS = frozenset({"open", "navigate", "snapshot", "text", "read", "find",
@@ -262,7 +294,7 @@ def interpret_request(request_text, *, history=(), workspace=None, active_docume
def create_request_authority(request_text, *, owner=None, session_id=None, workspace=None,
history=(), policy=None, active_document=False,
image_attachment=False, capabilities=None):
image_attachment=False, capabilities=None, client_runtime_context=None):
"""Deterministic server policy over semantic facts, never schema inventory."""
if not isinstance(request_text, str):
raise TypeError("Authority requires trusted request text")
@@ -298,6 +330,10 @@ def create_request_authority(request_text, *, owner=None, session_id=None, works
grants.append(OperationGrant(name, actions, inputs))
authority = RequestAuthority(uuid4().hex, _owner(owner), str(session_id or ""),
str(workspace or ""), tuple(grants))
if client_runtime_context is not None:
from src.agent_runtime.remote_resources import seal_backends
authority = replace(authority, backend_resources=seal_backends(
(g.tool for g in authority.grants), context=client_runtime_context, owner=authority.owner))
return authority.restrict(policy or build_effective_tool_policy(last_user_message=request_text))
@@ -385,7 +421,8 @@ def with_request_authority(func):
owner=parameters.get("owner"), session_id=parameters.get("session_id"),
workspace=parameters.get("workspace"),
history=getattr(parameters.get("history_session"), "history", ()) or (),
active_document=bool(parameters.get("active_document")))
active_document=bool(parameters.get("active_document")),
client_runtime_context=parameters.get("client_runtime_context"))
if not isinstance(authority, RequestAuthority):
raise TypeError("Missing or malformed server request authority")
if parent is None and parameters.get("exact_approval") is not None:
@@ -423,7 +460,9 @@ def seal_task_authority(prompt, task_type, action, *, owner=None, parent_authori
if parent is not None:
authority = parent.intersect(replace(authority, session_id=parent.session_id,
workspace=parent.workspace,
resource_roots=parent.resource_roots))
resource_roots=parent.resource_roots,
backend_resources=parent.backend_resources,
owned_scopes=parent.owned_scopes))
return _json({"task_input": [prompt, task_type, action], "authority": authority.to_dict()})
+451
View File
@@ -0,0 +1,451 @@
"""Resolve owned selectors before execution and consume exact server identities."""
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
import json
import re
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from src.agent_runtime.authority import ExactOperation
from src.agent_runtime.resources import (
FilesystemResource, FilesystemRoot, FilesystemScope, OwnedResource,
OWNED_TOOL_NAMESPACES, ResourceIdentityError,
)
def _args(content):
if not isinstance(json.loads(content or "{}"), dict):
raise ResourceIdentityError("Owned resource arguments must be an object")
from src.tools._common import _parse_tool_args
value = _parse_tool_args(content)
if not isinstance(value, dict):
raise ResourceIdentityError("Owned resource arguments must be an object")
return dict(value)
def _selector(args, keys):
values = [args[k] for k in keys if k in args and args[k] not in (None, "")]
if any(not isinstance(v, str) or not v.strip() for v in values):
raise ResourceIdentityError("Record selectors must be strings")
values = [v.strip() for v in values]
if len(set(values)) > 1:
raise ResourceIdentityError("Conflicting record aliases")
return values[0] if values else ""
def _revision(row, namespace):
created = getattr(row, "created_at", None)
updated = getattr(row, "updated_at", None)
if created is None or not hasattr(created, "isoformat") or updated is None or not hasattr(updated, "isoformat"):
raise ResourceIdentityError("Record has no observable revision")
version = getattr(row, "version_count", "") if namespace == "documents" else ""
if namespace == "documents" and type(version) is not int:
raise ResourceIdentityError("Document version is unresolved")
return f"{created.isoformat()}:{updated.isoformat()}:{version}"
def _record(namespace, owner, thread, row):
if (getattr(row, "owner", None) != owner or not isinstance(getattr(row, "id", None), str)
or row.id in {"", "*"}):
raise ResourceIdentityError("Record ownership is unresolved")
linked = str(getattr(row, "session_id", "") or "") if namespace == "documents" else row.id if namespace == "threads" else ""
return OwnedResource(namespace, owner, thread, namespace, row.id, _revision(row, namespace), linked)
def _row(namespace, identifier, owner):
from core.database import SessionLocal, Document, Session, Note
model = {"documents": Document, "threads": Session, "notes": Note}[namespace]
db = SessionLocal()
try:
row = db.query(model).filter(model.id == identifier, model.owner == owner).first()
if row is None or (namespace == "documents" and not row.is_active):
raise ResourceIdentityError("Owned record is missing or inaccessible")
db.expunge(row)
return row
finally:
db.close()
@dataclass(frozen=True)
class AttachmentResource:
record: OwnedResource
file: FilesystemResource
def __post_init__(self):
if not isinstance(self.record, OwnedResource) or not isinstance(self.file, FilesystemResource) or self.file.root.owner != self.record.owner:
raise ValueError("Malformed attachment identity")
def to_dict(self):
return {"record": self.record.to_dict(), "file": self.file.to_dict()}
def _attachment(identifier, owner, thread):
from src.tool_utils import get_upload_handler
handler = get_upload_handler()
if handler is None:
raise ResourceIdentityError("Attachment store is unavailable")
info = handler.resolve_upload(identifier, owner=owner, allow_admin=False)
if not isinstance(info, dict) or info.get("id") != identifier or info.get("owner") != owner:
raise ResourceIdentityError("Attachment ownership is unresolved")
root = FilesystemRoot.seal(handler.upload_dir, scope=FilesystemScope.PRIVATE, owner=owner)
file = FilesystemResource.resolve(root, info.get("path"))
if file.identity.kind != "file":
raise ResourceIdentityError("Attachment must identify a file")
revision = str(info.get("checksum_sha256") or info.get("hash") or info.get("uploaded_at") or "")
if not revision:
raise ResourceIdentityError("Attachment has no observable revision")
return AttachmentResource(OwnedResource("attachments", owner, thread, "attachments", identifier, revision), file)
_VAULT_RECORDS = {}
def _vault_revision(cfg, owner):
from src.agent_runtime.remote_resources import endpoint_identity, configuration_incarnation
if not isinstance(cfg, dict) or cfg.get("owner") != owner:
raise ResourceIdentityError("Vault configuration has no matching explicit owner")
endpoint = endpoint_identity(cfg.get("server_url") or cfg.get("url") or "")
return endpoint + ":" + configuration_incarnation((cfg.get("server_url") or cfg.get("url"), cfg.get("email"), cfg.get("unlocked_at"), cfg.get("session")))
def observe_vault_records(owner, cfg, records):
"""Only a server search response produces record observations, not grants."""
from src.tools.vault import _load_vault_config
from src.agent_runtime.remote_resources import configuration_incarnation
from uuid import UUID
revision = _vault_revision(cfg, owner)
if _vault_revision(_load_vault_config(), owner) != revision or not isinstance(records, list):
raise ResourceIdentityError("Vault producer configuration changed")
observed = {}
for row in records:
if not isinstance(row, dict):
raise ResourceIdentityError("Malformed vault producer record")
try:
identifier = str(UUID(row.get("id", "")))
except (ValueError, TypeError, AttributeError) as error:
raise ResourceIdentityError("Vault producer record has no exact UUID") from error
if identifier in observed or not isinstance(row.get("name", ""), str):
raise ResourceIdentityError("Ambiguous vault producer identity")
observed[identifier] = (row.get("name", ""), configuration_incarnation(json.dumps(row, sort_keys=True, allow_nan=False)))
catalog = _VAULT_RECORDS.setdefault((owner, revision), {})
catalog.update(observed)
def _vault_resource(owner, thread, identifier):
from src.tools.vault import _load_vault_config
revision = _vault_revision(_load_vault_config(), owner)
if identifier != "*":
record = _VAULT_RECORDS.get((owner, revision), {}).get(identifier)
if record is None:
raise ResourceIdentityError("Vault record has no server observation; search the owner vault first")
revision += ":" + record[1]
return OwnedResource("vault", owner, thread, "vault", identifier, revision)
def _vault_selector(owner, selector):
from src.tools.vault import _load_vault_config
revision = _vault_revision(_load_vault_config(), owner)
rows = _VAULT_RECORDS.get((owner, revision), {})
if selector in rows:
return selector
matches = [identifier for identifier, (name, _) in rows.items()
if identifier.startswith(selector) or name == selector]
if not selector or len(matches) != 1:
raise ResourceIdentityError("Vault selector is missing or ambiguous")
return matches[0]
def _memory_record(identifier, owner, thread, *, prefix=False):
from src.ai_interaction import _memory_manager
if _memory_manager is None:
raise ResourceIdentityError("Memory store is unavailable")
rows = [row for row in _memory_manager.load(owner=owner) if isinstance(row, dict)
and row.get("owner") == owner and isinstance(row.get("id"), str)
and (row["id"].startswith(identifier) if prefix else row["id"] == identifier)]
if len(rows) != 1 or not identifier or rows[0].get("timestamp") is None or rows[0]["id"] in {"", "*"}:
raise ResourceIdentityError("Memory selector is missing or ambiguous")
from src.agent_runtime.remote_resources import configuration_incarnation
row = rows[0]
# A same-second edit still changes the private revision without serializing content.
revision = configuration_incarnation(json.dumps(row, sort_keys=True, allow_nan=False))
return OwnedResource("memory", owner, thread, "memory", row["id"], revision)
@dataclass(frozen=True)
class BoundOwnedOperation:
operation: "ExactOperation"
execution_input: str
request_id: str
owner: str
thread_id: str
resources: tuple[OwnedResource, ...]
attachments: tuple[AttachmentResource, ...] = ()
document_id: str = ""
document_version: int | None = None
document_digest: str = ""
def __post_init__(self):
from src.agent_runtime.authority import ExactOperation
if (not isinstance(self.operation, ExactOperation) or not self.owner or not self.thread_id
or any(not isinstance(v, str) for v in (self.execution_input, self.request_id, self.owner, self.thread_id, self.document_id, self.document_digest))
or not isinstance(self.resources, tuple) or not self.resources
or any(not isinstance(r, OwnedResource) or (r.owner, r.thread_id) != (self.owner, self.thread_id) for r in self.resources)
or not isinstance(self.attachments, tuple) or any(not isinstance(a, AttachmentResource) for a in self.attachments)):
raise ValueError("Malformed owned resource operation")
namespace = OWNED_TOOL_NAMESPACES.get(self.operation.tool)
if (any(r.namespace != namespace or r.collection != namespace or (r.record_id != "*" and not r.revision) for r in self.resources)
or tuple(a.record for a in self.attachments) != tuple(r for r in self.resources if r.namespace == "attachments")):
raise ValueError("Malformed owned resource identity")
if self.document_id:
if (type(self.document_version) is not int or self.document_version < 1
or not re.fullmatch(r"[0-9a-f]{64}", self.document_digest)
or not any(r.namespace == "documents" and r.record_id == self.document_id for r in self.resources)):
raise ValueError("Malformed document binding")
elif any(r.namespace == "documents" and r.record_id != "*" for r in self.resources):
raise ValueError("Missing document binding")
def to_dict(self):
from src.agent_runtime.remote_resources import configuration_incarnation
return {"request_id": self.request_id, "owner": self.owner, "thread_id": self.thread_id,
"tool": self.operation.transport_tool,
"execution_input_digest": configuration_incarnation(self.execution_input),
"resources": [r.to_dict() for r in self.resources],
"attachments": [a.to_dict() for a in self.attachments],
"document_id": self.document_id, "document_version": self.document_version,
"document_digest": self.document_digest}
def validate(self):
for resource in self.resources:
if resource.record_id == "*":
if resource.namespace == "vault" and _vault_resource(self.owner, self.thread_id, "*") != resource:
raise ResourceIdentityError("Vault identity changed")
continue
if resource.namespace == "attachments":
expected = next((a for a in self.attachments if a.record == resource), None)
if expected is None or _attachment(resource.record_id, self.owner, self.thread_id) != expected:
raise ResourceIdentityError("Attachment identity changed")
expected.file.validate()
elif resource.namespace == "vault":
if _vault_resource(self.owner, self.thread_id, resource.record_id) != resource:
raise ResourceIdentityError("Vault identity changed")
elif resource.namespace == "memory":
if _memory_record(resource.record_id, self.owner, self.thread_id) != resource:
raise ResourceIdentityError("Memory identity changed")
elif _record(resource.namespace, self.owner, self.thread_id,
_row(resource.namespace, resource.record_id, self.owner)) != resource:
raise ResourceIdentityError("Owned record identity changed")
def needs_owned_binding(operation):
if operation.tool == "app_api":
# The generic internal-token bridge must not bypass migrated owner
# namespaces. Dedicated tools carry their typed record operations.
from urllib.parse import unquote, urlsplit
import posixpath
args = _args(operation.input)
path = args.get("path", "")
if not isinstance(path, str):
raise ResourceIdentityError("Malformed internal resource selector")
for _ in range(4):
decoded = unquote(path)
if decoded == path:
break
path = decoded
if "%" in path or "\\" in path:
raise ResourceIdentityError("Unresolved internal resource selector")
path = posixpath.normpath(urlsplit(path).path)
private = {"document", "documents", "session", "sessions", "history", "chat", "chats",
"notes", "memory", "vault", "upload", "uploads", "attachments"}
segments = path.strip("/").split("/")
if len(segments) >= 2 and segments[0] == "api" and segments[1].casefold() in private:
raise ResourceIdentityError("Owned records require a dedicated resource-bound tool")
return False
if operation.tool not in OWNED_TOOL_NAMESPACES:
return False
if operation.tool in {"extract_text", "inspect_media", "transcribe_media"}:
return "odysseus://attachment/" in operation.input
return True
def resolve_owned_operation(operation, *, owner, thread_id, request_id="", document_id=None):
if not owner or not thread_id:
raise ResourceIdentityError("Owned operations require an owner and invocation thread")
if document_id is not None and (not isinstance(document_id, str) or not document_id.strip()):
raise ResourceIdentityError("Malformed server document selector")
namespace = OWNED_TOOL_NAMESPACES[operation.tool]
args = _args(operation.input) if operation.tool not in {"create_document", "edit_document", "update_document", "suggest_document", "send_to_session", "create_session", "list_sessions", "search_chats", "manage_session", "manage_memory"} else {}
execution_input = operation.input
resources = []
attachments = []
doc_id = ""
doc_version = None
doc_digest = ""
collection = lambda: OwnedResource(namespace, owner, thread_id, namespace, "*")
if namespace == "documents":
action = str(args.get("action") or "list").strip().lower()
if operation.tool == "create_document" or (operation.tool == "manage_documents" and action in {"list", "search", "find", "tidy"}):
resources.append(collection())
else:
identifier = _selector(args, ("document_id", "id", "uid")) or document_id or ""
if identifier in {"active", "current"}:
if not document_id or document_id in {"active", "current", "latest"}:
raise ResourceIdentityError("Active document selector is unresolved")
identifier = document_id
if not identifier and operation.tool == "manage_documents" and action != "delete":
raise ResourceIdentityError("Document selector is required")
if not identifier or identifier == "latest":
from core.database import SessionLocal, Document
db = SessionLocal()
try:
row = db.query(Document).filter(Document.owner == owner, Document.is_active == True).order_by(Document.updated_at.desc(), Document.id).first()
identifier = row.id if row is not None else ""
finally:
db.close()
if not identifier:
raise ResourceIdentityError("Document selector is unresolved")
row = _row(namespace, identifier, owner)
resources.append(_record(namespace, owner, thread_id, row))
doc_id, doc_version = row.id, row.version_count
from src.tool_approvals import document_content_digest
doc_digest = document_content_digest(row.current_content)
if operation.tool == "manage_documents":
for key in ("id", "uid"):
args.pop(key, None)
args["document_id"] = doc_id
execution_input = json.dumps(args, sort_keys=True)
elif namespace == "threads":
if operation.tool in {"list_sessions", "search_chats", "create_session"}:
resources.append(collection())
else:
if operation.tool == "send_to_session":
identifier, _, message = operation.input.partition("\n")
identifier = identifier.strip()
else:
if operation.input.lstrip().startswith("{"):
args = _args(operation.input)
else:
lines = operation.input.strip().split("\n", 2)
args = {"action": lines[0], "session_id": lines[1] if len(lines) > 1 else ""}
if len(lines) > 2:
args["value"] = lines[2]
if args.get("action") == "list":
resources.append(collection())
identifier = _selector(args, ("session_id", "session", "id"))
if not resources:
identifier = thread_id if identifier == "current" else identifier
row = _row(namespace, identifier, owner)
resources.append(_record(namespace, owner, thread_id, row))
if operation.tool == "send_to_session":
execution_input = row.id + "\n" + message
else:
args.pop("id", None)
args.pop("session", None)
args["session_id"] = row.id
execution_input = json.dumps(args, sort_keys=True)
elif namespace == "notes":
action = str(args.get("action") or "").strip().lower().replace("-", "_")
if action in {"list", "search", "find", "add", "create", "new", "save", "remind"}:
resources.append(collection())
else:
identifier = _selector(args, ("id", "note_id", "noteId"))
from core.database import SessionLocal, Note
db = SessionLocal()
try:
q = db.query(Note).filter(Note.owner == owner)
if identifier:
rows = q.filter(Note.id.startswith(identifier, autoescape=True)).limit(2).all()
else:
title = _selector(args, ("title", "query", "text"))
rows = q.filter(Note.title == title).limit(2).all() if title else []
if len(rows) != 1:
raise ResourceIdentityError("Note selector is missing or ambiguous")
identifier = rows[0].id
finally:
db.close()
row = _row(namespace, identifier, owner)
resources.append(_record(namespace, owner, thread_id, row))
args.pop("note_id", None)
args.pop("noteId", None)
args["id"] = identifier
execution_input = json.dumps(args, sort_keys=True)
elif namespace == "attachments":
selector = args.get("path")
match = re.fullmatch(r"odysseus://attachment/([A-Za-z0-9_-]+(?:\.[A-Za-z0-9]+)?)", selector or "")
if match is None:
raise ResourceIdentityError("Malformed attachment selector")
attachment = _attachment(match[1], owner, thread_id)
resources.append(attachment.record)
attachments.append(attachment)
elif namespace == "memory":
from src.ai_interaction import _manage_memory_lines
lines = _manage_memory_lines(operation.input)
if not lines:
raise ResourceIdentityError("Memory action is unresolved")
action = lines[0].strip().lower()
if action in {"list", "search", "add"}:
resources.append(collection())
elif action in {"edit", "delete"} and len(lines) >= 2:
resource = _memory_record(lines[1].strip(), owner, thread_id, prefix=True)
resources.append(resource)
lines[1] = resource.record_id
execution_input = "\n".join(lines)
else:
raise ResourceIdentityError("Memory operation is unresolved")
elif namespace == "vault":
identifier = "*"
if operation.tool == "vault_get":
identifier = _vault_selector(owner, _selector(args, ("item_id",)))
args["item_id"] = identifier
execution_input = json.dumps(args, sort_keys=True)
resources.append(_vault_resource(owner, thread_id, identifier))
bound = BoundOwnedOperation(operation, execution_input, request_id, owner, thread_id,
tuple(resources), tuple(attachments), doc_id, doc_version, doc_digest)
bound.validate()
return bound
def admit_owned_operation(authority, operation, *, document_id=None, approved=None, exact_admission=False):
bound = (approved if approved is not None else resolve_owned_operation(operation, owner=authority.owner,
thread_id=authority.session_id, request_id=authority.request_id, document_id=document_id))
if (not isinstance(bound, BoundOwnedOperation) or bound.operation != operation
or (bound.owner, bound.thread_id) != (authority.owner, authority.session_id)
or (bound.request_id and bound.request_id != authority.request_id)):
raise ResourceIdentityError("Owned operation approval binding changed")
if not all(any(scope.permits(r) for scope in authority.owned_scopes) for r in bound.resources):
if not (approved is not None and exact_admission and not authority.inherited and not authority.owned_scopes):
raise ResourceIdentityError("Owned resource exceeds parent/request scope")
bound.validate()
return bound
_ACTIVE = ContextVar("owned_resource_operation", default=None)
def active_owned_operation():
return _ACTIVE.get()
@contextmanager
def bind_owned_operation(operation):
if operation is not None:
if not isinstance(operation, BoundOwnedOperation):
raise TypeError("Owned operation must be server-owned")
operation.validate()
token = _ACTIVE.set(operation)
try:
yield operation
finally:
_ACTIVE.reset(token)
def bound_attachment_path(owner, selector):
operation = active_owned_operation()
if operation is None:
return None
operation.validate()
for attachment in operation.attachments:
if owner == operation.owner and selector == "odysseus://attachment/" + attachment.record.record_id:
return attachment.file.path
raise ResourceIdentityError("Attachment is not declared by this operation")
+238
View File
@@ -0,0 +1,238 @@
"""Backend resolution and pinning, independent of transport and lifecycle.
Connection/configuration incarnations here are not process identities. Backend
snapshots are captured by trusted admission; discovery never supplies a grant.
"""
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
from urllib.parse import urlsplit, urlunsplit
from uuid import uuid4
import hashlib
import hmac
import secrets
import json
from src.agent_runtime.resources import ExternalResource, NativeBackendResource, ResourceIdentityError
def endpoint_identity(url):
"""Credential-free origin. Paths may themselves contain access tokens."""
if not isinstance(url, str) or any(c in url for c in ("\0", "\n", "\r")):
raise ValueError("Malformed resource endpoint")
parsed = urlsplit(url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise ValueError("Resource endpoint requires an HTTP origin")
host = parsed.hostname.lower()
if ":" in host:
host = "[" + host + "]"
port = parsed.port
if port and port != (443 if parsed.scheme == "https" else 80):
host += f":{port}"
return urlunsplit((parsed.scheme, host, "", "", ""))
_CLIENT_ENDPOINTS = {}
_CONFIG_KEY = secrets.token_bytes(32)
def configuration_incarnation(value):
"""Opaque in-process configuration identity, including secret URL changes."""
return hmac.new(_CONFIG_KEY, str(value).encode(), hashlib.sha256).hexdigest()
def _client_resource(tool, context, *, admission):
from src.tool_execution import _client_bridge, _tui_host_bridge_patch_url, _ROUTED_BRIDGE_TOOLS
bridge = _client_bridge(context)
target = _tui_host_bridge_patch_url(context) if tool == "apply_patch" else None
if target is not None:
url = target[0]
elif bridge is not None and (tool in _ROUTED_BRIDGE_TOOLS or tool == "host_shell"):
url = bridge["url"]
else:
return None
endpoint = endpoint_identity(url)
# Never cache credentials. A request cannot create a registry entry during
# dispatch; only trusted server admission may register an endpoint.
key = configuration_incarnation((url, bridge.get("token") if bridge else None))
if admission:
_CLIENT_ENDPOINTS.setdefault(key, uuid4().hex)
incarnation = _CLIENT_ENDPOINTS.get(key)
if incarnation is None:
raise ResourceIdentityError("External bridge endpoint is not sealed")
return ExternalResource("client_bridge", endpoint, "tui", tool, incarnation)
def http_bridge_resource(tool, context, *, admission=False):
config = context.get("external_execution_bridge") if isinstance(context, dict) else None
if not isinstance(config, dict) or tool not in (config.get("supported_tools") or ()):
return None
url, token = config.get("url"), config.get("token")
if not isinstance(token, str) or not token:
raise ResourceIdentityError("External HTTP bridge has no server configuration")
epoch = configuration_incarnation((url, token, tuple(sorted(config["supported_tools"]))))
if admission:
_CLIENT_ENDPOINTS.setdefault(epoch, epoch)
if epoch not in _CLIENT_ENDPOINTS:
raise ResourceIdentityError("External HTTP bridge configuration is not sealed")
return ExternalResource("execution_bridge", endpoint_identity(url), "request_local_http", tool, epoch)
def integration_resource(config):
if not isinstance(config, dict) or not config.get("enabled", True) or not isinstance(config.get("id"), str) or not config["id"]:
raise ResourceIdentityError("Integration identity is unresolved")
endpoint = endpoint_identity(config.get("base_url"))
epoch = configuration_incarnation(json.dumps(config, sort_keys=True, allow_nan=False))
return ExternalResource("integration", endpoint, config["id"], "api_call", epoch)
def api_arguments(content):
if content.lstrip().startswith("{"):
args = json.loads(content)
else:
lines = content.strip().split("\n", 2)
args = {"integration": lines[0].strip()}
if len(lines) > 1:
method, _, path = lines[1].strip().partition(" ")
args.update(method=method, path=path or "/")
if len(lines) > 2:
args["body"] = json.loads(lines[2])
selector = args.get("integration")
if not isinstance(selector, str) or not selector.strip():
raise ResourceIdentityError("Integration selector is unresolved")
return args
def resolve_backend(tool, *, context=None, admission=False, content="", owner=None):
from src.tool_execution import get_active_execution_bridge, get_mcp_manager, _MCP_TOOL_MAP
from src.tool_security import BUILTIN_EMAIL_TOOLS
bridge = get_active_execution_bridge()
if bridge is not None and tool in bridge.supported_tools:
return bridge.resource_identity(tool)
configured_bridge = http_bridge_resource(tool, context, admission=admission)
if configured_bridge is not None:
return configured_bridge
client = _client_resource(tool, context, admission=admission)
if client is not None:
return client
if tool == "api_call":
from src.integrations import load_integrations
selector = api_arguments(content)["integration"]
rows = [row for row in load_integrations() if row.get("id") == selector
or str(row.get("name", "")).casefold() == selector.casefold()]
if len(rows) != 1:
raise ResourceIdentityError("Integration alias is missing or ambiguous")
return integration_resource(rows[0])
qualified = tool
required = tool.startswith("mcp__") or tool in BUILTIN_EMAIL_TOOLS
if tool in BUILTIN_EMAIL_TOOLS:
qualified = "mcp__email__" + tool
elif tool in _MCP_TOOL_MAP and tool not in {"read_file", "write_file", "generate_image"}:
server, name = _MCP_TOOL_MAP[tool]
qualified = f"mcp__{server}__{name}"
if qualified.startswith("mcp__"):
manager = get_mcp_manager()
identity = manager.resource_identity(qualified) if manager is not None else None
if isinstance(identity, ExternalResource):
if identity.owner and owner != identity.owner:
raise ResourceIdentityError("MCP backend belongs to another owner")
return identity
if required:
raise ResourceIdentityError("MCP backend/tool identity is unresolved")
if tool == "host_shell":
raise ResourceIdentityError("Host-shell backend identity is unresolved")
return NativeBackendResource(tool)
def seal_backends(tools, *, context=None, owner=None):
result = []
for tool in tools:
try:
if tool == "api_call":
# A generic API operation grant does not select an integration.
# Trusted admission must supply its explicit backend identity,
# or a user can approve one fully sealed exact operation.
continue
result.append(resolve_backend(tool, context=context, admission=True, owner=owner))
except (ValueError, TypeError, AttributeError):
continue
return tuple(dict.fromkeys(result))
@dataclass(frozen=True)
class BoundBackendOperation:
resource: ExternalResource | NativeBackendResource
request_id: str
owner: str
session_id: str
transport_tool: str
exact_input: str
def __post_init__(self):
if not isinstance(self.resource, (ExternalResource, NativeBackendResource)):
raise ValueError("Malformed bound backend operation")
if any(not isinstance(v, str) for v in (self.request_id, self.owner, self.session_id, self.transport_tool, self.exact_input)):
raise ValueError("Malformed backend operation binding")
def to_dict(self):
# Exact arguments/selectors are already digest-bound by the approval's
# original content. Keep credentials out of the identity serializer.
return {"resource": self.resource.to_dict(), "request_id": self.request_id,
"owner": self.owner, "session_id": self.session_id, "tool": self.transport_tool,
"input_digest": configuration_incarnation(self.exact_input)}
def validate(self, context=None):
current = resolve_backend(self.transport_tool, context=context, content=self.exact_input, owner=self.owner)
if current != self.resource:
# A pinned native backend remains native when MCP availability
# changes. It cannot be upgraded to an external backend.
if isinstance(self.resource, NativeBackendResource) and isinstance(current, ExternalResource) and current.namespace == "mcp":
return
raise ResourceIdentityError("Backend resource identity changed")
def bind_backend_for_operation(authority, operation, *, context=None, approved=None, exact_admission=False):
current = resolve_backend(operation.transport_tool, context=context, content=operation.input, owner=authority.owner)
native = NativeBackendResource(operation.transport_tool)
if approved is not None:
if (not isinstance(approved, BoundBackendOperation)
or (approved.request_id and approved.request_id != authority.request_id)
or (approved.owner, approved.session_id) != (authority.owner, authority.session_id)
or (approved.transport_tool, approved.exact_input) != (operation.transport_tool, operation.input)):
raise ResourceIdentityError("Approved backend binding changed")
selected = approved.resource
elif current in authority.backend_resources:
selected = current
elif native in authority.backend_resources:
selected = native
elif isinstance(current, NativeBackendResource) and not authority.inherited:
# Legacy operation authority can only retain the fixed local backend;
# it cannot reconstruct any external backend from current availability.
selected = current
else:
raise ResourceIdentityError("External backend is outside sealed request scope")
if isinstance(selected, ExternalResource) and selected not in authority.backend_resources:
if not (exact_admission and approved is not None and not authority.inherited):
raise ResourceIdentityError("External backend exceeds parent/request scope")
bound = BoundBackendOperation(selected, authority.request_id, authority.owner, authority.session_id,
operation.transport_tool, operation.input)
bound.validate(context)
return bound
_ACTIVE = ContextVar("backend_resource_operation", default=None)
def active_backend_operation():
return _ACTIVE.get()
@contextmanager
def bind_backend_operation(operation):
if operation is not None and not isinstance(operation, BoundBackendOperation):
raise TypeError("Backend operation must be server-owned")
token = _ACTIVE.set(operation)
try:
yield operation
finally:
_ACTIVE.reset(token)
+166 -12
View File
@@ -10,6 +10,7 @@ from enum import Enum
import os
from pathlib import Path
import stat
import sys
from src.agent_runtime.path_policy import _is_sensitive_path
from src.path_confinement import canonical_root, confine
@@ -30,11 +31,65 @@ def _absolute(value):
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)}:
from src import constants
protected = {canonical_root(getattr(constants, name)) for name in (
"BG_JOBS_FILE", "CONTAINMENT_STATE_FILE", "APP_DB", "AUTH_FILE",
"SETTINGS_FILE", "SESSIONS_FILE", "USER_PREFS_FILE", "VAULT_FILE",
"SCHEDULED_EMAILS_DB", "EMAIL_CACHE_DB", "MEMORY_FILE", "INTEGRATIONS_FILE",
)}
job_dirs = {canonical_root(constants.BG_JOBS_DIR)}
# Producers may have configured paths different from the default constants.
# Inspect already-loaded server metadata without initializing a store here.
bg = sys.modules.get("src.bg_jobs")
if bg is not None:
for name, targets in (("_STORE", protected), ("_JOBS_DIR", job_dirs)):
value = getattr(bg, name, None)
if isinstance(value, (str, os.PathLike)):
targets.add(canonical_root(value))
containment = sys.modules.get("src.containment")
if containment is not None:
value = containment._store_path()
if isinstance(value, (str, os.PathLike)):
protected.add(canonical_root(value))
database = sys.modules.get("core.database")
url = getattr(getattr(database, "engine", None), "url", None)
if url is not None and url.get_backend_name() == "sqlite":
location = url.database
if isinstance(location, str) and location not in {"", ":memory:"}:
from urllib.parse import unquote
if location.startswith("file:"):
location = unquote(location[5:].split("?", 1)[0])
protected.update(canonical_root(location + suffix) for suffix in ("", "-wal", "-shm", "-journal"))
from src.tool_utils import get_upload_handler
uploader = get_upload_handler()
if uploader is not None and isinstance(getattr(uploader, "upload_dir", None), (str, os.PathLike)):
protected.add(canonical_root(Path(uploader.upload_dir) / "uploads.json"))
for directory in job_dirs:
jobs = Path(directory)
if Path(path).is_relative_to(jobs):
return True
if jobs.exists():
# Uninspectable state fails closed; hardlinks retain object identity.
protected.update(canonical_root(p) for p in jobs.iterdir())
protected.update(canonical_root(getattr(constants, name) + suffix)
for name in ("APP_DB", "SCHEDULED_EMAILS_DB", "EMAIL_CACHE_DB")
for suffix in ("-wal", "-shm", "-journal"))
protected.add(canonical_root(Path(constants.DATA_DIR) / ".app_key"))
protected.add(canonical_root(Path(constants.UPLOAD_DIR) / "uploads.json"))
if path in protected:
return True
return (Path(path).is_relative_to(canonical_root(BG_JOBS_DIR))
and path.endswith(".authority.json"))
try:
candidate = os.stat(path)
except FileNotFoundError:
return False
for control in protected:
try:
observed = os.stat(control)
except FileNotFoundError:
continue
if (candidate.st_dev, candidate.st_ino) == (observed.st_dev, observed.st_ino):
return True
return False
class FilesystemScope(str, Enum):
@@ -201,18 +256,16 @@ def intersect_roots(parent, child):
for right in child:
if (left.scope, left.owner) != (right.scope, right.owner):
continue
if left == right:
result.append(left)
continue
try:
left.validate()
right.validate()
if left == right:
result.append(left)
continue
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
@@ -279,12 +332,44 @@ class ExternalResource:
tool_id: str
incarnation: str
external: bool = True
contained: bool = False
owner: str = ""
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:
if self.external is not True or self.contained is not False:
raise ValueError("External resource cannot attest local containment")
_text(self.owner, "external owner", optional=True)
def to_dict(self):
return {"kind": "external", **asdict(self)}
@dataclass(frozen=True)
class NativeBackendResource:
tool_id: str
namespace: str = "native"
external: bool = False
contained: bool = False
def __post_init__(self):
_text(self.tool_id, "native tool")
if self.namespace != "native" or self.external is not False or self.contained is not False:
raise ValueError("Malformed native backend identity")
def to_dict(self):
return {"kind": "native", **asdict(self)}
def backend_from_dict(value):
if not isinstance(value, dict):
raise ValueError("Malformed backend snapshot")
fields = dict(value)
kind = fields.pop("kind", None)
if kind not in {"native", "external"}:
raise ValueError("Malformed backend kind")
return (NativeBackendResource if kind == "native" else ExternalResource)(**fields)
@dataclass(frozen=True)
@@ -295,8 +380,77 @@ class OwnedResource:
collection: str
record_id: str
revision: str = ""
record_thread_id: 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)
_text(self.record_thread_id, "record thread", optional=True)
def to_dict(self):
return asdict(self)
@dataclass(frozen=True)
class OwnedScope:
namespace: str
owner: str
thread_id: str
record_ids: frozenset[str] | None = None
def __post_init__(self):
for name in ("namespace", "owner", "thread_id"):
_text(getattr(self, name), name)
if self.record_ids is not None:
if not isinstance(self.record_ids, frozenset):
raise ValueError("Owned scope must be immutable")
for identifier in self.record_ids:
_text(identifier, "record identifier")
if identifier == "*":
raise ValueError("Collection authority must be explicit")
def permits(self, resource):
return (isinstance(resource, OwnedResource)
and (self.namespace, self.owner, self.thread_id) ==
(resource.namespace, resource.owner, resource.thread_id)
and resource.collection == self.namespace
and (self.record_ids is None or resource.record_id in self.record_ids))
def intersect(self, other):
if (self.namespace, self.owner, self.thread_id) != (other.namespace, other.owner, other.thread_id):
return None
ids = (other.record_ids if self.record_ids is None else self.record_ids if other.record_ids is None
else self.record_ids & other.record_ids)
return OwnedScope(self.namespace, self.owner, self.thread_id, ids)
def to_dict(self):
return {"namespace": self.namespace, "owner": self.owner, "thread_id": self.thread_id,
"record_ids": None if self.record_ids is None else sorted(self.record_ids)}
@classmethod
def from_dict(cls, value):
if not isinstance(value, dict) or set(value) != {"namespace", "owner", "thread_id", "record_ids"}:
raise ValueError("Malformed owned scope snapshot")
ids = value["record_ids"]
if ids is not None and (not isinstance(ids, list) or any(not isinstance(v, str) for v in ids)):
raise ValueError("Malformed owned record limits")
return cls(value["namespace"], value["owner"], value["thread_id"],
None if ids is None else frozenset(ids))
OWNED_TOOL_NAMESPACES = {
**{name: "documents" for name in ("create_document", "edit_document", "update_document", "suggest_document", "manage_documents")},
**{name: "threads" for name in ("create_session", "list_sessions", "manage_session", "send_to_session", "search_chats")},
**{name: "attachments" for name in ("extract_text", "inspect_media", "transcribe_media")},
"manage_notes": "notes",
"manage_memory": "memory",
**{name: "vault" for name in ("vault_get", "vault_search", "vault_unlock")},
}
def seal_owned_scopes(owner, thread_id, tools):
if not owner or not thread_id:
return ()
return tuple(OwnedScope(namespace, owner, thread_id)
for namespace in sorted({OWNED_TOOL_NAMESPACES[t] for t in tools if t in OWNED_TOOL_NAMESPACES}))