mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-07 15:32:21 +02:00
fix(effects): close independent review correctness gaps
This commit is contained in:
@@ -155,10 +155,16 @@ def completion_answer(text: str, ledger: EvidenceLedger, decision: CompletionDec
|
||||
|
||||
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 answer
|
||||
return (answer.rstrip() + '\n\n' + summary) if answer.strip() else summary
|
||||
return ''
|
||||
return ('\n\n' + summary) if answer.strip() else summary
|
||||
|
||||
|
||||
def _completion_answer(text: str, ledger: EvidenceLedger, decision: CompletionDecision) -> tuple[str, str]:
|
||||
@@ -351,7 +357,7 @@ def with_completion_gate(func):
|
||||
# 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 = safe_answer[len(filtered_answer):] if safe_answer != filtered_answer else ''
|
||||
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(
|
||||
@@ -392,7 +398,7 @@ def with_completion_gate(func):
|
||||
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] + disclosure]
|
||||
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
|
||||
|
||||
@@ -98,7 +98,7 @@ def _pre_state_text(resource: Any, *, newline: str | None) -> str | None:
|
||||
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 = _read_whole(resource, _PRE_STATE_LIMIT).data
|
||||
if data is None or len(data) > _PRE_STATE_LIMIT:
|
||||
return None
|
||||
try:
|
||||
@@ -336,33 +336,58 @@ def settle_effect(journal: Any, action: Any, capture: DispatchCapture | 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) or result.get("exit_code") != 0 or result.get("error"):
|
||||
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):
|
||||
log.observe(**fields)
|
||||
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 ------------------------------------------------------------
|
||||
|
||||
def _read_whole(resource: Any, limit: int) -> bytes | None:
|
||||
"""Re-read the exact admitted source binding; None if it is not stable."""
|
||||
@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 None
|
||||
return _WholeFileRead()
|
||||
data = stream.read(limit + 1)
|
||||
resource.validate()
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return data
|
||||
except (OSError, ValueError, RuntimeError):
|
||||
return _WholeFileRead()
|
||||
return _WholeFileRead(data=data)
|
||||
|
||||
|
||||
def _file_observation(capture: DispatchCapture, action: Any) -> dict[str, Any] | None:
|
||||
@@ -373,17 +398,21 @@ def _file_observation(capture: DispatchCapture, action: Any) -> dict[str, Any] |
|
||||
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)
|
||||
data = _read_whole(resource, producer.MAX_READ_CHARS * 4)
|
||||
if data is None:
|
||||
read = _read_whole(resource, producer.MAX_READ_CHARS * 4)
|
||||
data = read.data
|
||||
if data is None and not read.known_absent:
|
||||
return None
|
||||
if len(data) > producer.MAX_READ_CHARS * 4 or len(data.decode("utf-8", errors="replace")) > producer.MAX_READ_CHARS:
|
||||
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=True, content_sha256=hashlib.sha256(data).hexdigest() if complete else "")
|
||||
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]]:
|
||||
|
||||
@@ -176,7 +176,7 @@ def resolve_filesystem_operation(operation, *, roots, workspace="", request_id="
|
||||
raise ValueError("Search root is unresolved")
|
||||
args["path"] = bind(selector, "search_root" if search else
|
||||
"source" if tool == "read_file" else "destination" if tool == "write_file" else "target",
|
||||
missing=tool == "write_file")
|
||||
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()
|
||||
|
||||
@@ -1355,7 +1355,7 @@ from src.agent_runtime.process_resources import (
|
||||
active_process_operation, bind_process_operation, needs_process_binding, resolve_process_operation,
|
||||
)
|
||||
from src.browser_identity import (
|
||||
native_browser, parse_operation as parse_browser_operation, SESSION_ACTIONS,
|
||||
native_browser, parse_operation as parse_browser_operation, SESSION_ACTIONS, PAGE_FAILURE,
|
||||
page_unavailable, resolve_browser_operation, bind_browser_operation, revalidate_browser_operation,
|
||||
)
|
||||
|
||||
@@ -1642,6 +1642,8 @@ async def execute_tool_block(
|
||||
)
|
||||
return output
|
||||
except ResourceIdentityError as error:
|
||||
if native_browser(operation, backend_operation.resource) and str(error) == PAGE_FAILURE:
|
||||
return f"{transport}: UNSUPPORTED", page_unavailable()
|
||||
return f"{transport}: BLOCKED", {
|
||||
"error": str(error), "exit_code": 1, "blocked": True,
|
||||
"failure_kind": "resource_identity_denied",
|
||||
|
||||
Reference in New Issue
Block a user