Merge commit 'refs/phase3/pre-ajax/publication-tip' into integration/pre-ajax-release

# Conflicts:
#	routes/chat_routes.py
#	routes/session_routes.py
#	src/agent_loop.py
#	src/agent_tools/filesystem_tools.py
#	src/teacher_escalation.py
#	src/tool_capabilities.py
#	src/tool_execution.py
#	tests/test_mcp_add_server_args_validation.py
#	tests/test_token_cache_atomic_swap.py
This commit is contained in:
Alexandre Teixeira
2026-10-05 15:59:59 +01:00
1395 changed files with 360455 additions and 105938 deletions
+1
View File
@@ -0,0 +1 @@
"""Run-scoped contracts behind the public agent-loop compatibility facade."""
+588
View File
@@ -0,0 +1,588 @@
"""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})),))
if task_type == "action" and action == "cookbook_serve":
# The direct admin scheduling ingress selects the native Cookbook
# producer. Restore never infers this from task names/availability.
# Any model-created task still intersects with its parent's ceiling.
backend = NativeBackendResource("serve_model")
authority = replace(authority, backend_resources=tuple(dict.fromkeys(
(*authority.backend_resources, backend))))
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)
+421
View File
@@ -0,0 +1,421 @@
"""One presentation gate between agent execution and externally visible prose.
Tool, progress and interaction events stay live. Answer deltas are held until
the generator unwinds so a later replacement cannot conceal an earlier false
claim. This consumes no provider calls. Cancellation closes the inner generator
under the same journal/turn authority; it never emits a successful terminal event.
"""
from __future__ import annotations
from contextlib import aclosing
from dataclasses import replace
from functools import wraps
from inspect import signature
import json
import re
from time import perf_counter
from src.agent_evidence import (
CompletionDecision, CompletionStatus, EvidenceKind, EvidenceLedger,
requirements_from_runtime_context, _execution_obligation, _unquoted_statements,
_ARTIFACT_PATH,
)
from .effect_log import EffectLog
from .journal import ActionJournal, bind_journal, current_journal
def _ledger(journal: ActionJournal, requirements) -> EvidenceLedger:
"""The single evidence view used for the decision and the prose filter."""
ledger = EvidenceLedger.from_tool_events(journal.evidence_events(), requirements)
ledger.record_effects(journal.effect_entries(),
{action.action_id: index for index, action in enumerate(journal.actions, 1)},
journal.partial_reads())
return ledger
_TEST_CLAIM = re.compile(
r'\b(?:(?:all\s+)?(?:tests?|checks?|verification|suite)\s+(?:have\s+|has\s+|now\s+|are\s+|is\s+)*(?:passed|passing|successful|green)|'
r'(?:passed|passing)\s+(?:all\s+)?(?:the\s+)?tests?|\d+\s+passed)\b', re.I)
_TEST_STATUS_CLAIM = re.compile(
r'\b(?:tests?|pytest|unittest|test suite|checks?|verification)\s*[:—-]?\s*'
r'(?:all\s+|have\s+|has\s+|now\s+|are\s+|is\s+|ran\s+)*'
r'(?:pass(?:ed|ing)?|succeeded|successful(?:ly)?|green)\b|'
r'\b(?:zero|no|0)\s+(?:test\s+)?failures\b', re.I)
_EXECUTION_CLAIM = re.compile(
r'\b(?:(?:I|we|I\'ve|we\'ve|and)\s+(?:have\s+)?(?:successfully\s+)?(?:ran|executed|tested|verified|created|updated|modified|wrote|saved|fixed|completed|sent|deleted|submitted|published|deployed|configured|uploaded)|'
rf'(?:file|artifact|command|script|service|server|email|message|record|resource|{_ARTIFACT_PATH})\s+(?:was\s+|has\s+been\s+|is\s+)?(?:successfully\s+)?(?:created|updated|written|saved|executed|started|sent|deleted|submitted|published|deployed|configured)|'
r'(?:successfully\s+)(?:ran|executed|created|updated|saved|completed|sent|deleted|submitted|published|deployed)|'
r'(?:the\s+)?(?:remote\s+)?(?:operation|request|call|mutation|action)\s+(?:was\s+|has\s+)?(?:successfully\s+)?(?:completed|succeeded|finished))\b', re.I)
_UNATTESTED_TEST_METRIC = re.compile(
r'\b\d+\s+(?:(?:unit|integration)\s+)?tests?\s+pass(?:ed|ing)?\b|'
r'\b\d+\s+passed\b|\b\d+(?:\.\d+)?%\s+(?:test\s+)?coverage\b', re.I)
_UNBOUNDED_SUCCESS = re.compile(
r'\b(?:everything|all\s+(?:bugs|issues))\s+(?:is\s+|are\s+|has\s+been\s+)?'
r'(?:fixed|resolved|working)\b', re.I)
_MUTATION_CLAIM = re.compile(
r'\b(?:created|updated|modified|wrote|written|saved|fixed|sent|deleted|submitted|published|deployed|configured|uploaded)\b', re.I)
_TEST_IDENTITY = re.compile(r'\b(?:pytest|unittest)\b', re.I)
_TEST_SUBJECT = re.compile(r'\b(?:tests?|test suite|pytest|unittest|checks?|verification)\b', re.I)
_CLAIM_PATH = re.compile(_ARTIFACT_PATH)
_BARE_SUCCESS = re.compile(r'^\s*(?:done|completed|success|all done|all set|fixed)[.!]?\s*$', re.I)
_NON_REPORT_SCOPE = re.compile(
r'^\s*(?:if|unless|suppose|imagine|hypothetically|for\s+(?:example|instance))\b|'
r'\b(?:if|when|whenever|unless|until)\b|'
r'\b(?:can|could|may|might|should|would|will|must)\b|'
r'\b(?:says?|said|states?|stated|example)\b', re.I)
def _current_run_claims(statement: str, *, execution_required: bool) -> list[tuple[str, str]]:
"""Classify asserted execution, separately from the turn's obligation.
Past actions and current result/status predicates are reports. Conditional,
modal, attributed and example clauses are scoped prose. Bare terminal
success only carries execution meaning under an execution contract.
"""
if _BARE_SUCCESS.fullmatch(statement):
return [('terminal', statement)] if execution_required else []
actions = list(_EXECUTION_CLAIM.finditer(statement))
leading = re.match(r'^\s*(?:successfully\s+)?(?:created|updated|modified|wrote|saved)\b', statement, re.I)
if leading:
actions.insert(0, leading)
candidates = [('action', match) for match in actions]
for kind, pattern in [('metric', _UNATTESTED_TEST_METRIC), ('metric', _UNBOUNDED_SUCCESS),
('test', _TEST_CLAIM), ('test', _TEST_STATUS_CLAIM)]:
candidates.extend((kind, match) for match in pattern.finditer(statement))
claims = []
for kind, match in candidates:
# Scope markers after an asserted action do not make that action
# hypothetical ("I ran pytest to see if ..."). An immediate conditional
# continuation does qualify a result ("Tests passed if ...").
if _NON_REPORT_SCOPE.search(statement[:match.start()]) or re.match(
r'\s+(?:if|when|whenever|unless|until)\b', statement[match.end():], re.I):
continue
end = next((action.start() for action in actions if action.start() > match.start()), len(statement))
scope = statement[match.start():end]
if kind == 'action':
if _MUTATION_CLAIM.search(match.group()):
kind = 'mutation'
elif _TEST_SUBJECT.search(scope):
kind = 'test'
else:
kind = 'execution'
claims.append((kind, scope))
return claims
def _supported_prose(text: str, ledger: EvidenceLedger, decision: CompletionDecision) -> tuple[str, str]:
"""Remove unsupported assertions at statement boundaries; add no notice."""
incomplete = decision.reason if not decision.can_complete and decision.status != CompletionStatus.AWAITING_USER else ''
execution_required = _execution_obligation(ledger.requirements)
# Bare "Done." cannot stand for an external effect nobody verified.
terminal_claims = execution_required or bool(ledger.unverified_external_effects())
kept = []
removed = ''
for statement, scoped in _unquoted_statements(text):
why = ''
for claim, scope in _current_run_claims(scoped, execution_required=terminal_claims):
paths = tuple(match.group().rstrip('.') for match in _CLAIM_PATH.finditer(scope))
if claim == 'metric':
why = 'test counts, coverage or exhaustive correctness were not established by execution evidence'
elif claim == 'test':
identities = tuple(match.group().lower() for match in _TEST_IDENTITY.finditer(scope))
if (decision.status not in {CompletionStatus.VERIFIED, CompletionStatus.UNVERIFIED}
or not ledger._supports_verifier_claim(identities, paths)):
why = 'no current passing executable verification supports the claim'
elif claim == 'mutation':
if not ledger._supports_artifact_claim(EvidenceKind.ARTIFACT_MUTATION, paths):
why = 'no matching artifact mutation supports the execution claim'
elif claim == 'execution':
# A generic assertion cannot be tied confidently to a receipt.
why = 'no matching operation supports the execution claim'
elif claim == 'terminal' and decision.status not in {CompletionStatus.SATISFIED, CompletionStatus.VERIFIED}:
why = incomplete or 'no successful execution supports completion'
if why:
break
if why:
removed = removed or why
else:
kept.append(statement)
prose = ''.join(kept).strip() if removed else text
return prose, removed
def completion_answer(text: str, ledger: EvidenceLedger, decision: CompletionDecision) -> tuple[str, str]:
"""Keep explanatory prose; remove unsupported assertions and attach facts.
Exit status proves neither test counts nor coverage. A bad assertion is
removed at statement boundaries instead of erasing an entire explanation.
The execution outcome remains separate from a discarded model assertion.
Unverified external effects are always stated by the server, so no
surviving prose can present a reported remote success as a verified one.
"""
answer, reason = _completion_answer(text, ledger, decision)
return _disclose(answer, ledger), reason
def _disclose(answer: str, ledger: EvidenceLedger) -> str:
"""Append the server's facts for unverified external effects."""
disclosure = _disclosure(answer, ledger)
return answer.rstrip() + disclosure if disclosure else answer
def _disclosure(answer: str, ledger: EvidenceLedger) -> str:
"""Build the complete server-owned disclosure independently of prose length."""
summary = ' '.join(ledger.effect_disclosures())
if not summary:
return ''
return ('\n\n' + summary) if answer.strip() else summary
def _completion_answer(text: str, ledger: EvidenceLedger, decision: CompletionDecision) -> tuple[str, str]:
incomplete = decision.reason if not decision.can_complete and decision.status != CompletionStatus.AWAITING_USER else ''
execution_required = _execution_obligation(ledger.requirements)
prose, removed = _supported_prose(text, ledger, decision)
if incomplete or (removed and execution_required and decision.status in {CompletionStatus.UNVERIFIED, CompletionStatus.AWAITING_USER}):
reason = incomplete or removed
missing = (' Missing artifacts: ' + ', '.join(decision.missing_artifacts) + '.'
if decision.missing_artifacts else '')
notice = 'The task is incomplete: ' + reason.rstrip('.') + '.' + missing
recorded = [path for path in ledger.requirements.required_artifacts
if ledger._supports_artifact_claim(EvidenceKind.ARTIFACT_MUTATION, (path,))]
if removed and recorded:
notice += ' Recorded artifact mutation: ' + ', '.join(recorded) + '.'
return notice + ('\n\n' + prose if prose.strip() else ''), reason
if removed and not execution_required and decision.status != CompletionStatus.VERIFIED:
notice = 'Unsupported execution claims were omitted: ' + removed.rstrip('.') + '.'
return (prose.rstrip() + '\n\n' + notice) if prose.strip() else notice, removed
if decision.can_complete and (ledger.requirements.required_artifacts or ledger.requirements.verifier_required or removed):
facts = []
if ledger.requirements.required_artifacts:
facts.append('Output available: ' + ', '.join(ledger.requirements.required_artifacts) + '.')
if decision.status == CompletionStatus.VERIFIED or ledger._supports_verifier_claim():
facts.append('The latest executable verification passed.')
elif any(e.kind == EvidenceKind.ARTIFACT_VALIDATION and e.authoritative and e.success for e in ledger.events):
facts.append('Artifact readback verified. No passing executable test result was recorded.')
else:
facts.append('No passing executable test result was recorded.')
summary = ' '.join(facts)
return (prose.rstrip() + '\n\n' + summary) if prose.strip() else summary, removed
return prose, removed
def _event(data: dict) -> str:
return 'data: ' + json.dumps(data) + '\n\n'
def with_completion_gate(func):
call_signature = signature(func)
@wraps(func)
async def wrapped(*args, **kwargs):
started = perf_counter()
first_answer_at = None
arguments = call_signature.bind(*args, **kwargs)
arguments.apply_defaults()
bound = arguments.arguments
messages = bound.get('messages') or []
instruction = next((m.get('content', '') for m in reversed(messages)
if m.get('role') == 'user' and isinstance(m.get('content'), str)), '')
context = bound.get('client_runtime_context') or {}
requirements = requirements_from_runtime_context(context, instruction=instruction)
from src.tool_execution import vet_workspace
# A completion declaration is not a filesystem permission. Only the
# explicit, vetted runtime workspace may be read for artifact versions.
trusted_workspace = vet_workspace(bound.get('workspace')) if bound.get('workspace') else ''
requirements = replace(requirements, workspace_root=trusted_workspace or '')
parent = current_journal()
journal = ActionJournal(
workspace=requirements.workspace_root, observed_artifacts=requirements.required_artifacts,
parent_run_id=bound.get('_parent_run_id') or (parent.run_id if parent is not None else None))
# One durable effect log per run lineage gives child effects and parent
# observations a single total order for invalidation.
journal.effects = (parent.effects if parent is not None and parent.effects is not None
else EffectLog(journal.run_id))
answer_events: list[dict] = []
metrics_events: list[dict] = []
answer = ''
has_final = False
done = False
awaiting = False
exhausted = False
provider_error: str | None = None
with bind_journal(journal):
async with aclosing(func(*args, **kwargs)) as stream:
async for chunk in stream:
if chunk.strip() == 'data: [DONE]':
done = True
continue
try:
data = json.loads(chunk[6:]) if chunk.startswith('data: ') else None
except (ValueError, TypeError):
data = None
if not isinstance(data, dict):
if chunk.startswith('event: error'):
# The inner stream may still emit failed-terminal
# diagnostics. Hold the original error until those
# and the buffered answer have been released.
provider_error = provider_error or chunk
continue
yield chunk
continue
kind = data.get('type')
if kind == 'completion_decision':
existing = data.get('data') or {}
awaiting |= existing.get('status') == 'awaiting_user'
exhausted |= existing.get('status') == 'exhausted'
continue
if kind in {'metrics', 'agent_terminal'}:
metrics_events.append(data)
declared = (data.get('data') or {}).get('completion_requirements')
awaiting |= bool((data.get('data') or {}).get('missing_workspace'))
if isinstance(declared, dict):
requirements = requirements_from_runtime_context({'completion_requirements': declared})
requirements = replace(requirements, workspace_root=trusted_workspace or '')
# New obligations affect future receipts only. Never
# backfill historical versions with present bytes.
journal.observed_artifacts = tuple(dict.fromkeys(
(*journal.observed_artifacts, *requirements.required_artifacts)))
continue
if kind == 'ask_user':
awaiting = True
payload = data.get('data') or {}
if isinstance(payload.get('question'), str):
current = _ledger(journal, requirements)
question, why = completion_answer(payload['question'], current, current.evaluate(awaiting_user=True))
if why:
data = {**data, 'data': {**payload, 'question': question}}
chunk = _event(data)
if kind == 'final_response':
if first_answer_at is None:
first_answer_at = perf_counter()
answer = str(data.get('content') or '')
has_final = True
answer_events.append(data)
continue
if 'delta' in data or isinstance(data.get('thinking'), str):
if first_answer_at is None:
first_answer_at = perf_counter()
# Boolean thinking=True marks a reasoning-only delta;
# a textual thinking companion must not hide an answer
# delta. Both shapes remain buffered until the gate.
if isinstance(data.get('thinking'), str):
answer_events.append({'delta': data['thinking'], 'thinking': True})
data = {key: value for key, value in data.items() if key != 'thinking'}
if 'delta' not in data:
continue
if data.get('thinking') is not True and 'delta' in data:
if has_final:
answer = ''
has_final = False
answer += str(data.get('delta') or '')
answer_events.append(data)
continue
yield chunk
if provider_error and not answer_events and not metrics_events:
yield provider_error
return
presentation_replaced = False
if not provider_error and not has_final and requirements.required_artifacts:
terminal_texts = next((event.get('data', {}).get('round_texts')
for event in reversed(metrics_events)
if isinstance(event.get('data', {}).get('round_texts'), list)
and all(isinstance(text, str) for text in event['data']['round_texts'])), None)
if terminal_texts is not None:
terminal_answer = '\n\n'.join(text for text in terminal_texts if text.strip())
if terminal_answer != answer:
# The loop can retract a rejected round while retaining
# its live deltas. Do not resurrect those buffered drafts
# after recovery. Terminal prose still passes this gate.
presentation_replaced = True
answer = terminal_answer
answer_events = [event for event in answer_events if event.get('thinking') is True]
ledger = _ledger(journal, requirements)
decision = ledger.evaluate(exhausted=exhausted, awaiting_user=awaiting)
if provider_error:
decision = replace(decision, status=CompletionStatus.FAILED,
can_complete=False, reason='Model request failed')
# Exhaustion limits execution; factual source synthesis can remain
# useful and must not be replaced merely because the budget ended.
presentation_decision = ledger.evaluate(awaiting_user=awaiting) if exhausted and not provider_error else decision
filtered_answer, reason = _completion_answer(answer, ledger, presentation_decision)
safe_answer = _disclose(filtered_answer, ledger)
# Evaluate each earlier draft as well as the final replacement.
# Never replay an unsupported intermediate success claim.
draft = ''.join(str(e.get('delta') or e.get('content') or '')
+ (e['thinking'] if isinstance(e.get('thinking'), str) else '')
for e in answer_events)
_, unsafe_draft = completion_answer(draft, ledger, presentation_decision)
if not answer.strip() and unsafe_draft:
reason = reason or unsafe_draft
safe_answer, _ = completion_answer(draft, ledger, presentation_decision)
if reason and _execution_obligation(requirements) and decision.can_complete and decision.status == CompletionStatus.UNVERIFIED:
decision = CompletionDecision(CompletionStatus.UNVERIFIED, False, reason,
decision.evidence_ids, decision.missing_artifacts)
released_at = perf_counter()
if not provider_error:
yield _event({'type': 'completion_decision', 'data': decision.to_dict()})
# When the only change is the server's effect disclosure, the
# model's answer events are released unchanged and the disclosure
# follows them, so no earlier-round text is dropped.
disclosure = _disclosure(filtered_answer, ledger)
disclosure_only = bool(disclosure) and not (presentation_replaced or reason or unsafe_draft
or filtered_answer != answer)
replaced_answer = not disclosure_only and bool(
presentation_replaced or reason or unsafe_draft or safe_answer != answer)
if replaced_answer:
reasoning = [event for event in answer_events if event.get('thinking') is True]
_, unsafe_reasoning = completion_answer(
''.join(str(event.get('delta') or '') for event in reasoning), ledger,
replace(presentation_decision, can_complete=True))
if not unsafe_reasoning:
for event in reasoning:
yield _event(event)
yield _event({'type': 'final_response', 'content': safe_answer})
else:
for event in answer_events:
yield _event(event)
if disclosure_only:
yield _event({'delta': disclosure})
if provider_error:
yield _event({'type': 'completion_decision', 'data': decision.to_dict()})
for event in metrics_events:
metadata = event.setdefault('data', {})
metadata.update(completion_decision=decision.to_dict(), evidence_events=ledger.to_list(),
action_receipts=journal.to_list(), completion_requirements=requirements.to_dict(),
run_id=journal.run_id, parent_run_id=journal.parent_run_id)
if ledger.effects:
metadata['effect_assessments'] = [entry['assessment'].to_dict() for entry in ledger.effects]
metadata['completion_gate'] = {
'buffer_seconds': released_at - first_answer_at if first_answer_at is not None else 0,
'first_visible_answer_seconds': released_at - started,
'additional_provider_calls': 0,
'answer_replaced': replaced_answer,
}
if replaced_answer:
if not provider_error:
metadata['round_texts'] = [safe_answer]
metadata['completion_gate_reason'] = reason or unsafe_draft or 'receipt_summary'
elif disclosure_only and metadata.get('round_texts') and isinstance(metadata['round_texts'], list) \
and isinstance(metadata['round_texts'][-1], str):
# Reload renders round_texts: keep the disclosure with them.
metadata['round_texts'] = [*metadata['round_texts'][:-1], metadata['round_texts'][-1].rstrip() + disclosure]
if provider_error and isinstance(metadata.get('round_texts'), list):
# Failed rounds stay as per-round diagnostics, but they are
# rendered again on reload. Apply the same statement filter
# as the live answer so a rejected claim cannot reappear.
metadata['round_texts'] = [
_supported_prose(text, ledger, presentation_decision)[0] if isinstance(text, str) else text
for text in metadata['round_texts']]
if isinstance(metadata.get('thinking'), str):
_, unsafe_thinking = completion_answer(metadata['thinking'], ledger,
replace(presentation_decision, can_complete=True))
if unsafe_thinking:
metadata.pop('thinking')
yield _event(event)
if provider_error:
yield provider_error
return
if done:
yield 'data: [DONE]\n\n'
return wrapped
+519
View File
@@ -0,0 +1,519 @@
"""Effective model context window, resolved once per logical turn.
A turn resolves the window it budgets against at preparation time, before any
model request, and keeps the value together with the evidence that chose it.
Terminal metrics report that stored resolution; they never start discovery.
Evidence classes are kept apart instead of being folded into one "known" flag:
* ``runtime_confirmed``: the serving process reported its active window
(llama.cpp ``/slots`` or ``/props``) or rejected a request of this turn with
an explicit limit.
* ``provider_advertised``: the provider's model catalog lists a window.
* ``operator_declared``: the client or operator declared a transport window.
It caps runtime or provider evidence and replaces weaker evidence.
* ``known_table``: the static ``KNOWN_CONTEXT_WINDOWS`` fallback.
* ``unknown``: nothing above is available. The value is 0, never a default.
Any disagreement between sources is recorded as a conflict. An operator value
below a measured value is a cap, not a contradiction; an operator value above
it is a contradiction.
Context sizing is not authority: nothing here grants or denies an operation.
"""
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field, replace
from enum import Enum
import hashlib
import json
import logging
import time
from typing import Any, Mapping, Optional
from urllib.parse import urlparse
import httpx
logger = logging.getLogger(__name__)
# Upper bound for all provider metadata I/O of one turn preparation. A slow
# or unreachable metadata endpoint costs at most this much before the turn
# proceeds with whatever evidence it has.
PROBE_DEADLINE_SECONDS = 3.0
# Remote provider metadata changes rarely; failures are retried sooner so a
# transient outage does not pin a turn to weaker evidence for long. Local
# servers are always re-probed because they can restart with another window.
PROBE_CACHE_TTL_SECONDS = 600.0
PROBE_FAILURE_TTL_SECONDS = 60.0
# Headers that describe the chat request body rather than the caller.
_REQUEST_ONLY_HEADERS = frozenset({"content-type", "content-length", "accept", "accept-encoding"})
class ContextEvidence(str, Enum):
RUNTIME_CONFIRMED = "runtime_confirmed"
PROVIDER_ADVERTISED = "provider_advertised"
OPERATOR_DECLARED = "operator_declared"
KNOWN_TABLE = "known_table"
UNKNOWN = "unknown"
@dataclass(frozen=True)
class ContextObservation:
evidence: ContextEvidence
value: int
source: str
def to_dict(self) -> dict:
return {"evidence": self.evidence.value, "value": self.value, "source": self.source}
@dataclass(frozen=True)
class ContextConflict:
first: ContextObservation
second: ContextObservation
def to_dict(self) -> dict:
return {"first": self.first.to_dict(), "second": self.second.to_dict()}
@dataclass(frozen=True)
class ContextResolution:
"""The effective window of one turn and why it was chosen."""
effective: int
evidence: ContextEvidence
source: str
observations: tuple[ContextObservation, ...] = ()
conflicts: tuple[ContextConflict, ...] = ()
provider_io: bool = False
cached: bool = False
probe_errors: tuple[str, ...] = ()
# The route this resolution describes. Empty for resolutions built
# directly from observations by internal callers. The URL can carry
# credentials, so it stays out of repr() and to_dict().
endpoint_url: str = field(default="", repr=False)
model: str = ""
@property
def mismatch(self) -> bool:
return bool(self.conflicts)
@property
def budget_limit(self) -> int:
"""Window the runtime may budget against; 0 means budget reactively."""
return self.effective if self.evidence is not ContextEvidence.UNKNOWN else 0
@property
def shaping_window(self) -> int:
"""Window for the legacy history compaction/trim helpers.
Those helpers predate typed evidence and always size against some
window, using DEFAULT_CONTEXT when none is known. This only feeds them
a number; it never creates provenance for that number.
"""
if self.budget_limit:
return self.budget_limit
from src.model_context import DEFAULT_CONTEXT
return DEFAULT_CONTEXT
def applies_to(self, endpoint_url: str, model: str) -> bool:
"""Whether this resolution may be reused for the given route."""
if not self.endpoint_url and not self.model:
return True
return self.endpoint_url == endpoint_url and self.model == model
def observe_runtime_limit(self, limit: Any, source: str = "provider_rejection") -> "ContextResolution":
"""Fold a limit the provider stated during this turn. Performs no I/O."""
try:
value = int(limit or 0)
except (TypeError, ValueError):
return self
if value <= 0:
return self
observation = ContextObservation(ContextEvidence.RUNTIME_CONFIRMED, value, source)
if observation in self.observations:
return self
combined = combine_observations((*self.observations, observation))
return replace(
combined,
provider_io=self.provider_io,
cached=self.cached,
probe_errors=self.probe_errors,
endpoint_url=self.endpoint_url,
model=self.model,
)
def to_dict(self) -> dict:
return {
"effective": self.effective,
"evidence": self.evidence.value,
"source": self.source,
"mismatch": self.mismatch,
"conflicts": [conflict.to_dict() for conflict in self.conflicts],
"observations": [observation.to_dict() for observation in self.observations],
"provider_io": self.provider_io,
"cached": self.cached,
"probe_errors": list(self.probe_errors),
}
UNRESOLVED_CONTEXT = ContextResolution(0, ContextEvidence.UNKNOWN, "none")
_MEASURED = (ContextEvidence.RUNTIME_CONFIRMED, ContextEvidence.PROVIDER_ADVERTISED)
def _conflicts(observations: tuple[ContextObservation, ...]) -> tuple[ContextConflict, ...]:
conflicts = []
for index, first in enumerate(observations):
for second in observations[index + 1:]:
if first.value == second.value:
continue
classes = {first.evidence, second.evidence}
if ContextEvidence.OPERATOR_DECLARED in classes:
operator, other = (
(first, second) if first.evidence is ContextEvidence.OPERATOR_DECLARED
else (second, first)
)
# A declared window replaces the static table and may cap a
# measured window. Only a declaration above what the runtime
# or provider supports contradicts it.
if other.evidence not in _MEASURED or operator.value < other.value:
continue
conflicts.append(ContextConflict(first, second))
return tuple(conflicts)
def _strongest(observations, evidence: ContextEvidence) -> Optional[ContextObservation]:
matching = [observation for observation in observations if observation.evidence is evidence]
return min(matching, key=lambda observation: observation.value) if matching else None
def combine_observations(observations) -> ContextResolution:
"""Choose the effective window deterministically from observations.
The smallest runtime-confirmed value wins, else the smallest provider
value. An operator declaration caps either, and replaces the known table
or an unknown window. The known table is used only when nothing stronger
exists. No observation yields an unknown window of 0.
"""
observations = tuple(observations)
measured = (
_strongest(observations, ContextEvidence.RUNTIME_CONFIRMED)
or _strongest(observations, ContextEvidence.PROVIDER_ADVERTISED)
)
operator = _strongest(observations, ContextEvidence.OPERATOR_DECLARED)
if measured and operator:
chosen = operator if operator.value < measured.value else measured
else:
chosen = measured or operator or _strongest(observations, ContextEvidence.KNOWN_TABLE)
conflicts = _conflicts(observations)
if chosen is None:
return replace(UNRESOLVED_CONTEXT, observations=observations, conflicts=conflicts)
return ContextResolution(
chosen.value, chosen.evidence, chosen.source,
observations=observations, conflicts=conflicts,
)
# ---------------------------------------------------------------------------
# Provider metadata probe
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class _ProbeResult:
observations: tuple[ContextObservation, ...] = ()
errors: tuple[str, ...] = ()
io: bool = False
_probe_cache: dict[tuple[str, str, str], tuple[float, _ProbeResult]] = {}
def clear_probe_cache() -> None:
_probe_cache.clear()
_DEFAULT_PORTS = {"http": 80, "https": 443}
def _origin(url: str) -> tuple[str, str, Optional[int]]:
parsed = urlparse(url or "")
scheme = parsed.scheme.lower()
try:
port = parsed.port
except ValueError:
return ("", "", None)
return (scheme, (parsed.hostname or "").lower(), port or _DEFAULT_PORTS.get(scheme))
def _http_client(timeout: float):
# Credentials must never follow a redirect to another location.
return httpx.AsyncClient(timeout=timeout, follow_redirects=False)
def _provider_urls(endpoint_url: str) -> tuple[Optional[str], str]:
"""Models catalog URL and the server-resolved form of the endpoint.
Both come from the existing endpoint resolver, which may rewrite an
unresolvable host to its Tailscale address. Blocking (DNS, subprocess);
call it off the event loop.
"""
from src.endpoint_resolver import build_models_url, resolve_url
return build_models_url(endpoint_url), resolve_url(endpoint_url)
def _probe_headers(trusted_origins, target_url: str, headers: Optional[Mapping[str, Any]]) -> dict:
"""Forward the turn's provider credentials only to the provider's origin."""
origin = _origin(target_url)
if not headers or not origin[1] or origin not in trusted_origins:
return {}
return {
str(name): str(value) for name, value in headers.items()
if value is not None and str(name).lower() not in _REQUEST_ONLY_HEADERS
}
def _auth_fingerprint(headers: Optional[Mapping[str, Any]]) -> str:
if not headers:
return ""
material = json.dumps(
sorted((str(k).lower(), str(v)) for k, v in headers.items()
if v is not None and str(k).lower() not in _REQUEST_ONLY_HEADERS),
separators=(",", ":"),
)
return hashlib.sha256(material.encode("utf-8")).hexdigest()[:16]
def _serving_base(endpoint_url: str) -> str:
# Same derivation the regular runtime uses for llama.cpp server routes.
return endpoint_url.split("/v1")[0] if "/v1" in endpoint_url else endpoint_url.rsplit("/", 1)[0]
async def _get_json(client, url, headers, errors, label):
try:
response = await client.get(url, headers=headers)
except httpx.TimeoutException:
errors.append(f"{label}:timeout")
return None
except httpx.TransportError:
errors.append(f"{label}:transport_error")
return None
status = getattr(response, "status_code", 0)
if not (200 <= int(status or 0) < 300):
errors.append(f"{label}:http_{status}")
return None
try:
return response.json()
except Exception:
errors.append(f"{label}:invalid_payload")
return None
def _positive_int(value) -> int:
if isinstance(value, bool) or not isinstance(value, (int, float)) or value <= 0:
return 0
return int(value)
async def _probe(endpoint_url, model, headers, is_local, observations, errors, timeout):
from src.copilot import is_copilot_base
from src.model_context import _model_ctx_from_entry
# Credentials go only to the configured provider's origin, or to the
# form of that same endpoint the server-owned resolver produced.
trusted = {_origin(endpoint_url)}
async with _http_client(timeout) as client:
if is_local:
base = _serving_base(endpoint_url)
slots = await _get_json(
client, f"{base}/slots", _probe_headers(trusted, f"{base}/slots", headers),
errors, "slots",
)
n_ctx = _positive_int(slots[0].get("n_ctx")) if (
isinstance(slots, list) and slots and isinstance(slots[0], dict)
) else 0
if not n_ctx:
props = await _get_json(
client, f"{base}/props", _probe_headers(trusted, f"{base}/props", headers),
errors, "props",
)
generation = props.get("default_generation_settings") if isinstance(props, dict) else None
n_ctx = _positive_int(generation.get("n_ctx")) if isinstance(generation, dict) else 0
source = "llamacpp_props"
else:
source = "llamacpp_slots"
if n_ctx:
observations.append(
ContextObservation(ContextEvidence.RUNTIME_CONFIRMED, n_ctx, source)
)
# Copilot's catalog needs headers this layer does not own; an
# unauthenticated probe only fails. Its models are table-covered.
if is_copilot_base(endpoint_url):
errors.append("models:unsupported_endpoint")
return
# URL building may resolve the host (DNS, tailscale lookup); keep that
# off the event loop and inside the probe deadline.
models_url, resolved_endpoint = await asyncio.to_thread(_provider_urls, endpoint_url)
if not models_url:
errors.append("models:unsupported_endpoint")
return
trusted.add(_origin(resolved_endpoint))
payload = await _get_json(
client, models_url, _probe_headers(trusted, models_url, headers),
errors, "models",
)
if payload is None:
return
entries = payload.get("data") if isinstance(payload, dict) else None
if not isinstance(entries, list):
errors.append("models:invalid_payload")
return
wanted = model.split("/")[-1]
for entry in entries:
if not isinstance(entry, dict):
continue
entry_id = str(entry.get("id") or "")
if entry_id == model or entry_id.split("/")[-1] == wanted:
value = _model_ctx_from_entry(entry)
if value:
observations.append(ContextObservation(
ContextEvidence.PROVIDER_ADVERTISED, int(value), "models_catalog",
))
else:
errors.append("models:no_window_listed")
return
errors.append("models:model_not_listed")
async def probe_provider_context(
endpoint_url: str,
model: str,
*,
headers: Optional[Mapping[str, Any]] = None,
deadline_seconds: float = PROBE_DEADLINE_SECONDS,
is_local: Optional[bool] = None,
) -> _ProbeResult:
"""Query provider metadata once, bounded by ``deadline_seconds``.
Never raises: every failure is reported as a short, secret-free error code
so a turn can continue with other evidence.
"""
observations: list[ContextObservation] = []
errors: list[str] = []
if is_local is None:
is_local = await _is_local(endpoint_url)
timeout = max(0.1, float(deadline_seconds))
try:
await asyncio.wait_for(
_probe(endpoint_url, model, headers, is_local, observations, errors, timeout),
timeout=timeout,
)
except asyncio.TimeoutError:
errors.append("deadline_exceeded")
except Exception as exc:
logger.debug("Context window probe failed: %s", type(exc).__name__)
errors.append("probe_failed")
return _ProbeResult(tuple(observations), tuple(errors), io=True)
async def _is_local(endpoint_url: str) -> bool:
from src.model_context import is_local_endpoint
try:
# Reads configured endpoints from the local database on the calling
# thread, as the regular runtime does. Moving it to worker threads
# gives SQLite sessions per-thread connections the app never uses.
return bool(is_local_endpoint(endpoint_url))
except Exception:
return False
async def _cached_probe(endpoint_url, model, headers, deadline_seconds, clock):
is_local = await _is_local(endpoint_url)
key = (endpoint_url, model, _auth_fingerprint(headers))
if not is_local:
cached = _probe_cache.get(key)
if cached and cached[0] > clock():
return cached[1], True
result = await probe_provider_context(
endpoint_url, model, headers=headers, deadline_seconds=deadline_seconds,
is_local=is_local,
)
if not is_local:
ttl = PROBE_CACHE_TTL_SECONDS if result.observations else PROBE_FAILURE_TTL_SECONDS
_probe_cache[key] = (clock() + ttl, result)
return result, False
def declared_context_window(client_runtime_context: Any) -> int:
"""Operator/client declared transport window, or 0."""
if not isinstance(client_runtime_context, Mapping):
return 0
try:
value = int(client_runtime_context.get("model_context_window") or 0)
except (TypeError, ValueError):
return 0
return value if value > 0 else 0
async def resolve_effective_context(
endpoint_url: str,
model: str,
*,
headers: Optional[Mapping[str, Any]] = None,
client_runtime_context: Any = None,
deadline_seconds: float = PROBE_DEADLINE_SECONDS,
probe: bool = True,
clock=time.monotonic,
) -> ContextResolution:
"""Resolve the effective context window for one turn preparation."""
from src.model_context import _lookup_known
observations: list[ContextObservation] = []
errors: tuple[str, ...] = ()
provider_io = cached = False
if probe and endpoint_url and model:
result, cached = await _cached_probe(
endpoint_url, model, headers, deadline_seconds, clock,
)
observations.extend(result.observations)
errors = result.errors
provider_io = result.io and not cached
declared = declared_context_window(client_runtime_context)
if declared:
observations.append(ContextObservation(
ContextEvidence.OPERATOR_DECLARED, declared, "client_runtime_context",
))
known = _lookup_known(model or "")
if known:
observations.append(ContextObservation(ContextEvidence.KNOWN_TABLE, int(known), "known_table"))
resolution = combine_observations(observations)
resolution = replace(
resolution, provider_io=provider_io, cached=cached, probe_errors=errors,
endpoint_url=endpoint_url or "", model=model or "",
)
if resolution.mismatch:
logger.info(
"Context window sources disagree for %s: %s",
model, [conflict.to_dict() for conflict in resolution.conflicts],
)
return resolution
def context_metrics(resolution: Optional[ContextResolution], request_tokens: int) -> dict:
"""Metrics fields derived only from a stored resolution. Performs no I/O."""
resolution = resolution or UNRESOLVED_CONTEXT
length = resolution.budget_limit
percent = (
min(round((request_tokens / length) * 100, 1), 100.0)
if length and request_tokens else 0
)
return {
"context_length": length,
"context_percent": percent,
"context_resolution": resolution.to_dict(),
}
+522
View File
@@ -0,0 +1,522 @@
"""Server-boundary adapters from admitted Wave 3 bindings to effect records.
Runs only inside the dispatcher's existing admission scope: the bindings read
here are the contextvars the dispatcher bound after authority, resource and
approval checks. Nothing here admits, resolves, broadens or re-derives a
resource. Observations are recorded only for operations that were themselves
admitted reads of the exact bound resource; evidence bookkeeping never performs
a read that the operation was not already admitted to perform.
"""
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
import hashlib
import io
import json
import logging
import os
import stat
from typing import Any
from src.agent_runtime.effects import (
CleanupState, Coverage, EffectClaim, ExecutionOutcome, Impact, ObservationMechanism, OperationRef,
Postcondition, Predicate, ProducerFacts, ResourceKind, ResourceRef, producer_facts, resource_ref,
)
_FILESYSTEM_READS = frozenset({"read_file", "ls", "glob", "grep"})
_JOB_READS = frozenset({"list", "ls", "jobs", "output", "get", "read", "tail", "status", "show"})
_OWNED_READS = frozenset({"vault_get", "vault_search", "list_sessions", "search_chats"})
_JOB_SETTLED = {"done", "failed"}
# Largest pre-state an edit/patch postcondition is derived from.
_PRE_STATE_LIMIT = 10 * 1024 * 1024
logger = logging.getLogger(__name__)
@dataclass
class DispatchCapture:
"""The admitted bindings that were live when the backend was invoked."""
filesystem: Any = None
owned: Any = None
process: Any = None
backend: Any = None
browser: Any = None
claim: EffectClaim | None = None
read_only: bool = False
paths: tuple[str, ...] = field(default_factory=tuple)
def capture_dispatch() -> DispatchCapture:
from src.agent_runtime.owned_resources import active_owned_operation
from src.agent_runtime.process_resources import active_process_operation
from src.agent_runtime.remote_resources import active_backend_operation
from src.agent_runtime.resource_binding import active_resource_operation
import sys
browser_module = sys.modules.get("src.browser_identity")
browser = browser_module._ACTIVE.get() if browser_module is not None else None
return DispatchCapture(active_resource_operation(), active_owned_operation(), active_process_operation(),
active_backend_operation(), browser)
def _exact_operation(capture: DispatchCapture):
for bound in (capture.filesystem, capture.owned, capture.process, capture.browser):
if bound is not None:
return bound.operation, getattr(bound, "execution_input", None), getattr(bound, "request_id", "")
return None, None, ""
def _operation(capture: DispatchCapture, action: Any) -> OperationRef:
operation, execution_input, request_id = _exact_operation(capture)
if operation is not None:
return OperationRef.from_exact(operation, execution_input, request_id)
backend = capture.backend
# Unbound tools still name their final normalized dispatcher input.
digest = hashlib.sha256(str(action.arguments).encode("utf-8", errors="replace")).hexdigest()
return OperationRef(str(action.tool) or "unknown", "", digest,
getattr(backend, "request_id", "") if backend is not None else "")
def _write_file_digest(execution_input: str, path: str) -> str:
"""The exact bytes WriteFileTool commits for this admitted input, or ''."""
from src.agent_tools.filesystem_tools import _unwrap_fenced_source_body
try:
args = json.loads(execution_input)
except (TypeError, ValueError):
return ""
body = args.get("content") if isinstance(args, dict) else None
if not isinstance(body, str) or os.linesep != "\n":
return ""
return hashlib.sha256(_unwrap_fenced_source_body(body, path).encode("utf-8")).hexdigest()
def _pre_state_text(resource: Any, *, newline: str | None) -> str | None:
"""The exact bound file decoded as its producer decodes it, or None.
Reads only the admitted target binding (identity-checked). An oversized,
replaced or undecodable file yields None: a truncated read must never
stand in for the whole pre-state.
"""
data = _read_whole(resource, _PRE_STATE_LIMIT).data
if data is None or len(data) > _PRE_STATE_LIMIT:
return None
try:
return io.TextIOWrapper(io.BytesIO(data), encoding="utf-8", newline=newline).read()
except (UnicodeDecodeError, ValueError):
return None
def _edit_file_digest(execution_input: str, resource: Any) -> str:
"""SHA-256 of the exact bytes edit_file writes for this admitted input, or ''."""
from src.agent_tools.filesystem_tools import _edit_file_text
try:
args = json.loads(execution_input)
except (TypeError, ValueError):
return ""
if not isinstance(args, dict):
return ""
old, new, replace_all = args.get("old_string"), args.get("new_string"), args.get("replace_all", False)
if not isinstance(old, str) or not old or not isinstance(new, str) or type(replace_all) is not bool or old == new:
return ""
# edit_file reads with newline="" and writes with newline="": no translation.
original = _pre_state_text(resource, newline="")
if original is None:
return ""
updated, _ = _edit_file_text(original, old, new, replace_all)
return "" if updated is None else hashlib.sha256(updated.encode("utf-8")).hexdigest()
def _patch_update_digest(op: dict, resource: Any) -> str:
"""SHA-256 of the exact bytes apply_patch writes for one update, or ''."""
from src.agent_tools.filesystem_tools import _apply_patch_hunks
# apply_patch reads updates with universal newlines and writes newline="".
original = _pre_state_text(resource, newline=None)
if original is None:
return ""
try:
updated = _apply_patch_hunks(original, op["hunks"], op["path"])
except ValueError:
return ""
return hashlib.sha256(updated.encode("utf-8")).hexdigest()
def _filesystem_scope(bound: Any) -> tuple[tuple[ResourceRef, ...], tuple[Postcondition, ...]]:
"""Exact bindings and the requested post-state of each mutation target.
Each postcondition is the exact content (or absence) the producer's own
transformation yields from the admitted pre-state, so an unrelated change
can never satisfy it. When any target's requested state cannot be derived
the claim carries no postcondition at all and stays UNVERIFIED: a partial
set would let the derivable targets verify the whole operation.
"""
from src.agent_tools.filesystem_tools import _parse_agent_patch
tool = bound.operation.tool
refs = tuple(resource_ref(b.resource, b.role) for b in bound.bindings)
obligations: list[Postcondition] = []
if tool == "write_file":
expected = _write_file_digest(bound.execution_input, bound.bindings[0].resource.path)
intent = bound.write_intent
if intent is not None:
from src.agent_tools.filesystem_tools import _unwrap_fenced_source_body
resource = bound.bindings[0].resource
body = _unwrap_fenced_source_body(intent[1], resource.path)
if not body.strip() and not intent[3] and resource.identity is not None:
pre_state = _pre_state_text(resource, newline=None)
expected = hashlib.sha256(b"").hexdigest() if pre_state == "" else ""
if not expected:
return refs, ()
obligations.append(Postcondition(refs[0], Predicate.CONTENT_SHA256, expected))
elif tool == "edit_file":
expected = _edit_file_digest(bound.execution_input, bound.bindings[0].resource)
if not expected:
return refs, ()
obligations.append(Postcondition(refs[0], Predicate.CONTENT_SHA256, expected))
elif tool == "apply_patch":
ops = _parse_agent_patch(json.loads(bound.execution_input)["patch_text"])
if len(ops) != len(bound.bindings):
return refs, ()
for op, binding, ref in zip(ops, bound.bindings, refs):
if op["kind"] == "add":
obligations.append(Postcondition(ref, Predicate.CONTENT_SHA256,
hashlib.sha256(op["content"].encode("utf-8")).hexdigest()))
elif op["kind"] == "delete":
obligations.append(Postcondition(ref, Predicate.ABSENT))
else:
expected = _patch_update_digest(op, binding.resource)
if not expected:
return refs, ()
obligations.append(Postcondition(ref, Predicate.CONTENT_SHA256, expected))
return refs, tuple(obligations)
def classify(capture: DispatchCapture) -> dict[str, Any] | None:
"""Claim scope for the captured bindings, or None for an admitted read.
Unbound operations get an unknown-scope claim: they may change anything.
"""
impact: tuple[ResourceRef, ...] = ()
dependencies: tuple[ResourceRef, ...] = ()
obligations: tuple[Postcondition, ...] = ()
external = False
if capture.browser is not None:
# Wave 3 admits only session metadata. A page binding is never
# effect-bindable; leave its scope unknown rather than infer it.
if capture.browser.page is None:
return None
elif capture.filesystem is not None:
if capture.filesystem.operation.tool in _FILESYSTEM_READS:
return None
impact, obligations = _filesystem_scope(capture.filesystem)
elif capture.process is not None:
bound = capture.process
if bound.launch is not None:
# An arbitrary command has unknown impact scope; the exact launch
# reservation is kept only as lineage for background settlement.
dependencies = (resource_ref(bound.launch, "launch"),)
else:
action = str(json.loads(bound.operation.input or "{}").get("action", "list")).strip().lower()
if action in _JOB_READS:
return None
impact = tuple(resource_ref(job, "job") for job in bound.jobs) + tuple(
resource_ref(process, "process") for job in bound.jobs for process in job.processes) + tuple(
resource_ref(process, "process") for process in bound.processes)
elif capture.owned is not None:
if capture.owned.operation.tool in _OWNED_READS:
return None
impact = tuple(resource_ref(r, "record") for r in capture.owned.resources)
dependencies = tuple(resource_ref(a.file, "attachment") for a in capture.owned.attachments)
if capture.backend is not None:
from src.agent_runtime.resources import ExternalResource
if isinstance(capture.backend.resource, ExternalResource):
external = True
impact = (*impact, resource_ref(capture.backend.resource, "backend"))
return {"impact_scope": impact, "dependencies": dependencies, "obligations": obligations, "external": external}
def begin_effect(journal: Any, action: Any) -> DispatchCapture:
"""Capture bindings and durably claim a possible effect before invocation."""
capture = capture_dispatch()
log = journal.effects
try:
scope = classify(capture)
except Exception: # noqa: BLE001 - classification never blocks dispatch
# An unclassifiable admitted operation may change anything.
logger.warning("Effect scope classification failed; claiming unknown scope", exc_info=True)
scope = {"impact_scope": (), "dependencies": (), "obligations": (), "external": False}
if scope is None:
capture.read_only = True
else:
capture.claim = log.claim(effect_id=action.action_id + ":effect", run_id=journal.run_id,
action_id=action.action_id, operation=_operation(capture, action),
parent_run_id=journal.parent_run_id or "", **scope)
capture.paths = tuple(ref.location[-1] for ref in capture.claim.impact_scope
if ref.kind is ResourceKind.FILESYSTEM)
for ref in capture.claim.dependencies:
if ref.kind is ResourceKind.PROCESS_LAUNCH:
try:
log.index_launch(ref.incarnation, capture.claim.effect_id)
except (OSError, ValueError):
# Without the index a later turn cannot settle this
# launch: it stays running/unknown, never successful.
logger.warning("Background launch lineage was not indexed", exc_info=True)
return capture
def _server_producer(capture: DispatchCapture) -> bool:
"""The backend was a server-owned producer bound by Wave 3 admission.
Only such producers build their result dictionaries from server state. An
unbound dynamic/registry tool returns whatever it likes, so its keys carry
no lifecycle meaning. The MCP bridge builds only stdout/stderr/exit_code.
"""
return any(bound is not None for bound in (capture.filesystem, capture.owned, capture.process, capture.browser))
def _facts(result: Any, capture: DispatchCapture) -> ProducerFacts:
"""Typed producer facts, scoped to what the captured producer can attest."""
facts = producer_facts(result)
if not _server_producer(capture):
# Reported success or failure is all an untrusted result can say.
facts = ProducerFacts(exit_code=facts.exit_code)
if capture.backend is not None and capture.claim is not None and capture.claim.external:
facts = ProducerFacts(**{**facts.to_dict(), "external": True, "remote_acknowledged": facts.exit_code == 0})
return facts
def _execution(result: Any, facts: ProducerFacts, capture: DispatchCapture) -> ExecutionOutcome:
if not isinstance(result, dict):
return ExecutionOutcome.INTERRUPTED
if facts.timed_out:
return ExecutionOutcome.TIMED_OUT
# Only a server process producer can say this operation's own work
# continues: the native detached launch of an exact Wave 3 launch
# reservation, or the host bridge's server-set detachment. Lifecycle keys
# from any other producer (or a listing reporting something else as
# running) do not.
process = capture.process
if process is not None:
if process.launch is not None and isinstance(result.get("bg_job_id"), str) and facts.exit_code == 0:
return ExecutionOutcome.RUNNING
if result.get("detached") is True:
return ExecutionOutcome.RUNNING
denied = bool(result.get("blocked") or result.get("approval_required")
or facts.failure_kind.endswith("_denied"))
if facts.exit_code == 0 and not result.get("error") and not denied:
return ExecutionOutcome.REPORTED_SUCCESS
return ExecutionOutcome.FAILED
def _cleanup(result: Any, facts: ProducerFacts, capture: DispatchCapture) -> CleanupState:
if not isinstance(result, dict):
return CleanupState.UNKNOWN
if facts.external:
# External execution reports no locally observed teardown.
return CleanupState.UNKNOWN
if capture.process is None:
return CleanupState.NOT_APPLICABLE
# Teardown is attested only by the native process/containment producer.
if facts.failure_kind == "process_teardown_failed":
return CleanupState.FAILED
teardown = result.get("teardown")
if isinstance(teardown, dict) and type(teardown.get("dead")) is bool:
return CleanupState.VERIFIED if teardown["dead"] else CleanupState.FAILED
return CleanupState.NOT_APPLICABLE
def settle_effect(journal: Any, action: Any, capture: DispatchCapture | None, *,
result: Any = None, error: BaseException | None = None) -> None:
"""Append the outcome and any admitted-read observations for one action."""
if capture is None:
return
log = journal.effects
if capture.claim is not None:
if error is not None:
execution = (ExecutionOutcome.CANCELLED if isinstance(error, asyncio.CancelledError)
else ExecutionOutcome.INTERRUPTED)
facts, cleanup = ProducerFacts(), CleanupState.UNKNOWN
else:
facts = _facts(result, capture)
execution, cleanup = _execution(result, facts, capture), _cleanup(result, facts, capture)
log.outcome(effect_id=capture.claim.effect_id, execution=execution, impact=Impact.POSSIBLE,
facts=facts, cleanup=cleanup, execution_id=action.execution_id or "")
if (execution is ExecutionOutcome.REPORTED_SUCCESS and capture.process is not None
and capture.process.launch is None):
_settle_background(log, capture, result) # e.g. an exact kill
return
if error is not None or not isinstance(result, dict):
return
successful = result.get("exit_code") == 0 and not result.get("error")
# A missing-file read reports failure, but can independently establish
# absence. No other failed read is eligible for an observation.
absent_read = (capture.filesystem is not None and capture.filesystem.operation.tool == "read_file"
and capture.filesystem.bindings[0].resource.identity is None)
if not successful and not absent_read:
return
for fields in _observations(capture, action, result):
if successful or fields.get("exists") is False:
log.observe(**fields)
if capture.process is not None and capture.process.launch is None:
_settle_background(log, capture, result)
# -- observations ------------------------------------------------------------
@dataclass(frozen=True)
class _WholeFileRead:
data: bytes | None = None
known_absent: bool = False
def _read_whole(resource: Any, limit: int) -> _WholeFileRead:
"""Read a stable binding, distinguish validated ENOENT from uncertainty.
Only a binding admitted as absent can prove absence. Disappearance of an
existing identity, replacement, or any validation/access failure is unknown.
"""
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
try:
resource.validate()
if resource.identity is None:
try:
os.lstat(resource.path)
except FileNotFoundError:
resource.validate()
return _WholeFileRead(known_absent=True)
return _WholeFileRead()
descriptor = os.open(resource.path, flags)
with os.fdopen(descriptor, "rb") as stream:
info = os.fstat(stream.fileno())
identity = resource.identity
if (not stat.S_ISREG(info.st_mode) or identity is None
or (info.st_dev, info.st_ino) != (identity.device, identity.inode)):
return _WholeFileRead()
data = stream.read(limit + 1)
resource.validate()
except (OSError, ValueError, RuntimeError):
return _WholeFileRead()
return _WholeFileRead(data=data)
def _file_observation(capture: DispatchCapture, action: Any) -> dict[str, Any] | None:
from src.agent_tools import filesystem_tools as producer
bound = capture.filesystem
binding = bound.bindings[0]
resource = binding.resource
args = json.loads(bound.execution_input)
partial = bool(args.get("offset") or args.get("limit")) or (
os.path.splitext(resource.path)[1].lower() in producer._STRUCTURED_DOCUMENT_SUFFIXES)
read = _read_whole(resource, producer.MAX_READ_CHARS * 4)
data = read.data
if data is None and not read.known_absent:
return None
if read.known_absent:
partial = False # ENOENT establishes absence of the whole bound path.
elif len(data) > producer.MAX_READ_CHARS * 4 or len(data.decode("utf-8", errors="replace")) > producer.MAX_READ_CHARS:
partial = True # the producer truncated what it read
complete = not partial
return dict(observation_id=action.action_id + ":observation", resource=resource_ref(resource, binding.role),
mechanism=ObservationMechanism.FILESYSTEM_READ,
coverage=Coverage.COMPLETE if complete else Coverage.PARTIAL,
source_action_id=action.action_id, source_execution_id=action.execution_id or "",
exists=not read.known_absent,
content_sha256=hashlib.sha256(data).hexdigest() if complete and data is not None else "")
def _observations(capture: DispatchCapture, action: Any, result: dict) -> list[dict[str, Any]]:
base = dict(source_action_id=action.action_id, source_execution_id=action.execution_id or "")
if capture.browser is not None and capture.browser.page is None:
# Session lifecycle metadata only; never page/document state.
return [dict(observation_id=action.action_id + ":observation",
resource=resource_ref(capture.browser.session, "session"),
mechanism=ObservationMechanism.BROWSER_SESSION, coverage=Coverage.PARTIAL,
exists=True, **base)]
if capture.filesystem is not None:
tool = capture.filesystem.operation.tool
if tool == "read_file":
observation = _file_observation(capture, action)
return [observation] if observation else []
if tool in _FILESYSTEM_READS:
# Listings/searches are partial: they cannot decide content.
return [dict(observation_id=f"{action.action_id}:observation:{i}", resource=resource_ref(b.resource, b.role),
mechanism=ObservationMechanism.FILESYSTEM_READ, coverage=Coverage.PARTIAL, exists=True, **base)
for i, b in enumerate(capture.filesystem.bindings)]
if capture.owned is not None and capture.owned.operation.tool in _OWNED_READS:
return [dict(observation_id=f"{action.action_id}:observation:{i}", resource=resource_ref(r, "record"),
mechanism=ObservationMechanism.OWNED_RECORD_READ, coverage=Coverage.PARTIAL, exists=True, **base)
for i, r in enumerate(capture.owned.resources) if r.record_id != "*"]
if capture.process is not None and capture.process.launch is None:
from src.agent_runtime.process_resources import JOB_TOOL
job = result.get("job")
if isinstance(job, dict) and len(capture.process.jobs) == 1 and capture.process.operation.tool == JOB_TOOL:
return [dict(observation_id=action.action_id + ":observation",
resource=resource_ref(capture.process.jobs[0], "job"),
mechanism=ObservationMechanism.JOB_STATE, coverage=Coverage.PARTIAL, exists=True, **base)]
return []
def _settle_background(log: Any, capture: DispatchCapture, result: dict) -> None:
"""Settle a RUNNING launch claim from an admitted read of its exact job.
Linkage is the Wave 3 launch generation plus owner/request/thread, already
validated by ``job_from_record`` at admission. Job completion is execution
evidence for that claim; it verifies no postcondition.
"""
from src.agent_runtime.process_resources import JOB_TOOL
job_facts = result.get("job")
if (not isinstance(job_facts, dict) or len(capture.process.jobs) != 1
or capture.process.operation.tool != JOB_TOOL):
return
settle_background_job(capture.process.jobs[0], job_facts, log=log)
def settle_background_job(job: Any, job_facts: Any, *, log: Any = None) -> None:
"""Settle the RUNNING launch claim of one exact, Wave 3-validated job.
``job`` must be a ``BackgroundJobResource`` the caller obtained through
Wave 3 validation (an admitted job read, or the monitor's
``job_from_record``/``validate_job``). ``job_facts`` are typed lifecycle
facts from that server-owned record; delivered output is never consulted.
"""
from src.agent_runtime.effect_log import EffectLog, EffectPersistenceError, effects_dir
from src.agent_runtime.resources import BackgroundJobResource
if not isinstance(job, BackgroundJobResource) or not isinstance(job_facts, dict):
return
status = job_facts.get("status")
if status not in _JOB_SETTLED:
return
lineage = ("process_launch", "native:containment", job.owner, job.request_id, job.thread_id, job.generation)
owner = log if log is not None and any(any(ref.kind is ResourceKind.PROCESS_LAUNCH and ref.location == lineage
for ref in c.dependencies) for c in log.history().claims) else None
if owner is None:
# Background continuation: the launch was claimed by an earlier run.
directory = log.path.parent if log is not None and log.path is not None else effects_dir()
indexed = EffectLog.launch_owner(job.generation, directory=directory)
if indexed is not None:
try:
owner = EffectLog.open(indexed[0], directory=directory)
except (EffectPersistenceError, ValueError):
owner = None
if owner is None:
return
history = owner.history()
for claim in history.claims:
if not any(ref.kind is ResourceKind.PROCESS_LAUNCH and ref.location == lineage for ref in claim.dependencies):
continue
latest = history.latest_outcome(claim.effect_id)
if latest is None or latest.execution is not ExecutionOutcome.RUNNING:
continue
code = job_facts.get("exit_code")
code = code if type(code) is int else None
if job_facts.get("timed_out") is True:
execution = ExecutionOutcome.TIMED_OUT
elif job_facts.get("killed") is True:
execution = ExecutionOutcome.CANCELLED
elif status == "done" and code == 0 and job_facts.get("died") is not True:
execution = ExecutionOutcome.REPORTED_SUCCESS
else:
execution = ExecutionOutcome.FAILED
facts = ProducerFacts(exit_code=code, timed_out=job_facts.get("timed_out") is True, job_state=status)
owner.outcome(effect_id=claim.effect_id, execution=execution, impact=Impact.POSSIBLE, facts=facts,
cleanup=CleanupState.UNKNOWN, execution_id=latest.execution_id)
+511
View File
@@ -0,0 +1,511 @@
"""Durable append-only effect log for one root run lineage.
This is the Wave 4 semantic store: claims, outcomes and observations only. It
is not a resource database, a process/containment store or an authority source.
A claim is fsynced before the backend is invoked; if that fails, the caller must
refuse the invocation. Later records are appended; nothing is rewritten.
On reload, a claim without a settled outcome becomes an appended INTERRUPTED
outcome with possible impact. Reload never manufactures success and never
upgrades an old report to fresh state.
Several writers may append to one log (another ``EffectLog`` object, thread or
process settling a background launch). Each append takes an exclusive advisory
lock on the file, merges every durable record other writers appended, allocates
the next position from that merged tail, checks the record against the merged
history, then appends and fsyncs before releasing the lock. Positions therefore
stay unique and a settled outcome is never appended twice. Cross-process
exclusion relies on POSIX ``flock``; directory fsync relies on POSIX directory
semantics. Neither is claimed where the platform does not provide it.
"""
from __future__ import annotations
from contextlib import contextmanager
import json
import os
from pathlib import Path
import re
import stat
import threading
import weakref
from typing import Any, Callable
try: # POSIX only; elsewhere exclusion is per process.
import fcntl
except ImportError: # pragma: no cover - non-POSIX hosts
fcntl = None
from src.constants import DATA_DIR
from src.agent_runtime.effects import (
EffectAssessment, EffectClaim, EffectHistory, EffectOutcome, ExecutionOutcome, Observation, assess_all,
replay_interrupted,
)
from src.agent_runtime.resources import ResourceIdentityError
EFFECTS_DIR = os.path.join(DATA_DIR, "effects")
_RUN_ID = re.compile(r"[a-f0-9]{32}")
_TYPES = {"claim": EffectClaim, "outcome": EffectOutcome, "observation": Observation}
_VERSION = 1
def _fsync_directory(directory: str | os.PathLike) -> None:
"""Make a directory's entries durable. POSIX only; a no-op elsewhere."""
if os.name != "posix":
return
descriptor = os.open(os.fspath(directory), os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
def _ensure_directory(directory: Path) -> None:
"""Create ``directory`` and make every newly created entry durable."""
missing = []
current = directory
while not current.exists():
missing.append(current)
if current.parent == current:
break
current = current.parent
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
for created in reversed(missing):
_fsync_directory(created.parent)
class EffectPersistenceError(ResourceIdentityError):
"""A pre-invocation claim could not be made durable; do not invoke."""
def effects_dir() -> Path:
return Path(EFFECTS_DIR)
def _parse(line: bytes) -> tuple[str, Any]:
entry = json.loads(line.decode("utf-8"))
if (not isinstance(entry, dict) or set(entry) != {"v", "type", "record"}
or entry["v"] != _VERSION or entry["type"] not in _TYPES):
raise ValueError("unsupported effect record")
return entry["type"], _TYPES[entry["type"]].from_dict(entry["record"])
class _Index:
"""Incremental consistency of an append-ordered record stream.
At least as strict as ``EffectHistory`` validation for records appended in
position order, so a record it accepts never makes the history invalid.
"""
def __init__(self) -> None:
self.positions: set[int] = set()
self.claims: dict[str, int] = {}
self.settled: set[str] = set()
self.last: dict[str, int] = {}
def accepts(self, kind: str, record: Any) -> bool:
if record.sequence in self.positions:
return False
if kind == "claim":
return record.effect_id not in self.claims
if kind == "outcome":
effect = record.effect_id
return (effect in self.claims and record.sequence > self.claims[effect]
and effect not in self.settled and record.sequence > self.last.get(effect, -1))
return True
def add(self, kind: str, record: Any) -> None:
self.positions.add(record.sequence)
if kind == "claim":
self.claims[record.effect_id] = record.sequence
elif kind == "outcome":
self.last[record.effect_id] = record.sequence
if record.execution is not ExecutionOutcome.RUNNING:
self.settled.add(record.effect_id)
def _records() -> dict[str, list]:
return {"claim": [], "outcome": [], "observation": []}
class EffectLog:
# Logs still owned by a live run in this process. A later turn appends to
# the same object rather than a second copy of the same history.
_LIVE: "weakref.WeakValueDictionary[tuple[str, str], EffectLog]" = weakref.WeakValueDictionary()
# Serializes the _LIVE check-and-load in ``open``.
_OPEN_LOCK = threading.Lock()
def __init__(self, run_id: str, *, durable: bool = True, directory: str | os.PathLike | None = None) -> None:
if not isinstance(run_id, str) or not _RUN_ID.fullmatch(run_id):
raise ValueError("Effect log requires a server-generated run identifier")
self.run_id = run_id
self.path = (Path(directory) if directory is not None else effects_dir()) / f"{run_id}.jsonl" if durable else None
if self.path is not None:
self._LIVE[(str(self.path.parent), run_id)] = self
# Records known to be on disk, in file order, and the bytes they span.
self._durable, self._durable_index = _records(), _Index()
self._offset = 0
# Records this process holds that are not on disk: a failed non-claim
# write, or an observation of a run that has no durable file yet.
self._volatile: list[tuple[str, Any]] = []
# Durable records plus every volatile record still consistent with
# them. A volatile record that collides with a durable one (another
# writer took its position or settled the same effect) is hidden:
# losing an unpersisted outcome or observation is conservative.
self._view, self._view_index = _records(), _Index()
self._max_sequence = 0
self._descriptor: int | None = None
self._directory_synced = False
# A non-claim record failed to persist. In-memory history stays
# truthful for this process; replay may lack the later record.
self.degraded = False
self._lock = threading.RLock()
# -- merged view ---------------------------------------------------------
def _rebuild_view(self) -> None:
self._view, self._view_index = _records(), _Index()
durable = sorted(((kind, r) for kind, records in self._durable.items() for r in records),
key=lambda pair: pair[1].sequence)
for kind, record in durable:
self._view[kind].append(record)
self._view_index.add(kind, record)
for kind, record in self._volatile:
if self._view_index.accepts(kind, record):
self._view_index.add(kind, record)
self._view[kind].append(record)
def _add_durable(self, kind: str, record: Any) -> None:
self._durable[kind].append(record)
self._durable_index.add(kind, record)
self._view[kind].append(record)
self._view_index.add(kind, record)
self._max_sequence = max(self._max_sequence, record.sequence)
def _add_volatile(self, kind: str, record: Any) -> None:
self._volatile.append((kind, record))
self._view[kind].append(record)
self._view_index.add(kind, record)
self._max_sequence = max(self._max_sequence, record.sequence)
def _merged_history(self) -> EffectHistory:
return EffectHistory(tuple(self._view["claim"]), tuple(self._view["outcome"]),
tuple(self._view["observation"]))
# -- persistence -------------------------------------------------------
def _read_tail(self, descriptor: int, *, repair: bool) -> None:
"""Merge complete records other writers appended after our offset.
A trailing partial line is a write that never returned to its caller
(a crash or a failed write), so no backend invocation followed it.
With ``repair`` (exclusive lock held) it is truncated so the next
append starts on a record boundary.
"""
info = os.fstat(descriptor)
if info.st_nlink != 1 or not stat.S_ISREG(info.st_mode):
raise OSError("Effect log is aliased")
if info.st_size < self._offset:
raise OSError("Effect log shrank under its writer")
if info.st_size == self._offset:
return
os.lseek(descriptor, self._offset, os.SEEK_SET)
remaining, chunks = info.st_size - self._offset, []
while remaining:
chunk = os.read(descriptor, remaining)
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
data = b"".join(chunks)
complete = data[:data.rfind(b"\n") + 1]
if repair and len(complete) != len(data):
os.ftruncate(descriptor, self._offset + len(complete))
os.fsync(descriptor)
added: list[tuple[str, Any]] = []
for line in complete.splitlines():
try:
kind, record = _parse(line)
except (ValueError, TypeError, KeyError, UnicodeDecodeError) as error:
raise OSError("Effect log tail is corrupt") from error
if not self._durable_index.accepts(kind, record):
raise OSError("Effect log tail is inconsistent")
self._durable[kind].append(record)
self._durable_index.add(kind, record)
self._max_sequence = max(self._max_sequence, record.sequence)
added.append((kind, record))
self._offset += len(complete)
if added:
self._rebuild_view()
@contextmanager
def _locked(self):
"""Hold the file exclusively with every durable record merged."""
assert self.path is not None
_ensure_directory(self.path.parent)
flags = os.O_RDWR | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
descriptor = os.open(self.path, flags, 0o600)
try:
if fcntl is not None:
fcntl.flock(descriptor, fcntl.LOCK_EX)
self._read_tail(descriptor, repair=True)
self._descriptor = descriptor
yield
finally:
self._descriptor = None
os.close(descriptor) # releases the lock
def _write(self, kind: str, record: Any) -> None:
"""Append one record under the held lock and make it durable."""
descriptor = self._descriptor
assert descriptor is not None
line = json.dumps({"v": _VERSION, "type": kind, "record": record.to_dict()},
sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
data = line.encode("utf-8")
start = os.fstat(descriptor).st_size
try:
view = memoryview(data)
while view:
view = view[os.write(descriptor, view):]
os.fsync(descriptor)
if not self._directory_synced:
# The file's own fsync does not make its directory entry
# durable. Sync it before the first claim returns, still under
# the lock, so no writer can invoke a backend against a log a
# crash could lose.
_fsync_directory(self.path.parent)
self._directory_synced = True
except OSError:
# Unacknowledged: take the record back so the file ends on a
# record boundary and no writer later merges it as durable.
try:
os.ftruncate(descriptor, start)
os.fsync(descriptor)
except OSError:
pass
raise
self._offset = start + len(data)
def _append(self, kind: str, build: Callable[[int, "EffectLog"], Any], *, required: bool):
"""Allocate, validate and persist one record; ``None`` if it no longer applies.
``build(sequence, view)`` may return ``None`` when its precondition no
longer holds against the merged view (``self``).
"""
with self._lock:
durable = self.path is not None and (kind != "observation" or self._view["claim"]
or self.path.exists())
if not durable:
return self._append_volatile(kind, build, required=required)
try:
with self._locked():
record = build(self._max_sequence + 1, self)
if record is None:
return None
if not (self._view_index.accepts(kind, record) and self._durable_index.accepts(kind, record)):
# Another writer already settled it, or it collides.
if required:
raise ValueError("Effect record conflicts with the durable history")
return None
try:
self._write(kind, record)
except OSError:
if required:
raise
self.degraded = True
self._add_volatile(kind, record)
return record
self._add_durable(kind, record)
return record
except (OSError, ValueError) as error:
if required:
raise EffectPersistenceError("Effect claim could not be persisted durably") from error
self.degraded = True
# The lock or tail could not be taken: keep this process
# truthful without touching the file.
return self._append_volatile(kind, build, required=False)
def _append_volatile(self, kind: str, build, *, required: bool):
record = build(self._max_sequence + 1, self)
if record is None:
return None
if not self._view_index.accepts(kind, record):
if required:
raise ValueError("Effect record conflicts with the history")
return None
self._add_volatile(kind, record)
return record
# -- records -----------------------------------------------------------
def claim(self, **fields: Any) -> EffectClaim:
"""Persist a claim before invocation; raises if it is not durable."""
run_id = fields.pop("run_id", self.run_id)
return self._append("claim", lambda seq, _h: EffectClaim(sequence=seq, run_id=run_id, **fields),
required=True)
def outcome(self, **fields: Any) -> EffectOutcome | None:
"""Append an outcome; ``None`` if the effect was already settled."""
return self._append("outcome", lambda seq, _h: EffectOutcome(sequence=seq, **fields), required=False)
def observe(self, **fields: Any) -> Observation | None:
return self._append("observation", lambda seq, _h: Observation(sequence=seq, **fields), required=False)
def refresh(self) -> None:
"""Merge records other writers appended (shared lock, no repair)."""
if self.path is None:
return
with self._lock:
try:
descriptor = os.open(self.path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
| getattr(os, "O_CLOEXEC", 0))
except FileNotFoundError:
return
except OSError:
self.degraded = True
return
try:
if fcntl is not None:
fcntl.flock(descriptor, fcntl.LOCK_SH)
self._read_tail(descriptor, repair=False)
except OSError:
self.degraded = True
finally:
os.close(descriptor)
def history(self) -> EffectHistory:
self.refresh()
with self._lock:
return self._merged_history()
def assessments(self) -> tuple[EffectAssessment, ...]:
return assess_all(self.history())
# -- replay ------------------------------------------------------------
@classmethod
def load(cls, run_id: str, *, directory: str | os.PathLike | None = None) -> "EffectLog":
"""Reload a persisted log. A malformed record fails closed.
A torn final line (no newline) is the only tolerated damage: it was a
write interrupted by a crash, so its claim never returned to a caller
and no backend invocation followed it.
"""
log = cls(run_id, directory=directory)
assert log.path is not None
try:
descriptor = os.open(log.path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0))
except FileNotFoundError:
return log
except OSError as error:
raise EffectPersistenceError("Effect log is unreadable") from error
try:
if fcntl is not None:
fcntl.flock(descriptor, fcntl.LOCK_SH)
info = os.fstat(descriptor)
if info.st_nlink != 1 or not stat.S_ISREG(info.st_mode):
raise EffectPersistenceError("Effect log is aliased")
with os.fdopen(descriptor, "rb") as stream:
descriptor = None
raw = stream.read()
except OSError as error:
raise EffectPersistenceError("Effect log is unreadable") from error
finally:
if descriptor is not None:
os.close(descriptor)
complete = raw[:raw.rfind(b"\n") + 1] # drop a torn final write
for line in complete.splitlines():
try:
kind, record = _parse(line)
except (ValueError, TypeError, KeyError, UnicodeDecodeError) as error:
raise EffectPersistenceError("Effect log is corrupt") from error
if not log._durable_index.accepts(kind, record):
raise EffectPersistenceError("Effect log history is inconsistent")
log._durable[kind].append(record)
log._durable_index.add(kind, record)
log._max_sequence = max(log._max_sequence, record.sequence)
try:
log._rebuild_view()
log._merged_history()
except ValueError as error:
raise EffectPersistenceError("Effect log history is inconsistent") from error
log._offset = len(complete)
return log
@classmethod
def open(cls, run_id: str, *, directory: str | os.PathLike | None = None) -> "EffectLog":
"""The live log for a run, or its replayed durable history.
A log that is not live belongs to a finished or crashed run, so its
unsettled claims are recovered as interrupted before any append.
"""
base = Path(directory) if directory is not None else effects_dir()
with cls._OPEN_LOCK:
live = cls._LIVE.get((str(base), run_id))
if live is not None:
return live
log = cls.load(run_id, directory=base)
log.recover_interrupted()
return log
# -- background launch lineage ------------------------------------------
def index_launch(self, generation: str, effect_id: str) -> None:
"""Durably map an exact Wave 3 launch generation to its claim."""
if self.path is None:
return
if not _RUN_ID.fullmatch(generation or ""):
raise ValueError("Malformed launch generation")
target = self.path.parent / f"launch-{generation}.json"
temporary = target.with_suffix(".tmp")
data = json.dumps({"run_id": self.run_id, "effect_id": effect_id}, sort_keys=True).encode()
_ensure_directory(target.parent)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
descriptor = os.open(temporary, flags, 0o600)
try:
view = memoryview(data)
while view:
view = view[os.write(descriptor, view):]
os.fsync(descriptor)
finally:
os.close(descriptor)
os.replace(temporary, target)
_fsync_directory(target.parent)
@staticmethod
def launch_owner(generation: str, *, directory: str | os.PathLike | None = None) -> tuple[str, str] | None:
if not _RUN_ID.fullmatch(generation or ""):
return None
base = Path(directory) if directory is not None else effects_dir()
try:
descriptor = os.open(base / f"launch-{generation}.json", os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
with os.fdopen(descriptor, "rb") as stream:
if os.fstat(stream.fileno()).st_nlink != 1:
return None
value = json.loads(stream.read(4096))
except (OSError, ValueError):
return None
if (not isinstance(value, dict) or set(value) != {"run_id", "effect_id"}
or not isinstance(value["run_id"], str) or not _RUN_ID.fullmatch(value["run_id"])
or not isinstance(value["effect_id"], str)):
return None
return value["run_id"], value["effect_id"]
def recover_interrupted(self) -> tuple[EffectOutcome, ...]:
"""Append INTERRUPTED outcomes for claims that never settled.
Each claim is rechecked against the merged history under the lock, so
a claim another writer settled (or marked running) meanwhile is left
alone.
"""
with self._lock:
appended = []
for pending in replay_interrupted(self.history(), self._max_sequence + 1):
def build(seq: int, log: "EffectLog", o: EffectOutcome = pending) -> EffectOutcome | None:
if o.effect_id in log._view_index.last or o.effect_id in log._durable_index.last:
return None
return EffectOutcome(o.effect_id, seq, o.execution, o.impact, replayed=True)
record = self._append("outcome", build, required=False)
if record is not None:
appended.append(record)
return tuple(appended)
+823
View File
@@ -0,0 +1,823 @@
"""Wave 4 effect claims, outcomes, observations and verification.
This module consumes exact Wave 3 resource identities. It never resolves a
selector, discovers an alias, grants an operation or performs I/O. A
``ResourceRef`` can only be built from an already-admitted typed Wave 3 resource
object; names, paths, PIDs, URLs, labels and dictionaries are not accepted.
Facts are kept separate:
* a claim records intent and scope before backend invocation, not dispatch;
* an outcome records what the executor reported, not the resulting state;
* an observation records state seen through an admitted mechanism;
* verification is derived from fresh, relevant, complete observations made
after the effect settled, and never from receipts or acknowledgements.
History is append-only. Invalidation and freshness are computed from the
ordered record history; earlier records are never rewritten. Refresh is a new
observation. Unknown scope is conservative, never "no impact".
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from pathlib import PurePosixPath
import hashlib
import json
import re
from typing import Any, Iterable, Mapping
def _sha(value: Any) -> str:
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":"),
ensure_ascii=False, default=str).encode()).hexdigest()
def _text(value: Any, label: str, *, optional: bool = False) -> None:
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 effect {label}")
def _position(value: Any) -> None:
if type(value) is not int or value < 0:
raise ValueError("Effect history position must be a nonnegative integer")
_SHA256 = re.compile(r"[a-f0-9]{64}")
# ---------------------------------------------------------------------------
# Exact resource references (Wave 3 consumption only)
# ---------------------------------------------------------------------------
class ResourceKind(str, Enum):
FILESYSTEM = "filesystem"
PROCESS = "process"
PROCESS_LAUNCH = "process_launch"
BACKGROUND_JOB = "background_job"
OWNED = "owned"
EXTERNAL = "external"
BROWSER_SESSION = "browser_session"
@dataclass(frozen=True)
class ResourceRef:
"""Historical reference to one exact admitted Wave 3 resource.
``location`` identifies where the resource lives (including the identity of
its sealed root/namespace); ``incarnation`` identifies the object observed
there when the reference was taken. Replacement keeps the location and
changes the incarnation, so evidence never transfers to a replacement.
``snapshot_sha256`` digests the full Wave 3 snapshot for audit. A ref is not
authority: it is not accepted by any dispatcher, resolver or grant.
"""
kind: ResourceKind
role: str
location: tuple[str, ...]
incarnation: str
snapshot_sha256: str
def __post_init__(self) -> None:
if not isinstance(self.kind, ResourceKind):
raise ValueError("Unsupported effect resource kind")
_text(self.role, "resource role")
_text(self.incarnation, "resource incarnation", optional=True)
if (not isinstance(self.location, tuple) or len(self.location) < 2
or any(not isinstance(part, str) or any(c in part for c in ("\0", "\n", "\r"))
for part in self.location)
or self.location[0] != self.kind.value):
raise ValueError("Malformed effect resource location")
if not _SHA256.fullmatch(self.snapshot_sha256 or ""):
raise ValueError("Malformed effect resource snapshot digest")
@property
def location_key(self) -> str:
return _sha(list(self.location))
def same_location(self, other: "ResourceRef") -> bool:
return self.kind is other.kind and self.location == other.location
def overlaps(self, other: "ResourceRef") -> bool:
"""Conservative relevance between two exact references.
Filesystem relevance is ancestor-or-self within one sealed root
identity: a mutation of ``d/x`` invalidates a listing of ``d`` and a
replacement of ``d`` invalidates observations of ``d/x``. Other kinds
only overlap at the same exact location. No alias discovery is done.
"""
if self.kind is not other.kind:
return False
if self.kind is ResourceKind.OWNED:
# A collection binding ("*") covers every record it can create,
# list or change; specific records only overlap themselves.
return self.location[:-1] == other.location[:-1] and (
self.location[-1] == other.location[-1] or "*" in (self.location[-1], other.location[-1]))
if self.kind is not ResourceKind.FILESYSTEM:
return self.location == other.location
if self.location[:-1] != other.location[:-1]:
return False
left, right = PurePosixPath(self.location[-1]), PurePosixPath(other.location[-1])
return left == right or left.is_relative_to(right) or right.is_relative_to(left)
def to_dict(self) -> dict[str, Any]:
return {"kind": self.kind.value, "role": self.role, "location": list(self.location),
"incarnation": self.incarnation, "snapshot_sha256": self.snapshot_sha256}
@classmethod
def from_dict(cls, value: Any) -> "ResourceRef":
"""Reload a persisted historical reference. This creates no authority."""
if (not isinstance(value, dict)
or set(value) != {"kind", "role", "location", "incarnation", "snapshot_sha256"}
or not isinstance(value["location"], list)):
raise ValueError("Malformed persisted effect resource reference")
return cls(ResourceKind(value["kind"]), value["role"], tuple(value["location"]),
value["incarnation"], value["snapshot_sha256"])
def resource_ref(resource: Any, role: str) -> ResourceRef:
"""Reference an exact typed Wave 3 resource; anything else is refused.
Browser page/document resources are refused: Wave 3 fails closed for page
authority and Wave 4 must not promote page observations into identity.
"""
from src.agent_runtime import resources as wave3
if isinstance(resource, wave3.BrowserPageResource):
raise TypeError("Browser page resources are not effect-bindable")
if isinstance(resource, wave3.FilesystemResource):
root = resource.root
location = ("filesystem", root.scope.value, root.owner, root.path,
str(root.identity.device), str(root.identity.inode), resource.path)
chain = [[a.path, a.identity.device, a.identity.inode] for a in resource.ancestors]
identity = resource.identity
incarnation = ("absent:" + _sha(chain) if identity is None else
f"{identity.kind}:{identity.device}:{identity.inode}:" + _sha(chain))
return ResourceRef(ResourceKind.FILESYSTEM, role, location, incarnation, _sha(resource.to_dict()))
if isinstance(resource, wave3.ProcessResource):
ident = resource.identity
location = ("process", resource.namespace, resource.owner, resource.request_id, resource.thread_id,
str(ident.pid), ident.start_token, resource.role)
return ResourceRef(ResourceKind.PROCESS, role, location, ident.start_token, _sha(resource.to_dict()))
if isinstance(resource, wave3.ProcessLaunchResource):
# The reservation generation is the exact launch -> job linkage that
# Wave 3 validates in ``job_from_record``.
location = ("process_launch", resource.namespace, resource.owner, resource.request_id,
resource.thread_id, resource.generation)
return ResourceRef(ResourceKind.PROCESS_LAUNCH, role, location, resource.generation,
_sha(resource.to_dict()))
if isinstance(resource, wave3.BackgroundJobResource):
location = ("background_job", resource.namespace, resource.owner, resource.request_id,
resource.thread_id, resource.job_id, resource.generation)
return ResourceRef(ResourceKind.BACKGROUND_JOB, role, location, resource.generation,
_sha(resource.to_dict()))
if isinstance(resource, wave3.OwnedResource):
location = ("owned", resource.namespace, resource.owner, resource.thread_id,
resource.collection, resource.record_id)
return ResourceRef(ResourceKind.OWNED, role, location, resource.revision, _sha(resource.to_dict()))
if isinstance(resource, wave3.ExternalResource):
location = ("external", resource.namespace, resource.owner, resource.endpoint_id,
resource.server_id, resource.tool_id)
return ResourceRef(ResourceKind.EXTERNAL, role, location, resource.incarnation, _sha(resource.to_dict()))
if isinstance(resource, wave3.BrowserSessionResource):
observation = resource.observation
location = ("browser_session", resource.owner, resource.thread_id, observation.session_key)
return ResourceRef(ResourceKind.BROWSER_SESSION, role, location, observation.session_incarnation,
_sha(resource.to_dict()))
raise TypeError("Effect scope requires an exact Wave 3 resource identity")
def bound_filesystem_refs(bound: Any) -> tuple[ResourceRef, ...]:
"""References for an admitted ``BoundFilesystemOperation``'s exact bindings."""
from src.agent_runtime.resource_binding import BoundFilesystemOperation
if not isinstance(bound, BoundFilesystemOperation):
raise TypeError("Filesystem effect scope requires a server-owned bound operation")
return tuple(resource_ref(binding.resource, binding.role) for binding in bound.bindings)
# ---------------------------------------------------------------------------
# Claims
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class OperationRef:
"""Final normalized operation reference; not a second normalization API."""
tool: str
action: str
input_sha256: str
request_id: str = ""
def __post_init__(self) -> None:
_text(self.tool, "operation tool")
_text(self.action, "operation action", optional=True)
_text(self.request_id, "operation request", optional=True)
if not _SHA256.fullmatch(self.input_sha256 or ""):
raise ValueError("Malformed operation input digest")
@classmethod
def from_exact(cls, operation: Any, execution_input: str | None = None, request_id: str = "") -> "OperationRef":
from src.agent_runtime.authority import ExactOperation
if not isinstance(operation, ExactOperation):
raise TypeError("Effect claims require the admitted exact operation")
body = operation.input if execution_input is None else execution_input
return cls(str(operation.tool), str(operation.action or ""), _sha(body), request_id or "")
def to_dict(self) -> dict[str, Any]:
return {"tool": self.tool, "action": self.action, "input_sha256": self.input_sha256,
"request_id": self.request_id}
@classmethod
def from_dict(cls, value: Any) -> "OperationRef":
if not isinstance(value, dict) or set(value) != {"tool", "action", "input_sha256", "request_id"}:
raise ValueError("Malformed persisted operation reference")
return cls(**value)
class Predicate(str, Enum):
EXISTS = "exists"
ABSENT = "absent"
CONTENT_SHA256 = "content_sha256"
# The observed content digest differs from ``expected`` (the pre-state).
CONTENT_CHANGED = "content_changed"
_PREDICATE_KINDS = {
Predicate.EXISTS: {ResourceKind.FILESYSTEM, ResourceKind.OWNED, ResourceKind.EXTERNAL},
Predicate.ABSENT: {ResourceKind.FILESYSTEM, ResourceKind.OWNED, ResourceKind.EXTERNAL},
Predicate.CONTENT_SHA256: {ResourceKind.FILESYSTEM, ResourceKind.OWNED, ResourceKind.EXTERNAL},
Predicate.CONTENT_CHANGED: {ResourceKind.FILESYSTEM, ResourceKind.OWNED, ResourceKind.EXTERNAL},
}
@dataclass(frozen=True)
class Postcondition:
"""An explicit requested post-state predicate on one exact claimed target."""
target: ResourceRef
predicate: Predicate
expected: str = ""
def __post_init__(self) -> None:
if not isinstance(self.target, ResourceRef) or not isinstance(self.predicate, Predicate):
raise ValueError("Malformed postcondition")
if self.target.kind not in _PREDICATE_KINDS[self.predicate]:
raise ValueError("Predicate is not supported for this resource kind")
needs_digest = self.predicate in {Predicate.CONTENT_SHA256, Predicate.CONTENT_CHANGED}
if needs_digest != bool(_SHA256.fullmatch(self.expected or "")) or (not needs_digest and self.expected):
raise ValueError("Malformed postcondition expectation")
def to_dict(self) -> dict[str, Any]:
return {"target": self.target.to_dict(), "predicate": self.predicate.value, "expected": self.expected}
@classmethod
def from_dict(cls, value: Any) -> "Postcondition":
if not isinstance(value, dict) or set(value) != {"target", "predicate", "expected"}:
raise ValueError("Malformed persisted postcondition")
return cls(ResourceRef.from_dict(value["target"]), Predicate(value["predicate"]), value["expected"])
@dataclass(frozen=True)
class EffectClaim:
"""Server-owned claim, persisted before backend invocation.
The claim states intent and scope; it is not evidence that dispatch, the
backend operation, or any mutation happened. ``impact_scope`` holds the
exact admitted bindings the operation may change; empty means unknown
scope, never no impact. ``dependencies`` are resources the predicate
relies on without being mutation targets.
"""
effect_id: str
run_id: str
action_id: str
sequence: int
operation: OperationRef
impact_scope: tuple[ResourceRef, ...] = ()
dependencies: tuple[ResourceRef, ...] = ()
obligations: tuple[Postcondition, ...] = ()
parent_run_id: str = ""
external: bool = False
def __post_init__(self) -> None:
for name in ("effect_id", "run_id", "action_id"):
_text(getattr(self, name), name)
_text(self.parent_run_id, "parent run", optional=True)
_position(self.sequence)
if not isinstance(self.operation, OperationRef) or type(self.external) is not bool:
raise ValueError("Malformed effect claim")
for name in ("impact_scope", "dependencies"):
refs = getattr(self, name)
if not isinstance(refs, tuple) or any(not isinstance(r, ResourceRef) for r in refs):
raise ValueError("Effect scope must be exact resource references")
if (not isinstance(self.obligations, tuple)
or any(not isinstance(o, Postcondition) for o in self.obligations)):
raise ValueError("Malformed effect obligations")
for obligation in self.obligations:
if not any(obligation.target == ref for ref in self.impact_scope):
raise ValueError("Postcondition target must be a claimed impact binding")
@property
def unknown_scope(self) -> bool:
return not self.impact_scope
def to_dict(self) -> dict[str, Any]:
return {"effect_id": self.effect_id, "run_id": self.run_id, "action_id": self.action_id,
"sequence": self.sequence, "operation": self.operation.to_dict(),
"impact_scope": [r.to_dict() for r in self.impact_scope],
"dependencies": [r.to_dict() for r in self.dependencies],
"obligations": [o.to_dict() for o in self.obligations],
"parent_run_id": self.parent_run_id, "external": self.external}
@classmethod
def from_dict(cls, value: Any) -> "EffectClaim":
keys = {"effect_id", "run_id", "action_id", "sequence", "operation", "impact_scope",
"dependencies", "obligations", "parent_run_id", "external"}
if not isinstance(value, dict) or set(value) != keys or any(
not isinstance(value[k], list) for k in ("impact_scope", "dependencies", "obligations")):
raise ValueError("Malformed persisted effect claim")
return cls(value["effect_id"], value["run_id"], value["action_id"], value["sequence"],
OperationRef.from_dict(value["operation"]),
tuple(ResourceRef.from_dict(r) for r in value["impact_scope"]),
tuple(ResourceRef.from_dict(r) for r in value["dependencies"]),
tuple(Postcondition.from_dict(o) for o in value["obligations"]),
value["parent_run_id"], value["external"])
# ---------------------------------------------------------------------------
# Outcomes
# ---------------------------------------------------------------------------
class ExecutionOutcome(str, Enum):
NOT_EXECUTED = "not_executed" # refused before backend invocation
ATTEMPTED = "attempted" # claimed; no settled outcome yet
REPORTED_SUCCESS = "reported_success" # executor reported success; not post-state
FAILED = "failed"
TIMED_OUT = "timed_out"
CANCELLED = "cancelled"
RUNNING = "running" # admitted/background; not completed work
INTERRUPTED = "interrupted" # unknown: lost, crashed or replayed
class Impact(str, Enum):
NONE = "none" # known no-op: the backend was never invoked
POSSIBLE = "possible" # may have changed state, including partially
CHANGED = "changed" # a trusted before/after capture differs
class CleanupState(str, Enum):
NOT_APPLICABLE = "not_applicable"
VERIFIED = "verified"
FAILED = "failed"
UNKNOWN = "unknown"
_SETTLED = {ExecutionOutcome.NOT_EXECUTED, ExecutionOutcome.REPORTED_SUCCESS, ExecutionOutcome.FAILED,
ExecutionOutcome.TIMED_OUT, ExecutionOutcome.CANCELLED, ExecutionOutcome.INTERRUPTED}
@dataclass(frozen=True)
class ProducerFacts:
"""Bounded typed producer facts; arbitrary returned data is never kept.
These are execution/lifecycle facts reported by a server producer. None of
them is a post-state observation.
"""
exit_code: int | None = None
timed_out: bool = False
output_truncated: bool = False
failure_kind: str = ""
job_state: str = ""
remote_acknowledged: bool = False
external: bool = False
# The producer reached its mutation stage before reporting failure.
mutation_attempted: bool = False
def __post_init__(self) -> None:
if self.exit_code is not None and type(self.exit_code) is not int:
raise ValueError("Malformed producer exit code")
for name in ("timed_out", "output_truncated", "remote_acknowledged", "external", "mutation_attempted"):
if type(getattr(self, name)) is not bool:
raise ValueError("Malformed producer flag")
for name in ("failure_kind", "job_state"):
value = getattr(self, name)
_text(value, name, optional=True)
if len(value) > 64 or (value and not re.fullmatch(r"[a-z0-9_.:-]+", value)):
raise ValueError("Malformed producer label")
def to_dict(self) -> dict[str, Any]:
return {"exit_code": self.exit_code, "timed_out": self.timed_out,
"output_truncated": self.output_truncated, "failure_kind": self.failure_kind,
"job_state": self.job_state, "remote_acknowledged": self.remote_acknowledged,
"external": self.external, "mutation_attempted": self.mutation_attempted}
@classmethod
def from_dict(cls, value: Any) -> "ProducerFacts":
if not isinstance(value, dict) or set(value) != set(cls.__dataclass_fields__):
raise ValueError("Malformed persisted producer facts")
return cls(**value)
def _label(value: Any) -> str:
text = value.strip().lower() if isinstance(value, str) else ""
return text if len(text) <= 64 and re.fullmatch(r"[a-z0-9_.:-]+", text) else ""
def producer_facts(result: Any) -> ProducerFacts:
"""Project a dispatcher result into typed facts without trusting its shape.
Only exact scalar types are copied. Anything else becomes the default, so a
forged or malformed dictionary can only lose information, not add trust.
"""
if not isinstance(result, Mapping):
return ProducerFacts()
code = result.get("exit_code")
containment = result.get("containment")
external = isinstance(containment, Mapping) and containment.get("external") is True
job = result.get("status") if isinstance(result.get("job_id"), str) else ""
return ProducerFacts(
exit_code=code if type(code) is int else None,
timed_out=result.get("timed_out") is True or _label(result.get("failure_kind")) == "timeout",
output_truncated=result.get("output_truncated") is True or result.get("truncated") is True,
failure_kind=_label(result.get("failure_kind")),
job_state=_label(job),
external=external,
mutation_attempted=result.get("mutation_attempted") is True,
)
@dataclass(frozen=True)
class EffectOutcome:
"""Append-only execution outcome for one claim.
``impact`` must not claim no change for anything that reached a backend.
``cleanup`` is recorded separately: cleanup success is not business-effect
success and cleanup failure does not erase an achieved effect.
"""
effect_id: str
sequence: int
execution: ExecutionOutcome
impact: Impact
facts: ProducerFacts = ProducerFacts()
cleanup: CleanupState = CleanupState.NOT_APPLICABLE
execution_id: str = ""
replayed: bool = False
def __post_init__(self) -> None:
_text(self.effect_id, "effect identifier")
_text(self.execution_id, "execution identifier", optional=True)
_position(self.sequence)
if (not isinstance(self.execution, ExecutionOutcome) or not isinstance(self.impact, Impact)
or not isinstance(self.facts, ProducerFacts) or not isinstance(self.cleanup, CleanupState)
or type(self.replayed) is not bool):
raise ValueError("Malformed effect outcome")
if self.execution is ExecutionOutcome.ATTEMPTED:
raise ValueError("ATTEMPTED is derived from a claim without an outcome")
if (self.impact is Impact.NONE) != (self.execution is ExecutionOutcome.NOT_EXECUTED):
raise ValueError("Only a refused, never-invoked operation is a known no-op")
if self.execution is ExecutionOutcome.NOT_EXECUTED and self.execution_id:
raise ValueError("A refused operation has no execution identity")
def to_dict(self) -> dict[str, Any]:
return {"effect_id": self.effect_id, "sequence": self.sequence, "execution": self.execution.value,
"impact": self.impact.value, "facts": self.facts.to_dict(), "cleanup": self.cleanup.value,
"execution_id": self.execution_id, "replayed": self.replayed}
@classmethod
def from_dict(cls, value: Any) -> "EffectOutcome":
if not isinstance(value, dict) or set(value) != set(cls.__dataclass_fields__):
raise ValueError("Malformed persisted effect outcome")
return cls(value["effect_id"], value["sequence"], ExecutionOutcome(value["execution"]),
Impact(value["impact"]), ProducerFacts.from_dict(value["facts"]),
CleanupState(value["cleanup"]), value["execution_id"], value["replayed"])
# ---------------------------------------------------------------------------
# Observations
# ---------------------------------------------------------------------------
class ObservationMechanism(str, Enum):
FILESYSTEM_READ = "filesystem_read" # admitted read of the exact binding
OWNED_RECORD_READ = "owned_record_read" # admitted owner-scoped readback
REMOTE_READBACK = "remote_readback" # admitted independent remote query
PROCESS_OWNERSHIP = "process_ownership" # lifecycle owner's verdict
JOB_STATE = "job_state" # background job record transition
BROWSER_SESSION = "browser_session" # session lifecycle metadata only
# The following are never post-state verification.
EXECUTION_RECEIPT = "execution_receipt"
REMOTE_ACKNOWLEDGEMENT = "remote_acknowledgement"
class Coverage(str, Enum):
COMPLETE = "complete"
PARTIAL = "partial"
# Mechanisms able to decide a postcondition for each resource kind. Process,
# job and browser-session observations are lifecycle facts: they can make
# earlier evidence stale but cannot verify a file/record/remote predicate.
_VERIFYING = {
ResourceKind.FILESYSTEM: {ObservationMechanism.FILESYSTEM_READ},
ResourceKind.OWNED: {ObservationMechanism.OWNED_RECORD_READ},
ResourceKind.EXTERNAL: {ObservationMechanism.REMOTE_READBACK},
}
_ADMITTED_READS = {ObservationMechanism.FILESYSTEM_READ, ObservationMechanism.OWNED_RECORD_READ,
ObservationMechanism.REMOTE_READBACK}
@dataclass(frozen=True)
class Observation:
"""State seen through one mechanism for one exact resource.
``exists``/``content_sha256`` are what the mechanism saw; ``None``/empty
means not observed. A PARTIAL observation (offset/limit/truncated read,
listing, existence-only probe) never decides a whole-content predicate.
Admitted reads must name the journal action that performed them.
"""
observation_id: str
sequence: int
resource: ResourceRef
mechanism: ObservationMechanism
coverage: Coverage
source_action_id: str = ""
source_execution_id: str = ""
exists: bool | None = None
content_sha256: str = ""
evidence_event_id: str = ""
def __post_init__(self) -> None:
_text(self.observation_id, "observation identifier")
for name in ("source_action_id", "source_execution_id", "evidence_event_id"):
_text(getattr(self, name), name, optional=True)
_position(self.sequence)
if (not isinstance(self.resource, ResourceRef) or not isinstance(self.mechanism, ObservationMechanism)
or not isinstance(self.coverage, Coverage)
or (self.exists is not None and type(self.exists) is not bool)):
raise ValueError("Malformed observation")
if self.content_sha256 and (not _SHA256.fullmatch(self.content_sha256) or self.exists is not True):
raise ValueError("Malformed observed content digest")
if self.mechanism in _ADMITTED_READS and not self.source_action_id:
raise ValueError("Readback observations require the admitted action that performed them")
def to_dict(self) -> dict[str, Any]:
return {"observation_id": self.observation_id, "sequence": self.sequence,
"resource": self.resource.to_dict(), "mechanism": self.mechanism.value,
"coverage": self.coverage.value, "source_action_id": self.source_action_id,
"source_execution_id": self.source_execution_id, "exists": self.exists,
"content_sha256": self.content_sha256, "evidence_event_id": self.evidence_event_id}
@classmethod
def from_dict(cls, value: Any) -> "Observation":
if not isinstance(value, dict) or set(value) != set(cls.__dataclass_fields__):
raise ValueError("Malformed persisted observation")
return cls(**{**value, "resource": ResourceRef.from_dict(value["resource"]),
"mechanism": ObservationMechanism(value["mechanism"]),
"coverage": Coverage(value["coverage"])})
def predicate_holds(postcondition: Postcondition, observation: Observation) -> bool | None:
"""Decide one predicate from one observation; ``None`` means undecidable.
The check is performed here from the observed state, so no adapter can
attest verification by labelling an unrelated read.
"""
target = postcondition.target
if (not observation.resource.same_location(target)
or observation.mechanism not in _VERIFYING.get(target.kind, set())):
return None
predicate = postcondition.predicate
if predicate is Predicate.ABSENT:
return None if observation.exists is None else not observation.exists
if predicate is Predicate.EXISTS:
return observation.exists
if observation.exists is False:
return False
if observation.coverage is not Coverage.COMPLETE or not observation.content_sha256:
return None
if predicate is Predicate.CONTENT_SHA256:
return observation.content_sha256 == postcondition.expected
return observation.content_sha256 != postcondition.expected
# ---------------------------------------------------------------------------
# History, invalidation and freshness
# ---------------------------------------------------------------------------
class Freshness(str, Enum):
FRESH = "fresh"
STALE = "stale" # a later possible mutation or replacement overlaps
UNSETTLED = "unsettled" # an overlapping effect was still in flight
@dataclass(frozen=True)
class EffectHistory:
"""An immutable, totally ordered view of one effect log.
Sequences are unique positions in one log. Duplicate positions are rejected
rather than ordered arbitrarily.
"""
claims: tuple[EffectClaim, ...] = ()
outcomes: tuple[EffectOutcome, ...] = ()
observations: tuple[Observation, ...] = ()
def __post_init__(self) -> None:
positions = [r.sequence for r in (*self.claims, *self.outcomes, *self.observations)]
if len(positions) != len(set(positions)):
raise ValueError("Effect history positions must be unique")
ids = [c.effect_id for c in self.claims]
if len(ids) != len(set(ids)):
raise ValueError("Effect claims must have unique identifiers")
claim_at = {c.effect_id: c.sequence for c in self.claims}
settled: set[str] = set()
for outcome in sorted(self.outcomes, key=lambda o: o.sequence):
if outcome.effect_id not in claim_at or outcome.sequence <= claim_at[outcome.effect_id]:
raise ValueError("Outcome must follow its claim in one history")
# A RUNNING effect may later settle (background continuation or
# replay interruption); a settled outcome is never replaced.
if outcome.effect_id in settled:
raise ValueError("A settled effect outcome cannot be replaced")
if outcome.execution is not ExecutionOutcome.RUNNING:
settled.add(outcome.effect_id)
# Derived indexes (not fields): outcomes per effect in sequence order.
by_effect: dict[str, list[EffectOutcome]] = {}
for outcome in sorted(self.outcomes, key=lambda o: o.sequence):
by_effect.setdefault(outcome.effect_id, []).append(outcome)
object.__setattr__(self, "_outcomes_by_effect", by_effect)
object.__setattr__(self, "_claims_by_id", {c.effect_id: c for c in self.claims})
def claim(self, effect_id: str) -> EffectClaim | None:
return self._claims_by_id.get(effect_id)
def latest_outcome(self, effect_id: str, before: int | None = None) -> EffectOutcome | None:
for outcome in reversed(self._outcomes_by_effect.get(effect_id, ())):
if before is None or outcome.sequence < before:
return outcome
return None
def execution(self, effect_id: str, before: int | None = None) -> ExecutionOutcome:
outcome = self.latest_outcome(effect_id, before)
return ExecutionOutcome.ATTEMPTED if outcome is None else outcome.execution
def _claim_touches(claim: EffectClaim, resource: ResourceRef) -> bool:
return claim.unknown_scope or any(ref.overlaps(resource) for ref in claim.impact_scope)
def invalidated_by(observation: Observation, history: EffectHistory) -> tuple[str, ...]:
"""Identifiers of later records that make ``observation`` stale.
Any later claim that may touch the resource invalidates it once the claim
exists (it may already be executing), unless it settled as a known no-op.
A later observation of the same location with a different incarnation
reveals replacement. Execution receipts are never invalidated: they remain
historical execution facts.
"""
if observation.mechanism in {ObservationMechanism.EXECUTION_RECEIPT,
ObservationMechanism.REMOTE_ACKNOWLEDGEMENT}:
return ()
reasons: list[str] = []
for claim in history.claims:
if claim.sequence <= observation.sequence or not _claim_touches(claim, observation.resource):
continue
outcome = history.latest_outcome(claim.effect_id)
if outcome is not None and outcome.impact is Impact.NONE:
continue
reasons.append(claim.effect_id)
for later in history.observations:
if (later.sequence > observation.sequence and later.resource.same_location(observation.resource)
and later.resource.incarnation != observation.resource.incarnation):
reasons.append(later.observation_id)
return tuple(dict.fromkeys(reasons))
def freshness(observation: Observation, history: EffectHistory) -> Freshness:
if invalidated_by(observation, history):
return Freshness.STALE
for claim in history.claims:
if claim.sequence < observation.sequence and _claim_touches(claim, observation.resource):
state = history.execution(claim.effect_id, before=observation.sequence)
if state in {ExecutionOutcome.ATTEMPTED, ExecutionOutcome.RUNNING}:
return Freshness.UNSETTLED
return Freshness.FRESH
# ---------------------------------------------------------------------------
# Verification
# ---------------------------------------------------------------------------
class EffectVerdict(str, Enum):
NOT_EXECUTED = "not_executed"
PENDING = "pending" # attempted/running; not settled
VERIFIED = "verified" # reported success + fresh matching post-state
STATE_OBSERVED = "state_observed" # matching post-state; causality unknown
UNVERIFIED = "unverified" # no adequate fresh evidence
CONTRADICTED = "contradicted" # latest fresh check shows the predicate false
FAILED = "failed" # execution failed; never effect success
_VERDICT_RANK = {EffectVerdict.FAILED: 0, EffectVerdict.CONTRADICTED: 1, EffectVerdict.PENDING: 2,
EffectVerdict.UNVERIFIED: 3, EffectVerdict.NOT_EXECUTED: 4,
EffectVerdict.STATE_OBSERVED: 5, EffectVerdict.VERIFIED: 6}
@dataclass(frozen=True)
class EffectAssessment:
effect_id: str
action_id: str
execution: ExecutionOutcome
impact: Impact | None
verdict: EffectVerdict
reason: str
cleanup: CleanupState = CleanupState.NOT_APPLICABLE
observation_ids: tuple[str, ...] = ()
targets: tuple[ResourceRef, ...] = ()
@property
def unresolved_impact(self) -> bool:
"""Resources may have changed in a way no fresh evidence has settled."""
return (self.impact is not Impact.NONE
and self.execution is not ExecutionOutcome.REPORTED_SUCCESS
and self.verdict not in {EffectVerdict.STATE_OBSERVED, EffectVerdict.CONTRADICTED})
def to_dict(self) -> dict[str, Any]:
return {"effect_id": self.effect_id, "action_id": self.action_id, "execution": self.execution.value,
"impact": None if self.impact is None else self.impact.value, "verdict": self.verdict.value,
"reason": self.reason, "cleanup": self.cleanup.value,
"observation_ids": list(self.observation_ids),
"targets": [t.to_dict() for t in self.targets]}
def _assess_obligation(claim: EffectClaim, settled: EffectOutcome, obligation: Postcondition,
history: EffectHistory) -> tuple[EffectVerdict, str, str]:
candidates = [o for o in history.observations
if o.sequence > settled.sequence and o.resource.same_location(obligation.target)
and o.mechanism in _VERIFYING.get(obligation.target.kind, set())]
if not candidates:
return EffectVerdict.UNVERIFIED, "no authorized post-settlement observation of the target", ""
# The newest check wins. A newer partial or failed check never falls back
# to an earlier complete one.
latest = max(candidates, key=lambda o: o.sequence)
state = freshness(latest, history)
if state is not Freshness.FRESH:
return EffectVerdict.UNVERIFIED, f"the latest target observation is {state.value}", latest.observation_id
holds = predicate_holds(obligation, latest)
if holds is None:
return EffectVerdict.UNVERIFIED, "the latest observation does not decide the postcondition", latest.observation_id
if not holds:
return EffectVerdict.CONTRADICTED, "the latest fresh observation contradicts the postcondition", latest.observation_id
execution = settled.execution
if execution is ExecutionOutcome.FAILED:
return EffectVerdict.FAILED, "execution failed; matching state is not attributed to it", latest.observation_id
if execution is ExecutionOutcome.REPORTED_SUCCESS:
return EffectVerdict.VERIFIED, "fresh authorized observation matches the postcondition", latest.observation_id
return (EffectVerdict.STATE_OBSERVED,
"state matches, but this execution's outcome is unknown; causality is not established",
latest.observation_id)
def assess(claim: EffectClaim, history: EffectHistory) -> EffectAssessment:
"""Derive a claim's verdict from the append-only history."""
settled = history.latest_outcome(claim.effect_id)
targets = tuple(o.target for o in claim.obligations)
if settled is None:
return EffectAssessment(claim.effect_id, claim.action_id, ExecutionOutcome.ATTEMPTED, None,
EffectVerdict.PENDING, "no settled execution outcome", targets=targets)
base = dict(effect_id=claim.effect_id, action_id=claim.action_id, execution=settled.execution,
impact=settled.impact, cleanup=settled.cleanup, targets=targets)
if settled.execution is ExecutionOutcome.NOT_EXECUTED:
return EffectAssessment(**base, verdict=EffectVerdict.NOT_EXECUTED, reason="refused before invocation")
if settled.execution is ExecutionOutcome.RUNNING:
return EffectAssessment(**base, verdict=EffectVerdict.PENDING,
reason="background execution has not settled")
if not claim.obligations:
verdict = EffectVerdict.FAILED if settled.execution is ExecutionOutcome.FAILED else EffectVerdict.UNVERIFIED
return EffectAssessment(**base, verdict=verdict, reason="no explicit postcondition obligation")
results = [_assess_obligation(claim, settled, o, history) for o in claim.obligations]
worst = min(results, key=lambda r: _VERDICT_RANK[r[0]])
if settled.execution is ExecutionOutcome.FAILED and worst[0] is not EffectVerdict.CONTRADICTED:
worst = (EffectVerdict.FAILED, worst[1] if worst[0] is EffectVerdict.FAILED else
"execution failed and may have partially changed the target", worst[2])
return EffectAssessment(**base, verdict=worst[0], reason=worst[1],
observation_ids=tuple(dict.fromkeys(r[2] for r in results if r[2])))
def assess_all(history: EffectHistory) -> tuple[EffectAssessment, ...]:
return tuple(assess(claim, history) for claim in sorted(history.claims, key=lambda c: c.sequence))
def replay_interrupted(history: EffectHistory, next_sequence: int) -> tuple[EffectOutcome, ...]:
"""Outcomes to append for claims that never settled before a reload.
Unknown remains unknown: the backend may or may not have been invoked, so
impact is POSSIBLE. Running background effects are left to their own
lifecycle owner and are not converted here.
"""
_position(next_sequence)
pending = [c for c in sorted(history.claims, key=lambda c: c.sequence)
if history.latest_outcome(c.effect_id) is None]
return tuple(EffectOutcome(c.effect_id, next_sequence + i, ExecutionOutcome.INTERRUPTED,
Impact.POSSIBLE, replayed=True) for i, c in enumerate(pending))
+167
View File
@@ -0,0 +1,167 @@
"""Canonical evidence identities; these helpers never grant filesystem access."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import shlex
import stat
from .path_policy import _is_sensitive_path
TUI_PYTHON_RUNNER_SETUP = (
"runner=''; "
"if [ -x .venv/bin/python ]; then runner=.venv/bin/python; "
"elif [ -x venv/bin/python ]; then runner=venv/bin/python; "
"elif git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) "
"&& [ -x \"$(dirname \"$git_common\")/.venv/bin/python\" ]; then "
"runner=\"$(dirname \"$git_common\")/.venv/bin/python\"; "
"else runner=python; fi; "
)
def digest(value: object) -> str:
return hashlib.sha256(json.dumps(value, sort_keys=True, ensure_ascii=False,
separators=(",", ":"), default=str).encode()).hexdigest()
def artifact_version(value: str, workspace: str) -> str:
"""Content version for a confined declared output; missing/unreadable is explicit."""
identity = artifact_identity(value, workspace)
if not workspace or not identity.startswith('workspace:'):
return 'unobserved'
root = Path(workspace).resolve()
candidate = (root / identity.removeprefix('workspace:')).resolve()
if not candidate.is_relative_to(root) or _is_sensitive_path(str(candidate)):
return 'unobserved'
if not hasattr(os, 'O_NOFOLLOW') or not os.supports_dir_fd:
return 'unobserved'
directory = None
try:
directory = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
parts = candidate.relative_to(root).parts
if not parts:
return 'unobserved'
for part in parts[:-1]:
child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory)
os.close(directory)
directory = child
descriptor = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory)
with os.fdopen(descriptor, 'rb') as stream:
info = os.fstat(stream.fileno())
if not stat.S_ISREG(info.st_mode) or info.st_size > 64 * 1024 * 1024:
return 'unobserved'
result = hashlib.sha256()
remaining = 64 * 1024 * 1024
while block := stream.read(min(1024 * 1024, remaining + 1)):
remaining -= len(block)
if remaining < 0:
return 'unobserved'
result.update(block)
return result.hexdigest()
except (OSError, ValueError):
return 'missing-or-unreadable'
finally:
if directory is not None:
os.close(directory)
def artifact_identity(value: str, workspace: str = "") -> str:
"""Unify relative, virtual and host aliases without basename matching.
Resolving symlinks is evidence bookkeeping, never a confinement check. Paths
outside the workspace retain their absolute identity and cannot satisfy a
workspace obligation with the same basename.
"""
text = str(value or "").strip()
if not text:
return ""
path = PurePosixPath(text)
root = Path(workspace or "/workspace").resolve()
if path.parts[:2] == ('/', 'workspace'):
path = PurePosixPath(*path.parts[2:])
candidate = Path(str(path))
if not candidate.is_absolute():
candidate = root / candidate
try:
resolved = candidate.resolve()
relative = resolved.relative_to(root)
return "workspace:" + relative.as_posix()
except ValueError:
return "absolute:" + str(candidate.resolve())
except (OSError, RuntimeError):
return "unresolved:" + text
def executable_words(command: str) -> tuple[str, ...]:
"""Recognize one foreground command after exact interpreter or cd/set prefixes.
This is deliberately conservative evidence parsing, not shell authorization.
Other control flow, substitutions, pipelines and status-masking tails are not proof
that a verifier returned the recorded shell status.
"""
text = str(command or '').strip()
if text.startswith(TUI_PYTHON_RUNNER_SETUP):
remainder = text[len(TUI_PYTHON_RUNNER_SETUP):]
# This exact server-owned prelude only selects the interpreter. The
# trailing command must still be a single foreground invocation whose
# status is returned unchanged; the generic discovery fallback is not.
if remainder.startswith('"$runner" '):
arguments = remainder[len('"$runner" '):]
if '$' in arguments:
return ()
text = 'python ' + arguments
if any(marker in text for marker in ('`', '$(', '${', '\n', '\r')):
return ()
try:
lexer = shlex.shlex(text, posix=True, punctuation_chars=';&|<>()')
lexer.whitespace_split = True
words = list(lexer)
except ValueError:
return ()
while '&&' in words:
index = words.index('&&')
prefix = words[:index]
if not ((len(prefix) == 2 and prefix[0] == 'cd') or prefix == ['set', '-e']):
return ()
words = words[index + 1:]
if any(word and all(c in ';&|<>()' for c in word) for word in words):
return ()
while words and re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*=[^\n]*', words[0]):
words.pop(0)
return tuple(words)
def is_test_command(command: str) -> bool:
words = executable_words(command)
if not words:
return False
if any(word in {'--help', '-h', '--version', '--collect-only', '--co'} for word in words[1:]):
return False
binary = Path(words[0]).name
if binary in {'pytest', 'py.test'}:
return True
if re.fullmatch(r'python(?:\d+(?:\.\d+)?)?', binary):
args = list(words[1:])
while args and args[0] in {'-I', '-S', '-s', '-E', '-B', '-u'}:
args.pop(0)
return len(args) >= 2 and args[:2] in (['-m', 'pytest'], ['-m', 'unittest'])
if binary in {'npm', 'pnpm', 'yarn', 'make', 'cargo', 'go'}:
args = words[1:]
return bool(args and (args[0] == 'test' or binary == 'npm' and args[:2] == ('run', 'test')))
return bool(re.match(r'^/(?:tests?|verifier)/[^/]+', words[0]))
def is_validation_command(command: str) -> bool:
words = executable_words(command)
if not words:
return False
binary = Path(words[0]).name
return (is_test_command(command)
or binary in {'cat', 'head', 'tail', 'stat', 'wc', 'jq', 'cmp', 'diff',
'coqc', 'gcc', 'g++', 'clang', 'clang++', 'javac', 'rustc'}
or binary == 'test' and len(words) > 1 and words[1] in {'-e', '-f', '-s', '-d'}
or binary in {'cargo', 'go', 'npm', 'pnpm', 'yarn'} and words[1:2] in {('build',), ('check',)}
or binary == 'npm' and words[1:3] == ('run', 'build'))
+282
View File
@@ -0,0 +1,282 @@
"""Run-owned action history. Model text cannot insert authoritative receipts."""
from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
from copy import deepcopy
from dataclasses import dataclass, field, asdict
from functools import wraps
from inspect import signature
import logging
from typing import Any
from uuid import uuid4
from .identity import artifact_identity, artifact_version, digest
@dataclass
class ActionReceipt:
action_id: str
call_id: str
proposed_tool: str
proposed_arguments: str
provider_arguments: Any = None
provider_tool: str = ''
tool: str = ""
arguments: str = ""
transitions: list[dict[str, Any]] = field(default_factory=list)
execution_id: str | None = None
operation_started: bool = False
outcome: dict[str, Any] | None = None
artifact_versions: dict[str, str] = field(default_factory=dict)
artifact_changes: list[str] | None = None
def transition(self, stage: str, **details: Any) -> None:
self.transitions.append({'sequence': len(self.transitions), 'stage': stage, **details})
def normalize(self, block: Any, reason: str) -> None:
tool, arguments = str(block.tool_type), str(block.content)
if tool != self.tool or arguments != self.arguments or not any(t['stage'] == 'normalized' for t in self.transitions):
self.transition('normalized', reason=reason, tool=tool, arguments=arguments,
previous_sha256=digest((self.tool, self.arguments)))
self.tool, self.arguments = tool, arguments
def finish(self, result: dict[str, Any]) -> None:
if self.outcome is not None:
return
code = result.get('exit_code')
valid_code = isinstance(code, int) and not isinstance(code, bool)
denied = bool(result.get('blocked') or result.get('approval_required')
or str(result.get('failure_kind', '')).endswith('_denied'))
self.outcome = {
'exit_code': code if valid_code else None,
'success': valid_code and code == 0 and not result.get('error') and not denied,
'authoritative': self.execution_id is not None and valid_code and not denied,
'blocked': denied,
'output_sha256': digest(result.get('output') or result.get('error') or result.get('stdout') or ''),
}
self.transition('outcome', **self.outcome)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class ActionJournal:
run_id: str = field(default_factory=lambda: uuid4().hex)
actions: list[ActionReceipt] = field(default_factory=list)
workspace: str = ''
observed_artifacts: tuple[str, ...] = ()
parent_run_id: str | None = None
# Durable Wave 4 effect log, shared across one run lineage; None disables.
effects: Any = field(default=None, repr=False, compare=False)
_dispatches: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
def effect_entries(self) -> list[dict[str, Any]]:
"""Effect assessments ordered against this journal's actions.
Ordinal is the 1-based position of the action in this journal, so the
ledger can compare effects with receipt-derived evidence. Effects from
other journals in the lineage carry no ordinal here.
"""
if self.effects is None:
return []
order = {action.action_id: index for index, action in enumerate(self.actions, 1)}
changes = {action.action_id: action.artifact_changes for action in self.actions}
history = self.effects.history()
entries = []
for assessment in self.effects.assessments():
claim = history.claim(assessment.effect_id)
outcome = history.latest_outcome(assessment.effect_id)
entries.append({
'ordinal': order.get(assessment.action_id), 'assessment': assessment,
'tool': claim.operation.tool, 'unknown_scope': claim.unknown_scope, 'external': claim.external,
'paths': tuple(ref.location[-1] for ref in claim.impact_scope if ref.kind.value == 'filesystem'),
'mutation_attempted': bool(outcome and outcome.facts.mutation_attempted),
'artifact_changes': changes.get(assessment.action_id),
})
return entries
def partial_reads(self) -> tuple[str, ...]:
"""Read actions in this journal whose admitted observation was partial."""
if self.effects is None:
return ()
mine = {action.action_id for action in self.actions}
return tuple(o.source_action_id for o in self.effects.history().observations
if o.source_action_id in mine and o.mechanism.value == 'filesystem_read'
and o.coverage.value == 'partial')
def capture_versions(self, action: ActionReceipt) -> None:
if self.workspace:
action.artifact_versions = {
artifact_identity(path, self.workspace): artifact_version(path, self.workspace)
for path in self.observed_artifacts
}
def propose(self, block: Any, call_id: str = '', native_call: dict | None = None) -> ActionReceipt:
native = native_call or {}
function = native.get('function') or native
if not isinstance(function, dict):
function = {}
action = ActionReceipt(
action_id=f'{self.run_id}:action:{len(self.actions) + 1}', call_id=call_id,
proposed_tool=str(block.tool_type), proposed_arguments=str(block.content),
provider_arguments=deepcopy(function.get('arguments')),
provider_tool=str(function.get('name') or ''),
tool=str(block.tool_type), arguments=str(block.content),
)
action.transition('proposed')
self.actions.append(action)
return action
def to_list(self) -> list[dict[str, Any]]:
return [action.to_dict() for action in self.actions]
def evidence_events(self) -> list[dict[str, Any]]:
return [dict(tool=a.tool, command=a.arguments,
exit_code=(a.outcome or {}).get('exit_code'),
error=not (a.outcome or {}).get('success'),
execution_attempted=bool((a.outcome or {}).get('authoritative')),
blocked=(a.outcome or {}).get('blocked', False),
action_id=a.action_id, execution_id=a.execution_id,
artifact_versions=a.artifact_versions, artifact_changes=a.artifact_changes)
for a in self.actions if a.outcome is not None]
_JOURNAL: ContextVar[ActionJournal | None] = ContextVar('runtime_action_journal', default=None)
_ACTION: ContextVar[ActionReceipt | None] = ContextVar('runtime_current_action', default=None)
@contextmanager
def bind_journal(journal: ActionJournal):
token = _JOURNAL.set(journal)
action_token = _ACTION.set(None)
try:
yield journal
finally:
_ACTION.reset(action_token)
_JOURNAL.reset(token)
def current_journal() -> ActionJournal | None:
return _JOURNAL.get()
def propose_action(block: Any, call_id: str = '', native_call: dict | None = None) -> ActionReceipt | None:
journal = _JOURNAL.get()
return journal.propose(block, call_id, native_call) if journal else None
def mark_authorized() -> None:
action = _ACTION.get()
if action is not None and not any(t['stage'] == 'authorized' for t in action.transitions):
action.transition('authorized', authority='existing_dispatcher_policy')
def mark_dispatch() -> None:
action = _ACTION.get()
if action is not None and action.execution_id is None:
journal = _JOURNAL.get()
if journal is not None and journal.effects is not None:
# Durable claim first. If it cannot be persisted this raises and
# the action stays undispatched: the backend is never invoked.
from .effect_adapters import begin_effect
capture = begin_effect(journal, action)
journal._dispatches[action.action_id] = capture
if capture.claim is not None:
action.transition('effect_claimed', effect_id=capture.claim.effect_id,
sequence=capture.claim.sequence)
mark_authorized()
action.execution_id = action.action_id + ':execution:1'
action.transition('dispatched', execution_id=action.execution_id)
async def dispatched(operation):
"""Record an actual backend invocation, distinct from router admission."""
try:
mark_dispatch()
except BaseException:
close = getattr(operation, 'close', None)
if close is not None:
close() # never invoked; do not leave an un-awaited coroutine
raise
return await operation
def _settle(journal: ActionJournal | None, action: ActionReceipt, **outcome: Any) -> None:
if journal is None or journal.effects is None:
return
capture = journal._dispatches.pop(action.action_id, None)
if capture is None:
return
from .effect_adapters import settle_effect
try:
settle_effect(journal, action, capture, **outcome)
except Exception: # noqa: BLE001 - bookkeeping must not alter the tool result
# The claim stays unsettled (ATTEMPTED), which assesses as pending
# with possible impact: conservative, never a manufactured success.
journal.effects.degraded = True
logging.getLogger(__name__).warning('Effect outcome could not be recorded', exc_info=True)
def mark_operation_started(backend: str, **details: Any) -> None:
action = _ACTION.get()
if action is not None:
action.operation_started = True
action.transition('operation_started', backend=backend, **details)
async def execute_action(executor, action: ActionReceipt | None, block: Any, **kwargs):
"""Adapter binds the proposal across async tool-task execution and cleanup."""
if action is not None:
action.normalize(block, 'agent_loop compatibility adapters')
token = _ACTION.set(action)
try:
return await executor(block, **kwargs)
finally:
_ACTION.reset(token)
def record_action(func):
call_signature = signature(func)
@wraps(func)
async def wrapped(*args, **kwargs):
bound = call_signature.bind(*args, **kwargs)
block = bound.arguments['block']
action = _ACTION.get() or propose_action(block)
token = _ACTION.set(action)
try:
journal = current_journal()
before = {}
if action is not None:
action.normalize(block, 'dispatcher input')
if journal is not None and journal.workspace:
journal.capture_versions(action)
before = dict(action.artifact_versions)
description, result = await func(*args, **kwargs)
if action is not None:
journal = current_journal()
if journal is not None:
journal.capture_versions(action)
if journal.workspace:
action.artifact_changes = [key for key, value in action.artifact_versions.items()
if before.get(key) != value]
if 'BLOCKED' in description and action.execution_id is None:
action.transition('authorization_denied', reason=str(result.get('error', '')))
action.finish({**result, 'blocked': True})
else:
action.finish(result)
# Structured producer facts are projected here, before the
# receipt reduction drops them.
_settle(journal, action, result=result)
return description, result
except BaseException as exc:
if action is not None:
action.transition('interrupted', category=type(exc).__name__)
_settle(current_journal(), action, error=exc)
raise
finally:
_ACTION.reset(token)
return wrapped
+105
View File
@@ -0,0 +1,105 @@
"""One-use transport capabilities for admitted local Cookbook producers.
The internal HTTP token authenticates transport only. A capability bridges one
server-owned request/operation/backend to one exact resolved local launch body.
It is never persisted, returned to the model, or usable for shell/job control.
"""
from contextlib import contextmanager
from dataclasses import dataclass
import hashlib
import json
import secrets
import threading
import time
from src.agent_runtime.resources import NativeBackendResource, ResourceIdentityError
CAPABILITY_HEADER = "X-Odysseus-Local-Model-Capability"
_ROUTES = {"download_model": "/api/model/download", "serve_model": "/api/model/serve",
"serve_preset": "/api/model/serve"}
_PENDING = {}
_LOCK = threading.Lock()
def _digest(payload):
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":"),
allow_nan=False).encode()).hexdigest()
@dataclass(frozen=True)
class _Capability:
authority: object
operation: object
backend: NativeBackendResource
path: str
payload_digest: str
deadline: float
@contextmanager
def model_control_headers(tool, content, owner, payload, *, scheduled=False):
from src.tools._common import _internal_headers
headers = _internal_headers(owner)
if payload.get("remote_host"):
yield headers # Remote workload authority/transport is unchanged.
return
from src.agent_runtime.authority import active_request_authority, ExactOperation
from src.agent_runtime.remote_resources import active_backend_operation
from src.tool_security import owner_is_admin_or_single_user
authority = active_request_authority()
operation = ExactOperation.normalize(tool, content)
backend = active_backend_operation()
if (authority is None or authority.owner != str(owner or "").strip().casefold()
or tool not in _ROUTES or not owner_is_admin_or_single_user(owner)):
raise ResourceIdentityError("Local model producer has no matching server authority")
if scheduled:
# Called only by the server-owned scheduled action, after restoration of
# its immutable input ceiling. A task name or owner alone is not enough.
if tool != "serve_model" or not authority.permits(operation):
raise ResourceIdentityError("Scheduled local model input is outside authority")
resource = NativeBackendResource(tool)
if resource not in authority.backend_resources:
raise ResourceIdentityError("Scheduled local model backend is outside authority")
else:
# This binding exists only after dispatch admission (including one-use
# exact approval). A generic tool grant/header cannot create it over HTTP.
if (backend is None or backend.resource != NativeBackendResource(tool)
or (backend.request_id, backend.owner, backend.session_id,
backend.transport_tool, backend.exact_input) !=
(authority.request_id, authority.owner, authority.session_id, tool, operation.input)):
raise ResourceIdentityError("Local model producer operation or backend changed")
resource = backend.resource
capability = _Capability(authority, operation, resource, _ROUTES[tool], _digest(payload), time.monotonic() + 60)
token = secrets.token_urlsafe(32)
headers.update({CAPABILITY_HEADER: token, "X-Odysseus-Owner": authority.owner})
with _LOCK:
_PENDING[token] = capability
try:
yield headers
finally:
with _LOCK:
_PENDING.pop(token, None)
def consume_model_control(request, payload):
"""Claim exactly once at the local route, before any producer effect."""
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN, INTERNAL_TOOL_USER
from src.auth_helpers import is_direct_loopback_request
token = request.headers.get(CAPABILITY_HEADER)
if not token:
return False
if (not is_direct_loopback_request(request)
or not secrets.compare_digest(request.headers.get(INTERNAL_TOOL_HEADER, ""), INTERNAL_TOOL_TOKEN)):
raise ResourceIdentityError("Local model transport is untrusted")
with _LOCK:
capability = _PENDING.get(token)
if (capability is None or capability.deadline < time.monotonic()
or request.method != "POST" or request.url.path != capability.path
or payload.get("remote_host") or _digest(payload) != capability.payload_digest
or request.headers.get("X-Odysseus-Owner", "") != capability.authority.owner
or getattr(request.state, "current_user", None) not in
(None, INTERNAL_TOOL_USER, capability.authority.owner)):
raise ResourceIdentityError("Local model capability binding changed or expired")
del _PENDING[token]
request.state.local_model_authority = capability.authority
return True
+454
View File
@@ -0,0 +1,454 @@
"""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",
"shell", "model", "cookbook"}
segments = path.strip("/").split("/")
if len(segments) >= 3 and segments[:3] == ["api", "codex", "cookbook"]:
raise ResourceIdentityError("Cookbook wrappers require a dedicated resource-bound tool")
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")
+26
View File
@@ -0,0 +1,26 @@
"""Existing sensitive-path policy shared by tools and evidence observation.
This is a deny predicate, not an authorization grant or a workspace scope.
"""
import os
_SENSITIVE_BASENAMES: set[str] = {
".ssh", ".gnupg", ".gitconfig",
".bashrc", ".bash_profile", ".bash_logout",
".zshrc", ".zprofile", ".zshenv",
".profile", ".tcshrc", ".cshrc", ".env", ".netrc",
}
_SENSITIVE_FILE_PATTERNS: tuple[str, ...] = (
"authorized_keys", "id_rsa", "id_ed25519", "id_ecdsa",
"known_hosts", "auth.json", "app.db", "settings.json",
)
_SENSITIVE_BASENAMES_CF = frozenset(b.casefold() for b in _SENSITIVE_BASENAMES)
_SENSITIVE_FILE_PATTERNS_CF = frozenset(p.casefold() for p in _SENSITIVE_FILE_PATTERNS)
def _is_sensitive_path(resolved: str) -> bool:
# Case folding is required even on POSIX: default macOS volumes are
# case insensitive but os.path.normcase there does not fold path names.
parts = [p.casefold() for p in resolved.split(os.sep)]
filename = parts[-1] if parts else ""
return any(part in _SENSITIVE_BASENAMES_CF for part in parts) or filename in _SENSITIVE_FILE_PATTERNS_CF
+536
View File
@@ -0,0 +1,536 @@
"""Process/job admission. Lifecycle mechanics remain in process_lifecycle.
Only trusted launch producers publish observations. Persisted legacy records
are never enrolled by looking at their PID. Receipts identify boundaries, not
application authority. Resource snapshots contain no command or environment.
"""
from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass, field
import hashlib
import json
import os
from pathlib import Path
import re
import threading
from uuid import uuid4
from core.atomic_io import store_transaction
from src.agent_runtime.resources import (
BackgroundJobResource, NativeBackendResource, ProcessLaunchResource,
ProcessLaunchScope, ProcessResource, ResourceIdentityError,
)
from src.constants import PROCESS_RESOURCES_DIR
_LAUNCH_DIR = Path(PROCESS_RESOURCES_DIR)
LAUNCH_TOOLS = frozenset({"bash", "python"})
JOB_TOOL = "manage_bg_jobs"
_ACTIVE = ContextVar("process_resource_operation", default=None)
def digest(value):
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def _thread(authority):
return authority.session_id or "request:" + authority.request_id
def launch_path(generation):
if not isinstance(generation, str) or not re.fullmatch(r"[a-f0-9]{32}", generation):
raise ResourceIdentityError("Malformed launch generation")
return _LAUNCH_DIR / (generation + ".json")
def seal_launch_scopes(authority):
return tuple(seal_launch_scope(backend, root)
for backend in authority.backend_resources
if isinstance(backend, NativeBackendResource) and backend.tool_id in LAUNCH_TOOLS
for root in authority.resource_roots)
def seal_launch_scope(backend, root, *, env=None):
from src.agent_tools.subprocess_tools import _owned_spec
from src.tool_execution import _agent_subprocess_env
from src.agent_runtime.resources import PathObservation, FileObjectIdentity
env = _agent_subprocess_env() if env is None else env
extra = tuple(Path(p).resolve().as_posix() for p in str(env.get("ODYSSEUS_PYTHON_TOOL_SITE_PACKAGES", "")).split(os.pathsep)
if p and os.path.isabs(p)) if backend.tool_id == "python" else ()
spec = _owned_spec(root.path, env, 3600, extra)
return ProcessLaunchScope(backend, root, spec.required,
tuple(PathObservation(str(Path(p).resolve()), FileObjectIdentity.observe(Path(p).resolve())) for p in spec.readonly_extra),
spec.network, spec.wall_clock_s)
def validate_launch_spec(launch, spec):
scope = launch.scope
scope.validate()
if (spec.workspace != scope.root.path or spec.required != scope.required or spec.network != scope.network
or spec.wall_clock_s > scope.max_runtime_s or spec.writable_extra
or tuple(spec.readonly_extra) != tuple(r.path for r in scope.runtime_roots)):
raise ResourceIdentityError("Producer launch boundary exceeds the sealed reservation")
def job_from_record(record):
if not isinstance(record, dict):
raise ResourceIdentityError("Missing authoritative job")
try:
resource = BackgroundJobResource.from_dict(record["resource_identity"])
if (resource.namespace != "native:bg_jobs"
or (record["id"], record["session_id"], record["containment_id"])
!= (resource.job_id, resource.thread_id, resource.containment_id)):
raise ValueError("Job linkage changed")
supervisor = next(p for p in resource.processes if p.role == "supervisor")
if (record.get("pid"), record.get("start_token"), record.get("pgid")) != (
supervisor.identity.pid, supervisor.identity.start_token, supervisor.identity.pgid):
raise ValueError("Supervisor linkage changed")
launch = ProcessLaunchResource.from_dict(record["launch_resource"])
if (launch.generation, launch.owner, launch.request_id, launch.thread_id) != (
resource.generation, resource.owner, resource.request_id, resource.thread_id):
raise ValueError("Launch/job linkage changed")
return resource
except (ValueError, TypeError, KeyError, StopIteration, AttributeError) as error:
raise ResourceIdentityError("Malformed or unowned background job") from error
def validate_job(resource, *, mutation=False):
try:
return _validate_job(resource, mutation=mutation)
except ResourceIdentityError:
raise
except (ValueError, TypeError, OSError, KeyError, AttributeError) as error:
raise ResourceIdentityError("Background job linkage is missing or malformed") from error
def validate_job_receipt(resource, receipt):
from src import containment
supervisor = resource.processes[0]
if (not isinstance(receipt, dict) or receipt.get("id") != resource.containment_id
or receipt.get("launch_generation") != resource.generation
or receipt.get("owner") != "bg:" + resource.thread_id
or (receipt.get("supervisor_pid"), receipt.get("supervisor_token")) !=
(supervisor.identity.pid, supervisor.identity.start_token)
or receipt.get("mechanism") not in {m.name for m in containment.MECHANISMS}
or receipt.get("external") is True):
raise ResourceIdentityError("Containment receipt linkage changed")
def _validate_job(resource, *, mutation=False):
from src import bg_jobs, containment
if not isinstance(resource, BackgroundJobResource):
raise ResourceIdentityError("Missing exact background job identity")
record = bg_jobs.peek(resource.job_id)
if job_from_record(record) != resource:
raise ResourceIdentityError("Background job resource changed")
if record.get("status") not in {"running", "done", "failed"}:
raise ResourceIdentityError("Unknown job lifecycle")
launch = ProcessLaunchResource.from_dict(record["launch_resource"])
persisted = json.loads(launch_path(resource.generation).read_text())
if (persisted.get("launch") != launch.to_dict()
or persisted.get("job") != resource.to_dict()
or persisted.get("containment_id") != resource.containment_id):
raise ResourceIdentityError("Job/launch publication changed")
sidecar = json.loads((bg_jobs._JOBS_DIR / (resource.job_id + ".authority.json")).read_text())
origin = persisted.get("authority", {})
if (sidecar.get("job") != resource.to_dict() or sidecar.get("authority") != origin
or (origin.get("owner"), origin.get("request_id"), origin.get("session_id")) !=
(resource.owner, resource.request_id, resource.thread_id)):
raise ResourceIdentityError("Background authority linkage changed")
receipt = containment._load_records().get(resource.containment_id)
# Lifecycle receipts have a shorter retention than job results. A finished
# exact generation needs only its durable application linkage for history;
# it never regains signalling authority when its receipt has been pruned.
historical = record.get("status") in {"done", "failed"}
if receipt is None and not historical:
raise ResourceIdentityError("Missing active containment receipt")
if receipt is not None:
validate_job_receipt(resource, receipt)
if record.get("status") == "running":
for process in resource.processes:
try:
process.validate()
except ResourceIdentityError:
# Publication can precede store reconciliation. That exact
# completed generation is readable, but never signallable.
if mutation or not Path(record["exit_path"]).is_file():
raise
report = json.loads(Path(record["result_path"]).read_text())
if report.get("resource_identity") != resource.to_dict() or report.get("containment", {}).get("id") != resource.containment_id:
raise ResourceIdentityError("Historical result linkage changed")
# A completed record is readable history, never a new process observation.
return record
def seal_jobs(authority):
if not any(g.tool == JOB_TOOL for g in authority.grants) or not authority.session_id:
return ()
from src import bg_jobs
admitted = []
for record in bg_jobs._load().values():
try:
resource = job_from_record(record)
if (resource.owner, resource.thread_id) == (authority.owner, authority.session_id):
validate_job(resource)
admitted.append(resource)
except (ValueError, TypeError, OSError, RuntimeError):
continue
return tuple(admitted)
def intersect_observed(parent, child, validate):
# Validate both sides before equality. Seeing a replacement cannot renew a
# stale parent observation, even when the child has just sealed it.
# Stale/dead/unverifiable resources on EITHER side are conservatively
# excluded from the resulting authority — a normal process exit must not
# crash child authority intersection.
live_parent = []
for resource in parent:
try:
validate(resource)
live_parent.append(resource)
except ResourceIdentityError:
continue
live_child = set()
for resource in child:
try:
validate(resource)
live_child.add(resource)
except ResourceIdentityError:
continue
return tuple(resource for resource in live_parent if resource in live_child)
def intersect_launch_scopes(parent, child):
from src.agent_runtime.resources import FilesystemResource
for scope in (*parent, *child):
scope.validate()
narrowed = []
for left in parent:
for right in child:
if (left.backend != right.backend or not left.required <= right.required
or right.max_runtime_s > left.max_runtime_s
or not set(right.runtime_roots) <= set(left.runtime_roots)
or (left.network == "none" and right.network != "none")):
continue
if Path(right.root.path).is_relative_to(left.root.path):
observation = FilesystemResource.resolve(left.root, right.root.path)
if observation.identity == right.root.identity:
narrowed.append(right)
return tuple(dict.fromkeys(narrowed))
class _LaunchUse:
"""Non-persisted one-use producer reservation, shared by approval copies."""
def __init__(self):
self.used = False
self.lock = threading.Lock()
def claim(self):
with self.lock:
if self.used:
raise ResourceIdentityError("Launch reservation has already been used")
self.used = True
@dataclass(frozen=True)
class BoundProcessOperation:
operation: object
request_id: str
owner: str
thread_id: str
launch: ProcessLaunchResource | None = None
jobs: tuple[BackgroundJobResource, ...] = ()
processes: tuple[ProcessResource, ...] = ()
exact_approval: object | None = None
_launch_use: _LaunchUse = field(default_factory=_LaunchUse, compare=False, repr=False)
def __post_init__(self):
from src.agent_runtime.authority import ExactOperation
if (not isinstance(self.operation, ExactOperation) or not isinstance(self.request_id, str) or not self.request_id
or not isinstance(self.owner, str) or not isinstance(self.thread_id, str) or not self.thread_id
or (self.launch is not None and not isinstance(self.launch, ProcessLaunchResource))
or not isinstance(self.jobs, tuple) or any(not isinstance(j, BackgroundJobResource) for j in self.jobs)
or not isinstance(self.processes, tuple) or any(not isinstance(p, ProcessResource) for p in self.processes)):
raise ValueError("Malformed process-bound operation")
if self.launch is not None and (
(self.launch.owner, self.launch.request_id, self.launch.thread_id, self.launch.tool, self.launch.input_digest)
!= (self.owner, self.request_id, self.thread_id, self.operation.tool, digest(self.operation.input))):
raise ValueError("Launch operation/application binding changed")
if any((r.owner, r.thread_id) != (self.owner, self.thread_id) for r in (*self.jobs, *self.processes)):
raise ValueError("Observed resource application binding changed")
def validate(self):
if self.launch is not None:
self.launch.validate()
if self._launch_use.used:
raise ResourceIdentityError("Launch reservation has already been used")
for job in self.jobs:
validate_job(job, mutation=self.operation.action in {"kill", "stop", "cancel", "terminate", "ack"})
for process in self.processes:
process.validate()
def to_dict(self):
return {"tool": self.operation.transport_tool, "input_digest": digest(self.operation.input),
"request_id": self.request_id, "owner": self.owner, "thread_id": self.thread_id,
"launch": self.launch.to_dict() if self.launch else None,
"jobs": [r.to_dict() for r in self.jobs], "processes": [r.to_dict() for r in self.processes]}
def needs_process_binding(operation, backend):
return isinstance(backend, NativeBackendResource) and operation.tool in LAUNCH_TOOLS | {JOB_TOOL}
def resolve_process_operation(authority, operation, backend, *, approved=None, exact_admission=False):
if not needs_process_binding(operation, backend):
raise ResourceIdentityError("No native process adapter for this backend")
if approved is not None:
if (approved.operation != operation or (approved.request_id, approved.owner, approved.thread_id)
!= (authority.request_id, authority.owner, _thread(authority))):
raise ResourceIdentityError("Approved process operation binding changed")
bound = approved
elif operation.tool in LAUNCH_TOOLS:
scopes = [s for s in authority.launch_scopes if s.backend == backend]
if len(scopes) != 1:
raise ResourceIdentityError("Process creation requires a sealed workspace and launch scope")
launch = ProcessLaunchResource("native:containment", authority.owner, authority.request_id,
_thread(authority), uuid4().hex, operation.tool, digest(operation.input), scopes[0],
digest(json.dumps(authority.to_dict(), sort_keys=True)))
bound = BoundProcessOperation(operation, authority.request_id, authority.owner, _thread(authority), launch)
else:
try:
args = json.loads(operation.input)
action = str(args.get("action", "list")).strip().lower()
job_id = args.get("job_id", args.get("id", ""))
except (ValueError, TypeError, AttributeError) as error:
raise ResourceIdentityError("Malformed job operation") from error
if action in {"list", "ls", "jobs"}:
jobs = authority.job_resources
elif action in {"output", "get", "read", "tail", "status", "show", "kill", "stop", "cancel", "terminate", "ack"}:
if not isinstance(job_id, str) or not job_id:
raise ResourceIdentityError("An exact job selector is required")
jobs = tuple(r for r in authority.job_resources if r.job_id == job_id)
if len(jobs) != 1:
raise ResourceIdentityError("Job is outside admitted resource scope")
else:
raise ResourceIdentityError("Unsupported job operation")
bound = BoundProcessOperation(operation, authority.request_id, authority.owner, _thread(authority), jobs=jobs)
if not (approved is not None and exact_admission and not authority.inherited):
if bound.launch is not None and bound.launch.scope not in authority.launch_scopes:
raise ResourceIdentityError("Launch exceeds inherited creation scope")
if any(j not in authority.job_resources for j in bound.jobs) or any(p not in authority.process_resources for p in bound.processes):
raise ResourceIdentityError("Process/job exceeds inherited resource scope")
if bound.launch is not None and bound.launch.scope.backend != backend:
raise ResourceIdentityError("Launch backend changed")
bound.validate()
return bound
def active_process_operation():
return _ACTIVE.get()
@contextmanager
def bind_process_operation(operation):
if operation is not None and not isinstance(operation, BoundProcessOperation):
raise TypeError("Process operation must be server-owned")
if operation is not None:
operation.validate()
if operation.launch is not None:
# One fresh authoritative scan for each execution binding. Resolution
# and producer entry retain cheap exact identity checks; no scan is
# reused across independent bindings or persisted in an approval.
guard_launch_workspace(operation.launch.scope.root)
token = _ACTIVE.set(operation)
try:
yield operation
finally:
_ACTIVE.reset(token)
def require_launch(tool, *, cwd, content=None):
bound = active_process_operation()
if bound is None or bound.launch is None or bound.operation.tool != tool:
raise ResourceIdentityError("Native process producer has no bound launch reservation")
require_process_admission(bound)
bound.validate()
if Path(cwd).resolve() != Path(bound.launch.scope.root.path):
raise ResourceIdentityError("Launch workspace changed")
if content is not None and content.strip() != bound.operation.input.strip():
raise ResourceIdentityError("Launch operation changed at producer entry")
return bound.launch
def require_process_admission(bound):
from src.agent_runtime.authority import active_request_authority
authority = active_request_authority()
if authority is None or (authority.owner, authority.request_id, _thread(authority)) != (
bound.owner, bound.request_id, bound.thread_id):
raise ResourceIdentityError("Producer application authority changed")
if not authority.permits(bound.operation):
approval = bound.exact_approval
if (authority.inherited or approval is None or not approval._claimed
or approval.pending.process_operation is None
or approval.pending.process_operation.to_dict() != bound.to_dict()):
raise ResourceIdentityError("Producer operation has no request admission or exact claim")
def guard_launch_workspace(root):
"""Reject a boundary containing execution control state or its aliases.
These are pathname/inode observations, not an atomic kernel access policy.
They do not claim freedom from concurrent link replacement after checking.
"""
from src import bg_jobs, containment, constants
from src import browser_identity
from src.agent_runtime.resources import _control_plane_path, _control_plane_snapshot
control = (Path(bg_jobs._STORE), Path(bg_jobs._JOBS_DIR), containment._store_path(), _LAUNCH_DIR,
Path(constants.BROWSER_RESOURCES_DIR),
browser_identity.STATE_ROOT,
Path(constants.APP_DB), Path(constants.AUTH_FILE), Path(constants.SETTINGS_FILE))
base = Path(root.path)
if any(Path(p).resolve().is_relative_to(base) for p in control):
raise ResourceIdentityError("Launch boundary contains server control state")
def unresolved(error):
raise ResourceIdentityError("Launch workspace cannot be inspected") from error
snapshot = None
for directory, dirs, files in os.walk(base, followlinks=False, onerror=unresolved):
for name in (*dirs, *files):
path = Path(directory) / name
info = path.lstat()
if path.is_symlink() or info.st_nlink > 1:
if snapshot is None:
snapshot = _control_plane_snapshot()
if _control_plane_path(str(path.resolve()), snapshot=snapshot):
raise ResourceIdentityError("Launch boundary aliases server control state")
@store_transaction(lambda: _LAUNCH_DIR / "publication")
def publish_launch(launch, authority, containment_id, *, job=None, processes=()):
from core.atomic_io import atomic_write_json
launch.validate()
if authority is None or (authority.owner, authority.request_id) != (launch.owner, launch.request_id):
raise ResourceIdentityError("Launch authority linkage changed")
path = launch_path(launch.generation)
if path.exists():
raise ResourceIdentityError("Launch reservation has already been used")
bound = active_process_operation()
if bound is not None:
if bound.launch != launch:
raise ResourceIdentityError("Publication differs from the bound launch")
bound._launch_use.claim()
atomic_write_json(path, {"launch": launch.to_dict(), "authority": authority.to_dict(),
"containment_id": containment_id, "job": job.to_dict() if job else None,
"processes": [p.to_dict() for p in processes]})
@store_transaction(lambda: _LAUNCH_DIR / "publication")
def retire_launch(launch, containment_id, *, job=None):
"""Remove only this exact producer publication; never a replacement.
Callers establish the lifetime end (verified foreground teardown, or exact
background history pruning). Missing/malformed/replaced state is retained.
One-use launch reservations live in the bound operation, not this file.
"""
path = launch_path(launch.generation)
try:
published = json.loads(path.read_text())
except FileNotFoundError:
return False
if (not isinstance(published, dict)
or published.get("launch") != launch.to_dict()
or published.get("containment_id") != containment_id
or published.get("job") != (job.to_dict() if job else None)):
return False
path.unlink()
return True
@store_transaction(lambda: _LAUNCH_DIR / "publication")
def prune_foreground_publications():
"""Startup-only recovery: retire foreground generations without a caller.
A dead/replaced manager cannot resume attachment. A missing receipt also
makes attachment impossible; publication cannot reconstruct that receipt.
Its process tree still belongs to containment recovery; deleting a
publication never signals or asserts tree death. Live/unverifiable managers
retain publication even after child teardown: attachment may still need it.
Background history stays intact.
"""
from src import containment
from src import process_ownership
try:
receipts = json.loads(containment._store_path().read_text())
except FileNotFoundError:
receipts = {}
except (OSError, ValueError):
return 0 # Unreadable state is not evidence that consumers are gone.
if not isinstance(receipts, dict) or any(not isinstance(r, dict) for r in receipts.values()):
return 0
retired = 0
for path in _LAUNCH_DIR.glob("*.json"):
try:
published = json.loads(path.read_text())
launch = ProcessLaunchResource.from_dict(published["launch"])
receipt = receipts.get(published["containment_id"])
abandoned = (receipt is not None
and type(receipt.get("manager_pid")) is int and receipt["manager_pid"] > 0
and isinstance(receipt.get("manager_token"), str) and bool(receipt["manager_token"])
and process_ownership.verify(receipt["manager_pid"], receipt["manager_token"]) in {
process_ownership.GONE, process_ownership.FOREIGN})
if (published.get("job") is None and path == launch_path(launch.generation)
and (receipt is None or (
receipt.get("launch_generation") == launch.generation
and receipt.get("id") == published["containment_id"]
and abandoned))):
# Already under the publication lock; no nested file lock.
path.unlink()
retired += 1
except (ValueError, TypeError, KeyError, OSError):
continue
return retired
@store_transaction(lambda: _LAUNCH_DIR / "publication")
def attach_containment_processes(launch, containment_id):
"""Attach producer-frozen lifecycle records; never capture a current PID."""
from src import containment
from src.process_lifecycle import ProcessIdentity
record = containment._load_records().get(containment_id, {})
path = launch_path(launch.generation)
published = json.loads(path.read_text())
if (published.get("launch") != launch.to_dict() or published.get("containment_id") != containment_id
or record.get("id") != containment_id or record.get("launch_generation") != launch.generation
or record.get("workspace") != launch.scope.root.path):
raise ResourceIdentityError("Launch/receipt changed during publication")
processes = []
for role, pid_key, token_key, group_key in (("leader", "pid", "start_token", "pgid"),
("namespace_init", "namespace_pid", "namespace_start_token", None)):
pid = record.get(pid_key)
token = record.get(token_key)
if not pid or not token:
continue
processes.append(ProcessResource("native:containment", launch.owner, launch.request_id,
launch.thread_id, ProcessIdentity(pid, token, record.get(group_key) if group_key else None),
role, "", containment_id))
from core.atomic_io import atomic_write_json
published["processes"] = [p.to_dict() for p in processes]
atomic_write_json(path, published)
def expected_job(job_id, *, action):
bound = active_process_operation()
if bound is None or bound.operation.tool != JOB_TOOL:
raise ResourceIdentityError("Job producer has no bound operation")
require_process_admission(bound)
# The caller's actual action must agree with the normalized proposal.
args = json.loads(bound.operation.input)
proposed = str(args.get("action", "list")).strip().lower()
if action != proposed:
raise ResourceIdentityError("Job action changed at producer entry")
target = next((j for j in bound.jobs if j.job_id == job_id), None)
if target is None:
raise ResourceIdentityError("Job selector is outside the bound operation")
validate_job(target, mutation=action in {"kill", "stop", "cancel", "terminate", "ack"})
return target
+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)
+218
View File
@@ -0,0 +1,218 @@
"""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")
@property
def write_intent(self):
"""Trusted original intent; execution_input only normalizes the path."""
if self.operation.tool != "write_file":
return None
from src.agent_tools.filesystem_tools import _parse_write_intent
return _parse_write_intent(self.operation.input)
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
if operation.tool == "write_file":
from src.agent_tools.filesystem_tools import _parse_write_intent
original_path, _, section, _ = _parse_write_intent(content)
if not section:
raise ValueError("write_file: content required; missing content section")
if original_path.endswith(("/", "\\")):
raise ValueError("write_file: target is a directory")
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 in {"write_file", "read_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)
+740
View File
@@ -0,0 +1,740 @@
"""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
import sys
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 _effect_store_dirs():
from src import constants
directories = {canonical_root(os.path.join(constants.DATA_DIR, "effects"))}
effect_log = sys.modules.get("src.agent_runtime.effect_log")
if effect_log is not None:
directories.add(canonical_root(effect_log.EFFECTS_DIR))
return directories
def _aliases_effect_store(candidate, directories):
"""Whether ``candidate`` (an ``os.stat`` result) is a hardlink into the effect store.
The effect log and launch index refuse any file with more than one link,
and the store is flat. So only a multiply linked regular file on the
store's device can alias store state, and only then is the store listed,
one directory level, by inode. Ordinary single-link files cost nothing,
and the cost never depends on recursive store size. Uninspectable store
state fails closed.
"""
if not stat.S_ISREG(candidate.st_mode) or candidate.st_nlink < 2:
return False
for directory in directories:
try:
if os.stat(directory).st_dev != candidate.st_dev:
continue
with os.scandir(directory) as entries:
for entry in entries:
if entry.inode() != candidate.st_ino:
continue
observed = entry.stat(follow_symlinks=False)
if (observed.st_dev, observed.st_ino) == (candidate.st_dev, candidate.st_ino):
return True
except FileNotFoundError:
continue
except OSError:
return True
return False
def _control_plane_snapshot():
# Execution snapshots/receipts are server state, even if a workspace root
# contains the data directory. A writable user file cannot mint authority.
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), canonical_root(constants.PROCESS_RESOURCES_DIR),
canonical_root(constants.BROWSER_RESOURCES_DIR)}
browser = sys.modules.get("src.browser_identity")
if browser is not None:
job_dirs.add(canonical_root(browser.STATE_ROOT))
processes = sys.modules.get("src.agent_runtime.process_resources")
if processes is not None:
job_dirs.add(canonical_root(processes._LAUNCH_DIR))
# Durable effect claims/outcomes/observations are server evidence state.
# They are prefix-protected below, but never inventoried: the store grows
# with every run. Hardlink aliases are caught by ``_aliases_effect_store``.
effect_dirs = _effect_store_dirs()
# 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"))
# Prefix protection covers the complete runtime trees. Public policy denies
# every regular hardlink alias, so collecting all child inodes here would
# add an unbounded recursive state inventory without widening protection.
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"))
identities = set()
for control in protected:
try:
observed = os.stat(control)
except FileNotFoundError:
continue
identities.add((observed.st_dev, observed.st_ino))
# Effect directories are protected by prefix without inventorying children.
return frozenset(job_dirs | effect_dirs), frozenset(protected), frozenset(identities)
def _control_plane_path(path, *, snapshot=None):
# A scan-local snapshot bounds repeated hardlink checks. Ordinary resource
# resolution always observes fresh state. Neither form is an atomic kernel
# access policy, and snapshots must never survive a workspace guard call.
# Public state and hardlink denial also applies to sealed resources. Lazy
# import avoids coupling inert identity definitions to dispatcher startup.
from src.tool_execution import _is_app_state_path, _is_hardlinked_regular_file
if _is_sensitive_path(path) or _is_app_state_path(path) or _is_hardlinked_regular_file(path):
return True
directories, protected, identities = _control_plane_snapshot() if snapshot is None else snapshot
if any(Path(path).is_relative_to(directory) for directory in directories) or path in protected:
return True
try:
candidate = os.stat(path)
except FileNotFoundError:
return False
if (candidate.st_dev, candidate.st_ino) in identities:
return True
# Only a multiply linked file can alias the (uninventoried) effect store.
return candidate.st_nlink > 1 and _aliases_effect_store(candidate, directories & _effect_store_dirs())
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 BrowserSessionObservation:
producer_namespace: str
producer_version: str
platform: str
binary_sha256: str
configuration_digest: str
session_key: str
daemon: "ProcessIdentity"
browser_instance_digest: str
session_incarnation: str
def __post_init__(self):
from src.process_lifecycle import ProcessIdentity
from src.browser_identity import PRODUCER_HASHES, incarnation
if (self.producer_namespace != "native:agent-browser"
or self.producer_version != "0.35.0"
or PRODUCER_HASHES.get(self.platform) != self.binary_sha256
or not isinstance(self.daemon, ProcessIdentity)
or type(self.daemon.pid) is not int or self.daemon.pid <= 0
or (self.daemon.pgid is not None and (type(self.daemon.pgid) is not int or self.daemon.pgid <= 0))):
raise ValueError("Unsupported browser producer observation")
import re
_text(self.daemon.start_token, "daemon incarnation")
if not re.fullmatch(r"ody-[a-f0-9]{24}", self.session_key):
raise ValueError("Malformed browser session selector")
for value in (self.configuration_digest, self.browser_instance_digest, self.session_incarnation):
if not re.fullmatch(r"[a-f0-9]{64}", value):
raise ValueError("Malformed browser digest")
if incarnation(self) != self.session_incarnation:
raise ValueError("Browser incarnation digest changed")
def to_dict(self):
return {**asdict(self), "daemon": self.daemon.to_record()}
@classmethod
def from_dict(cls, value):
from src.process_lifecycle import ProcessIdentity
if not isinstance(value, dict) or set(value) != set(cls.__dataclass_fields__):
raise ValueError("Malformed browser observation snapshot")
daemon = value["daemon"]
if not isinstance(daemon, dict) or set(daemon) != {"pid", "start_token", "pgid"}:
raise ValueError("Malformed browser daemon observation")
return cls(**{**value, "daemon": ProcessIdentity(**daemon)})
@dataclass(frozen=True)
class BrowserSessionResource:
owner: str
thread_id: str
observation: BrowserSessionObservation
def __post_init__(self):
_text(self.owner, "browser owner")
_text(self.thread_id, "browser thread")
if not isinstance(self.observation, BrowserSessionObservation):
raise ValueError("Missing browser session observation")
def validate(self):
from src.browser_identity import validate_session
validate_session(self)
def to_dict(self):
return {"owner": self.owner, "thread_id": self.thread_id, "observation": self.observation.to_dict()}
@classmethod
def from_dict(cls, value):
if not isinstance(value, dict) or set(value) != {"owner", "thread_id", "observation"}:
raise ValueError("Malformed browser resource snapshot")
return cls(value["owner"], value["thread_id"], BrowserSessionObservation.from_dict(value["observation"]))
@dataclass(frozen=True)
class BrowserPageResource:
session: BrowserSessionResource
target_id: str
loader_id: str
resolved_alias: str = ""
observed_url: str = ""
scope: str = "document"
def __post_init__(self):
import re
if not isinstance(self.session, BrowserSessionResource) or not re.fullmatch(r"[A-F0-9]{32}", self.target_id):
raise ValueError("Malformed browser page identity")
if self.scope not in {"page", "document"}:
raise ValueError("Malformed browser page scope")
_text(self.loader_id, "document loader", optional=self.scope == "page")
_text(self.observed_url, "observed URL", optional=True)
if self.resolved_alias and not re.fullmatch(r"t[1-9][0-9]*", self.resolved_alias):
raise ValueError("Malformed browser alias metadata")
def authority_key(self):
return (self.session, self.target_id, self.loader_id if self.scope == "document" else None)
def validate(self):
from src.browser_identity import validate_page
validate_page(self)
def to_dict(self):
return {**asdict(self), "session": self.session.to_dict()}
@classmethod
def from_dict(cls, value):
if not isinstance(value, dict) or set(value) != set(cls.__dataclass_fields__):
raise ValueError("Malformed browser page snapshot")
return cls(**{**value, "session": BrowserSessionResource.from_dict(value["session"])})
@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
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.
result.append(right)
elif Path(left.path).is_relative_to(right.path):
result.append(left)
except (OSError, ValueError, RuntimeError):
continue
return tuple(dict.fromkeys(result))
@dataclass(frozen=True)
class ProcessResource:
namespace: str
owner: str
request_id: str
thread_id: str
identity: "ProcessIdentity"
role: str
job_id: str = ""
containment_id: str = ""
def __post_init__(self):
from src.process_lifecycle import ProcessIdentity
for name in ("namespace", "request_id", "thread_id"):
_text(getattr(self, name), name)
for name in ("owner", "job_id", "containment_id"):
_text(getattr(self, name), name, optional=True)
if (not isinstance(self.identity, ProcessIdentity)
or type(self.identity.pid) is not int or self.identity.pid <= 0
or (self.identity.pgid is not None and (type(self.identity.pgid) is not int or self.identity.pgid <= 0))
or self.role not in {"supervisor", "leader", "namespace_init", "manager", "pty", "service"}):
raise ValueError("Malformed process resource identity")
supported_roles = {"native:containment": {"leader", "namespace_init"},
"native:bg_jobs": {"supervisor"}}
if self.role not in supported_roles.get(self.namespace, set()):
raise ValueError("Unsupported process producer or role")
_text(self.identity.start_token, "process start token")
def validate(self):
if not self.identity.owned() or self.identity.exited():
raise ResourceIdentityError("Process resource is stale or unverifiable")
def to_dict(self):
return {"namespace": self.namespace, "owner": self.owner, "request_id": self.request_id,
"thread_id": self.thread_id, "identity": self.identity.to_record(), "role": self.role,
"job_id": self.job_id, "containment_id": self.containment_id}
@classmethod
def from_dict(cls, value):
from src.process_lifecycle import ProcessIdentity
if not isinstance(value, dict) or set(value) != {"namespace", "owner", "request_id", "thread_id", "identity", "role", "job_id", "containment_id"}:
raise ValueError("Malformed process resource snapshot")
identity = value["identity"]
if not isinstance(identity, dict) or set(identity) != {"pid", "start_token", "pgid"}:
raise ValueError("Malformed lifecycle identity snapshot")
return cls(**{**value, "identity": ProcessIdentity(**identity)})
@dataclass(frozen=True)
class ProcessLaunchScope:
backend: "NativeBackendResource"
root: FilesystemRoot
required: frozenset[str]
runtime_roots: tuple[PathObservation, ...] = ()
network: str = "inherit"
max_runtime_s: int = 3600
def __post_init__(self):
if (not isinstance(self.backend, NativeBackendResource) or not isinstance(self.root, FilesystemRoot)
or not isinstance(self.required, frozenset) or not self.required
or any(not isinstance(v, str) or not v for v in self.required)):
raise ValueError("Malformed process launch scope")
if self.backend.tool_id not in {"bash", "python"}:
raise ValueError("Unsupported native launch producer")
if (not isinstance(self.runtime_roots, tuple) or any(not isinstance(r, PathObservation) for r in self.runtime_roots)
or self.network not in {"inherit", "none"}
or type(self.max_runtime_s) is not int or self.max_runtime_s <= 0):
raise ValueError("Malformed launch boundary selectors")
def validate(self):
self.root.validate()
for runtime in self.runtime_roots:
if canonical_root(runtime.path) != runtime.path or FileObjectIdentity.observe(runtime.path) != runtime.identity:
raise ResourceIdentityError("Launch runtime root changed")
def to_dict(self):
return {"backend": self.backend.to_dict(), "root": self.root.to_dict(), "required": sorted(self.required),
"runtime_roots": [{"path": r.path, "identity": asdict(r.identity)} for r in self.runtime_roots],
"network": self.network, "max_runtime_s": self.max_runtime_s}
@classmethod
def from_dict(cls, value):
if not isinstance(value, dict) or set(value) != {"backend", "root", "required", "runtime_roots", "network", "max_runtime_s"} or not isinstance(value["required"], list) or not isinstance(value["runtime_roots"], list):
raise ValueError("Malformed launch scope snapshot")
return cls(backend_from_dict(value["backend"]), FilesystemRoot.from_dict(value["root"]), frozenset(value["required"]),
tuple(PathObservation(r["path"], FileObjectIdentity(**r["identity"])) for r in value["runtime_roots"]),
value["network"], value["max_runtime_s"])
@dataclass(frozen=True)
class ProcessLaunchResource:
namespace: str
owner: str
request_id: str
thread_id: str
generation: str
tool: str
input_digest: str
scope: ProcessLaunchScope
ceiling_digest: str
def __post_init__(self):
for name in ("namespace", "request_id", "thread_id", "generation", "tool", "input_digest", "ceiling_digest"):
_text(getattr(self, name), name)
_text(self.owner, "owner", optional=True)
if not isinstance(self.scope, ProcessLaunchScope) or self.tool != self.scope.backend.tool_id:
raise ValueError("Malformed launch resource")
import re
if (self.namespace != "native:containment" or not re.fullmatch(r"[a-f0-9]{32}", self.generation)
or any(not re.fullmatch(r"[a-f0-9]{64}", v) for v in (self.input_digest, self.ceiling_digest))):
raise ValueError("Malformed native launch producer or generation")
def validate(self):
self.scope.validate()
def to_dict(self):
return {**{k: getattr(self, k) for k in ("namespace", "owner", "request_id", "thread_id", "generation", "tool", "input_digest", "ceiling_digest")},
"scope": self.scope.to_dict()}
@classmethod
def from_dict(cls, value):
if not isinstance(value, dict) or set(value) != {"namespace", "owner", "request_id", "thread_id", "generation", "tool", "input_digest", "scope", "ceiling_digest"}:
raise ValueError("Malformed launch resource snapshot")
return cls(**{**value, "scope": ProcessLaunchScope.from_dict(value["scope"])})
@dataclass(frozen=True)
class BackgroundJobResource:
namespace: str
job_id: str
generation: str
owner: str
request_id: str
thread_id: str
containment_id: str
processes: tuple[ProcessResource, ...]
def __post_init__(self):
for name in ("namespace", "job_id", "generation", "request_id", "thread_id", "containment_id"):
_text(getattr(self, name), name)
_text(self.owner, "owner", optional=True)
import re
if (not re.fullmatch(r"[A-Za-z0-9_-]+", self.job_id)
or not re.fullmatch(r"[a-f0-9]{32}", self.generation)):
raise ValueError("Malformed job selector or launch generation")
if (not isinstance(self.processes, tuple) or not self.processes
or any(not isinstance(p, ProcessResource) or (p.owner, p.request_id, p.thread_id, p.job_id, p.containment_id)
!= (self.owner, self.request_id, self.thread_id, self.job_id, self.containment_id) for p in self.processes)
or len({p.role for p in self.processes}) != len(self.processes)):
raise ValueError("Malformed background job resource")
if self.namespace != "native:bg_jobs" or any(p.namespace != "native:bg_jobs" or p.role != "supervisor" for p in self.processes):
raise ValueError("Unsupported job producer or process role")
def to_dict(self):
return {**{k: getattr(self, k) for k in ("namespace", "job_id", "generation", "owner", "request_id", "thread_id", "containment_id")},
"processes": [p.to_dict() for p in self.processes]}
@classmethod
def from_dict(cls, value):
if not isinstance(value, dict) or set(value) != {"namespace", "job_id", "generation", "owner", "request_id", "thread_id", "containment_id", "processes"} or not isinstance(value["processes"], list):
raise ValueError("Malformed background resource snapshot")
return cls(**{**value, "processes": tuple(ProcessResource.from_dict(p) for p in value["processes"])})
@dataclass(frozen=True)
class ExternalResource:
namespace: str
endpoint_id: str
server_id: str
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 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)
class OwnedResource:
namespace: str
owner: str
thread_id: str
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}))
+44
View File
@@ -0,0 +1,44 @@
"""Whether a turn runs on the compact (clean v3) preview runtime.
The chat route decides this once, from facts known before context
preparation, and uses that one value both to prepare the turn (its typed
context resolution) and to stamp the turn contract's selection mode. The
agent loop dispatches on that stamp. Keeping both sides here, with no other
imports, means preparation and dispatch read one rule and cannot drift.
Runtime selection is not authority: it grants or denies no operation.
"""
COMPACT_PREVIEW_MODE = "clean_compact_v3_preview"
def uses_compact_preview_runtime(
*,
clean_route_requested: bool,
turn_contract_enabled: bool,
agent_mode: bool,
agent_permitted: bool,
image_generation: bool,
) -> bool:
"""The single compact-runtime eligibility rule for one turn.
``turn_contract_enabled`` is the route's contract policy for this turn
(exact approvals, TUI surface and full-schema routes opt out).
``agent_permitted`` is false when the user's privileges demote the turn
to plain chat; image generation sessions run their own execution path.
"""
return bool(
clean_route_requested
and turn_contract_enabled
and agent_mode
and agent_permitted
and not image_generation
)
def is_compact_preview_contract(turn_contract) -> bool:
"""Whether a turn contract was stamped for the compact runtime."""
return (
turn_contract is not None
and getattr(turn_contract, "selection_mode", None) == COMPACT_PREVIEW_MODE
)