mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 15:02:20 +02:00
582 lines
32 KiB
Python
582 lines
32 KiB
Python
"""Server-owned request admission, independent of model tool availability."""
|
|
from __future__ import annotations
|
|
|
|
from contextlib import aclosing, contextmanager
|
|
from contextvars import ContextVar
|
|
from dataclasses import dataclass, replace
|
|
from functools import wraps
|
|
from inspect import signature
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
from uuid import uuid4
|
|
|
|
from src.agent_runtime.resources import (
|
|
FilesystemRoot, ExternalResource, NativeBackendResource, OwnedScope,
|
|
ProcessLaunchScope, ProcessResource, BackgroundJobResource,
|
|
BrowserSessionResource, BrowserPageResource,
|
|
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,
|
|
RequiredReadOperation, required_read_operation_for_request, selected_tools_for_request,
|
|
)
|
|
|
|
|
|
def _owner(value):
|
|
return str(value or "").strip().casefold()
|
|
|
|
|
|
def _pairs(pairs):
|
|
result = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise ValueError("Duplicate operation argument")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _invalid_constant(value):
|
|
raise ValueError("Non-finite operation argument")
|
|
|
|
|
|
def _json(value):
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExactOperation:
|
|
tool: str
|
|
input: str
|
|
action: str | None = None
|
|
transport_tool: str = ""
|
|
|
|
@classmethod
|
|
def normalize(cls, tool, content):
|
|
if not isinstance(tool, str) or not tool.strip() or not isinstance(content, str):
|
|
raise ValueError("Operation requires a tool name and string input")
|
|
transport_tool = tool.strip()
|
|
tool = canonical_tool(transport_tool)
|
|
normalized = content
|
|
payload = None
|
|
raw_input = tool in {"bash", "python"} or tool.startswith("scheduled__")
|
|
if not raw_input and content.lstrip().startswith("{"):
|
|
payload = json.loads(content, object_pairs_hook=_pairs, parse_constant=_invalid_constant)
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("Structured tool input must be an object")
|
|
normalized = _json(payload)
|
|
# Reuse the runtime's existing multiplexed-action normalization; this
|
|
# classifies input and never grants permission or changes the input.
|
|
from src.tool_capabilities import _action_from_content
|
|
action = _action_from_content(tool, content)
|
|
if tool == "private_browser" and isinstance(payload, dict):
|
|
action = payload.get("action")
|
|
if action is not None and not isinstance(action, str):
|
|
raise ValueError("Browser action must be a string")
|
|
action = action.strip().casefold() if action else None
|
|
return cls(tool, normalized, action, transport_tool)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OperationGrant:
|
|
tool: str
|
|
actions: frozenset[str] | None = None
|
|
inputs: frozenset[str] | None = None
|
|
|
|
def __post_init__(self):
|
|
if not isinstance(self.tool, str) or not self.tool or canonical_tool(self.tool) != self.tool:
|
|
raise ValueError("Grant requires a canonical tool identity")
|
|
for values in (self.actions, self.inputs):
|
|
if values is not None and (not isinstance(values, frozenset)
|
|
or any(not isinstance(v, str) for v in values)):
|
|
raise TypeError("Grant limits must be immutable string sets")
|
|
|
|
def permits(self, operation):
|
|
return (self.tool == operation.tool
|
|
and (self.actions is None or operation.action in self.actions)
|
|
and (self.inputs is None or operation.input in self.inputs))
|
|
|
|
def intersect(self, other):
|
|
if self.tool != other.tool:
|
|
raise ValueError("Cannot intersect different operation classes")
|
|
def limits(left, right):
|
|
return right if left is None else left if right is None else left & right
|
|
return OperationGrant(self.tool, limits(self.actions, other.actions),
|
|
limits(self.inputs, other.inputs))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RequestAuthority:
|
|
request_id: str
|
|
owner: str
|
|
session_id: str
|
|
workspace: str
|
|
grants: tuple[OperationGrant, ...] = ()
|
|
denied: frozenset[str] = frozenset()
|
|
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
|
|
backend_resources: tuple[ExternalResource | NativeBackendResource, ...] | None = None
|
|
owned_scopes: tuple[OwnedScope, ...] | None = None
|
|
launch_scopes: tuple[ProcessLaunchScope, ...] | None = None
|
|
process_resources: tuple[ProcessResource, ...] = ()
|
|
job_resources: tuple[BackgroundJobResource, ...] | None = None
|
|
browser_sessions: tuple[BrowserSessionResource, ...] | None = None
|
|
browser_pages: tuple[BrowserPageResource, ...] | None = None
|
|
|
|
def __post_init__(self):
|
|
if (not isinstance(self.request_id, str) or not self.request_id
|
|
or any(not isinstance(v, str) for v in (self.owner, self.session_id, self.workspace))
|
|
or not isinstance(self.grants, tuple)
|
|
or any(not isinstance(g, OperationGrant) for g in self.grants)
|
|
or len({g.tool for g in self.grants}) != len(self.grants)
|
|
or not isinstance(self.denied, frozenset)
|
|
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")
|
|
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")
|
|
from src.agent_runtime.process_resources import seal_launch_scopes, seal_jobs
|
|
if self.launch_scopes is None:
|
|
object.__setattr__(self, "launch_scopes", seal_launch_scopes(self))
|
|
if self.job_resources is None:
|
|
object.__setattr__(self, "job_resources", seal_jobs(self))
|
|
for field, kind in (("launch_scopes", ProcessLaunchScope), ("process_resources", ProcessResource),
|
|
("job_resources", BackgroundJobResource)):
|
|
values = getattr(self, field)
|
|
if not isinstance(values, tuple) or any(not isinstance(r, kind) for r in values):
|
|
raise ValueError("Malformed process resource scope")
|
|
if any(r.owner != self.owner for r in (*self.process_resources, *self.job_resources)):
|
|
raise ValueError("Process resource owner changed")
|
|
if any(s.root.owner and s.root.owner != self.owner for s in self.launch_scopes):
|
|
raise ValueError("Launch resource owner changed")
|
|
if any(r.thread_id != self.session_id for r in self.job_resources):
|
|
raise ValueError("Job resource thread changed")
|
|
if any(r.thread_id != (self.session_id or "request:" + self.request_id) for r in self.process_resources):
|
|
raise ValueError("Process resource thread changed")
|
|
from src.browser_identity import seal_browser_resources
|
|
sessions, pages = seal_browser_resources(self) if self.browser_sessions is None or self.browser_pages is None else ((), ())
|
|
if self.browser_sessions is None:
|
|
object.__setattr__(self, "browser_sessions", sessions)
|
|
if self.browser_pages is None:
|
|
object.__setattr__(self, "browser_pages", pages)
|
|
for values, kind in ((self.browser_sessions, BrowserSessionResource), (self.browser_pages, BrowserPageResource)):
|
|
if not isinstance(values, tuple) or any(not isinstance(r, kind) for r in values):
|
|
raise ValueError("Malformed browser resource scope")
|
|
for r in values:
|
|
session = r.session if isinstance(r, BrowserPageResource) else r
|
|
if (session.owner, session.thread_id) != (self.owner, self.session_id):
|
|
raise ValueError("Browser owner/thread binding changed")
|
|
|
|
@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=(), backend_resources=(), owned_scopes=(), launch_scopes=(), job_resources=(), browser_sessions=(), browser_pages=())
|
|
|
|
def bound_to(self, *, owner=None, session_id=None, workspace=None):
|
|
return (self.owner == _owner(owner) and self.session_id == str(session_id or "")
|
|
and self.workspace == str(workspace or ""))
|
|
|
|
def restricted(self, operation):
|
|
return (self.block_all or operation.tool in self.denied
|
|
or (self.disable_mcp and (operation.tool.startswith("mcp__")
|
|
or operation.transport_tool.startswith("mcp__"))))
|
|
|
|
def permits(self, operation):
|
|
return not self.restricted(operation) and any(g.permits(operation) for g in self.grants)
|
|
|
|
def restrict(self, policy=None, disabled_tools=()):
|
|
policy = policy or ToolPolicy()
|
|
return replace(self, denied=self.denied | frozenset(
|
|
canonical_tool(n) for n in set(disabled_tools or ()) | policy.all_disabled_names()),
|
|
block_all=self.block_all or policy.block_all_tool_calls,
|
|
disable_mcp=self.disable_mcp or policy.disable_mcp)
|
|
|
|
def intersect(self, child):
|
|
if not isinstance(child, RequestAuthority):
|
|
raise TypeError("Child authority must be server-owned RequestAuthority")
|
|
grants = []
|
|
roots = ()
|
|
backends = ()
|
|
owned = ()
|
|
launches = processes = jobs = ()
|
|
browser_sessions = browser_pages = ()
|
|
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)
|
|
from src.agent_runtime.process_resources import intersect_observed, intersect_launch_scopes, validate_job
|
|
launches = intersect_launch_scopes(self.launch_scopes, child.launch_scopes)
|
|
processes = intersect_observed(self.process_resources, child.process_resources, lambda r: r.validate())
|
|
jobs = intersect_observed(self.job_resources, child.job_resources, validate_job)
|
|
from src.browser_identity import intersect_browser
|
|
browser_sessions, browser_pages = intersect_browser(self.browser_sessions, self.browser_pages,
|
|
child.browser_sessions, child.browser_pages)
|
|
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, backend_resources=backends, owned_scopes=owned,
|
|
launch_scopes=launches, process_resources=processes, job_resources=jobs,
|
|
browser_sessions=browser_sessions, browser_pages=browser_pages)
|
|
|
|
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)
|
|
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 (),
|
|
process_resources=tuple(r for r in self.process_resources if r.thread_id == rebound),
|
|
job_resources=tuple(r for r in self.job_resources if r.thread_id == rebound),
|
|
browser_sessions=tuple(r for r in self.browser_sessions if r.thread_id == rebound),
|
|
browser_pages=tuple(r for r in self.browser_pages if r.session.thread_id == rebound))
|
|
|
|
def to_dict(self):
|
|
return {"version": 5, "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],
|
|
"backend_resources": [r.to_dict() for r in self.backend_resources],
|
|
"owned_scopes": [s.to_dict() for s in self.owned_scopes],
|
|
"launch_scopes": [s.to_dict() for s in self.launch_scopes],
|
|
"process_resources": [r.to_dict() for r in self.process_resources],
|
|
"job_resources": [r.to_dict() for r in self.job_resources],
|
|
"browser_sessions": [r.to_dict() for r in self.browser_sessions],
|
|
"browser_pages": [r.to_dict() for r in self.browser_pages]}
|
|
|
|
@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, 3, 4, 5}):
|
|
raise ValueError("Unsupported authority snapshot")
|
|
def limits(value):
|
|
if value is None:
|
|
return None
|
|
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")
|
|
backends = value["backend_resources"] if value["version"] >= 3 else []
|
|
owned = value["owned_scopes"] if value["version"] >= 3 else []
|
|
process_fields = {name: value[name] if value["version"] >= 4 else []
|
|
for name in ("launch_scopes", "process_resources", "job_resources")}
|
|
if any(not isinstance(v, list) for v in process_fields.values()):
|
|
raise ValueError("Malformed process resource snapshot")
|
|
if not isinstance(backends, list) or not isinstance(owned, list):
|
|
raise ValueError("Malformed request resource scope snapshot")
|
|
if value["version"] >= 5 and any(not isinstance(value.get(name), list) for name in ("browser_sessions", "browser_pages")):
|
|
raise ValueError("Malformed browser 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(backend_from_dict(r) for r in backends), tuple(OwnedScope.from_dict(s) for s in owned),
|
|
tuple(ProcessLaunchScope.from_dict(s) for s in process_fields["launch_scopes"]),
|
|
tuple(ProcessResource.from_dict(r) for r in process_fields["process_resources"]),
|
|
tuple(BackgroundJobResource.from_dict(r) for r in process_fields["job_resources"]),
|
|
tuple(BrowserSessionResource.from_dict(r) for r in value["browser_sessions"]) if value["version"] >= 5 else (),
|
|
tuple(BrowserPageResource.from_dict(r) for r in value["browser_pages"]) if value["version"] >= 5 else ())
|
|
|
|
|
|
_BROWSER_READ_ACTIONS = frozenset({"open", "navigate", "snapshot", "text", "read", "find",
|
|
"screenshot", "scroll", "back", "forward", "wait", "status", "close", "tabs", "session_info"})
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SemanticIntent:
|
|
"""Routing facts, with no execution permission or provider inventory."""
|
|
capabilities: frozenset[str]
|
|
selected_tools: frozenset[str] | None
|
|
required_read: RequiredReadOperation | None
|
|
|
|
|
|
def interpret_request(request_text, *, history=(), workspace=None, active_document=False,
|
|
image_attachment=False):
|
|
if not isinstance(request_text, str):
|
|
raise TypeError("Intent requires request text")
|
|
# Routing may use model/tool history. Admission may only inherit intent
|
|
# from trusted user requests; a model's proposal or attempted tool call
|
|
# cannot establish a new authorized operation class.
|
|
history = tuple(history or ())
|
|
trusted_history = []
|
|
for row in history:
|
|
get = row.get if isinstance(row, dict) else lambda key, default=None: getattr(row, key, default)
|
|
metadata = get("metadata") or {}
|
|
if isinstance(metadata, str):
|
|
try:
|
|
metadata = json.loads(metadata)
|
|
except ValueError:
|
|
metadata = {}
|
|
if (get("role") == "user" and isinstance(metadata, dict)
|
|
and metadata.get("trusted") is not False and not metadata.get("tool_gate_untrusted")):
|
|
trusted_history.append({"role": "user", "content": get("content", "")})
|
|
families = requested_capabilities(request_text, trusted_history,
|
|
active_document=active_document, workspace=bool(workspace), image_attachment=image_attachment)
|
|
selected = selected_tools_for_request(request_text)
|
|
if (families <= {"unknown"} and selected is None
|
|
and re.search(r"\b(?:lan|local\s+(?:network|ip)|tailscale|arp|ip\s+route|default\s+route|subnet|network\s+interface|neighbor\s+table|wifi|ethernet)\b", request_text, re.I)
|
|
and re.search(r"\b(?:find|check|inspect|show|list|lookup|locate)\b", request_text, re.I)
|
|
and not re.search(r"\b(?:web|internet|online)\b", request_text, re.I)):
|
|
# An explicit local-network lookup is a host operation. The existing
|
|
# router already chooses host_shell; neither its schema nor bridge
|
|
# availability grants Bash/Python alongside this request.
|
|
return SemanticIntent(frozenset({"shell_files"}), frozenset({"host_shell"}), None)
|
|
return SemanticIntent(families, selected, required_read_operation_for_request(request_text, history))
|
|
|
|
|
|
def create_request_authority(request_text, *, owner=None, session_id=None, workspace=None,
|
|
history=(), policy=None, active_document=False,
|
|
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")
|
|
intent = interpret_request(request_text, history=history, active_document=active_document,
|
|
workspace=workspace, image_attachment=image_attachment)
|
|
families = intent.capabilities
|
|
if capabilities is not None:
|
|
families |= frozenset(capabilities)
|
|
tools = set().union(*(FAMILY_TOOLS.get(f, ()) for f in families))
|
|
selected = intent.selected_tools
|
|
if selected is not None:
|
|
tools = tools & set(selected) if families else set(selected)
|
|
if tools & {"web_search", "web_fetch"}:
|
|
tools.add("private_browser")
|
|
operation = intent.required_read
|
|
if operation is not None and canonical_tool(operation.tool) in {canonical_tool(n) for n in tools}:
|
|
tools = {canonical_tool(operation.tool)}
|
|
else:
|
|
operation = None
|
|
grants = []
|
|
for name in sorted(tools | {"ask_user", "update_plan"}):
|
|
name = canonical_tool(name)
|
|
actions = inputs = None
|
|
if name == "private_browser":
|
|
actions = _BROWSER_READ_ACTIONS
|
|
# Explicit interaction intent admits its operation class. A
|
|
# browser offered only as static-Web fallback gets no such grant.
|
|
if re.search(r"\b(?:browser|browse|private_browser)\b", request_text, re.I):
|
|
actions |= frozenset(action for action in ("click", "fill", "type", "press", "evaluate", "select")
|
|
if re.search(r"\b" + action + r"\b", request_text, re.I))
|
|
if operation is not None and name == canonical_tool(operation.tool):
|
|
inputs = frozenset({ExactOperation.normalize(name, _json(dict(operation.args))).input})
|
|
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))
|
|
|
|
|
|
_ACTIVE: ContextVar[RequestAuthority | None] = ContextVar("request_authority", default=None)
|
|
MISSING_AUTHORITY = object()
|
|
|
|
|
|
def active_request_authority():
|
|
return _ACTIVE.get()
|
|
|
|
|
|
def is_internal_tool_request(request):
|
|
"""HTTP authentication/owner attribution does not make a tool payload user intent."""
|
|
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN
|
|
return (request.headers.get(INTERNAL_TOOL_HEADER) == INTERNAL_TOOL_TOKEN
|
|
or getattr(request.state, "current_user", None) == "internal-tool")
|
|
|
|
|
|
def require_user_approval_request(request):
|
|
if is_internal_tool_request(request):
|
|
from fastapi import HTTPException
|
|
raise HTTPException(403, "Tool requests cannot submit user approval decisions.")
|
|
|
|
|
|
def request_authority_for_http(request, request_text, **context):
|
|
"""Known tool loopback is a continuation, never a fresh user grant source."""
|
|
if is_internal_tool_request(request):
|
|
return RequestAuthority.empty(owner=context.get("owner"),
|
|
session_id=context.get("session_id"), workspace=context.get("workspace")).restrict(context.get("policy"))
|
|
return create_request_authority(request_text, **context)
|
|
|
|
|
|
@contextmanager
|
|
def bind_request_authority(authority):
|
|
if not isinstance(authority, RequestAuthority):
|
|
raise TypeError("Authority must be server-owned RequestAuthority")
|
|
parent = _ACTIVE.get()
|
|
authority = parent.intersect(authority) if parent is not None else authority
|
|
token = _ACTIVE.set(authority)
|
|
try:
|
|
yield authority
|
|
finally:
|
|
_ACTIVE.reset(token)
|
|
|
|
|
|
def _request_text(messages):
|
|
for message in reversed(messages or ()):
|
|
metadata = message.get("metadata") or {}
|
|
if (message.get("role") != "user" or metadata.get("trusted") is False
|
|
or metadata.get("tool_gate_untrusted")):
|
|
continue
|
|
content = message.get("content", "")
|
|
if isinstance(content, str):
|
|
return content
|
|
if isinstance(content, list):
|
|
return "\n".join(p.get("text", "") for p in content
|
|
if isinstance(p, dict) and p.get("type") == "text")
|
|
return ""
|
|
|
|
|
|
def with_request_authority(func):
|
|
"""Bind once per invocation; model rounds/fallbacks never recreate grants."""
|
|
call_signature = signature(func)
|
|
@wraps(func)
|
|
async def wrapped(*args, **kwargs):
|
|
bound = call_signature.bind(*args, **kwargs)
|
|
bound.apply_defaults()
|
|
parameters = bound.arguments
|
|
parent = active_request_authority()
|
|
authority = parameters.get("request_authority", MISSING_AUTHORITY)
|
|
if authority is MISSING_AUTHORITY:
|
|
approval = parameters.get("exact_approval")
|
|
if parent is not None:
|
|
authority = parent
|
|
elif approval is not None:
|
|
authority = approval.pending.request_authority or RequestAuthority.empty(
|
|
owner=parameters.get("owner"), session_id=parameters.get("session_id"),
|
|
workspace=parameters.get("workspace"))
|
|
elif (parameters.get("_parent_run_id") or parameters.get("_is_teacher_run")
|
|
or parameters.get("workload") == "background"):
|
|
authority = RequestAuthority.empty(owner=parameters.get("owner"),
|
|
session_id=parameters.get("session_id"), workspace=parameters.get("workspace"))
|
|
else:
|
|
authority = create_request_authority(_request_text(parameters.get("messages")),
|
|
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")),
|
|
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:
|
|
authority = replace(authority, inherited=False)
|
|
authority = authority.restrict(parameters.get("tool_policy"), parameters.get("disabled_tools"))
|
|
with bind_request_authority(authority) as effective:
|
|
if "request_authority" in parameters:
|
|
parameters["request_authority"] = effective
|
|
async with aclosing(func(*bound.args, **bound.kwargs)) as stream:
|
|
async for chunk in stream:
|
|
yield chunk
|
|
return wrapped
|
|
|
|
|
|
def task_operation(task_type, action, prompt):
|
|
if task_type == "action":
|
|
tool = ("bash" if action in {"run_local", "run_script", "ssh_command"}
|
|
else "serve_model" if action == "cookbook_serve" else "scheduled__" + str(action))
|
|
return ExactOperation.normalize(tool, str(prompt or ""))
|
|
if task_type == "research":
|
|
return ExactOperation.normalize("trigger_research", str(prompt or ""))
|
|
return None
|
|
|
|
|
|
def seal_task_authority(prompt, task_type, action, *, owner=None, parent_authority=MISSING_AUTHORITY):
|
|
"""Only direct ingress grants; a model-created task is capped by its parent."""
|
|
operation = task_operation(task_type, action, prompt)
|
|
authority = create_request_authority(str(prompt or ""), owner=owner)
|
|
if operation is not None:
|
|
authority = replace(authority, grants=(OperationGrant(operation.tool,
|
|
inputs=frozenset({operation.input})),))
|
|
parent = active_request_authority() if parent_authority is MISSING_AUTHORITY else parent_authority
|
|
if parent_authority is None:
|
|
parent = RequestAuthority.empty(owner=owner)
|
|
if parent is not None:
|
|
authority = parent.intersect(replace(authority, session_id=parent.session_id,
|
|
workspace=parent.workspace,
|
|
resource_roots=parent.resource_roots,
|
|
backend_resources=parent.backend_resources,
|
|
owned_scopes=parent.owned_scopes,
|
|
launch_scopes=parent.launch_scopes,
|
|
process_resources=parent.process_resources,
|
|
job_resources=parent.job_resources,
|
|
browser_sessions=parent.browser_sessions,
|
|
browser_pages=parent.browser_pages))
|
|
return _json({"task_input": [prompt, task_type, action], "authority": authority.to_dict()})
|
|
|
|
|
|
def restore_task_authority(snapshot, prompt, task_type, action, *, owner=None, session_id=None):
|
|
try:
|
|
value = json.loads(snapshot)
|
|
if value["task_input"] != [prompt, task_type, action]:
|
|
raise ValueError("Scheduled request changed")
|
|
return RequestAuthority.from_dict(value["authority"]).continuation(owner=owner, session_id=session_id)
|
|
except (ValueError, TypeError, KeyError, AttributeError):
|
|
return RequestAuthority.empty(owner=owner, session_id=session_id)
|
|
|
|
|
|
def _background_path(job_id):
|
|
if not isinstance(job_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", job_id):
|
|
raise ValueError("Invalid background authority identity")
|
|
from src.bg_jobs import _JOBS_DIR
|
|
return Path(_JOBS_DIR) / (job_id + ".authority.json")
|
|
|
|
|
|
def save_background_authority(job_id, authority, *, resource=None):
|
|
from core.atomic_io import atomic_write_json
|
|
if resource is None or resource.job_id != job_id:
|
|
raise ValueError("Background authority requires exact job linkage")
|
|
atomic_write_json(_background_path(job_id), {"authority": authority.to_dict(), "job": resource.to_dict()})
|
|
|
|
|
|
def restore_background_authority(job_id, *, owner=None, session_id=None):
|
|
try:
|
|
value = json.loads(_background_path(job_id).read_text())
|
|
resource = BackgroundJobResource.from_dict(value["job"])
|
|
from src.agent_runtime.process_resources import validate_job
|
|
validate_job(resource)
|
|
authority = RequestAuthority.from_dict(value["authority"])
|
|
if (resource.job_id, resource.owner, resource.thread_id, resource.request_id) != (
|
|
job_id, authority.owner, authority.session_id, authority.request_id):
|
|
raise ValueError("Background authority linkage changed")
|
|
if authority.session_id != str(session_id or ""):
|
|
raise ValueError("Background session changed")
|
|
return authority.continuation(owner=owner, session_id=session_id)
|
|
except (OSError, ValueError, TypeError, KeyError, AttributeError):
|
|
return RequestAuthority.empty(owner=owner, session_id=session_id)
|