fix(agent): seal document approval content

This commit is contained in:
RaresKeY
2026-08-15 07:01:36 +00:00
parent 2b72531eaa
commit 7a138e8a3f
7 changed files with 151 additions and 10 deletions
+16 -1
View File
@@ -43,7 +43,11 @@ from src.tool_capabilities import (
tool_result_is_successful,
tool_result_should_arm_gate,
)
from src.tool_approvals import ExactToolApproval, tool_approval_store
from src.tool_approvals import (
ExactToolApproval,
document_content_digest,
tool_approval_store,
)
from src.tool_utils import _truncate, get_mcp_manager
from src.agent_tools import (
parse_tool_blocks,
@@ -5666,6 +5670,17 @@ async def stream_agent_loop(
"version_count",
None,
),
document_digest=(
document_content_digest(
getattr(
approval_document,
"current_content",
"",
)
)
if approval_document is not None
else None
),
external_untrusted_context_seen=(
run_security.external_untrusted_context_seen
),
+19 -5
View File
@@ -2,6 +2,7 @@ from typing import Any, Dict, List, Optional
import logging
import re
from src.constants import MAX_READ_CHARS
from src.tool_approvals import document_content_digest
from src.tool_utils import _parse_tool_args, get_upload_handler
from src.upload_handler import reserve_upload_references
@@ -82,14 +83,27 @@ def _most_recent_owned_document(db, Document, owner: Optional[str], active_only:
def _approved_document_version_error(doc: Any, ctx: dict) -> Optional[Dict]:
"""Reject a sealed document action when its target changed meanwhile."""
expected = ctx.get("expected_document_version")
if expected is None:
expected_version = ctx.get("expected_document_version")
expected_digest = (
str(ctx.get("expected_document_digest") or "").strip().lower()
)
if expected_version is None and not expected_digest:
return None
try:
unchanged = int(getattr(doc, "version_count", -1)) == int(expected)
version_unchanged = (
expected_version is None
or int(getattr(doc, "version_count", -1)) == int(expected_version)
)
except (TypeError, ValueError):
unchanged = False
if unchanged:
version_unchanged = False
content_unchanged = True
if expected_digest:
content_unchanged = (
doc is not None
and document_content_digest(getattr(doc, "current_content", ""))
== expected_digest
)
if version_unchanged and content_unchanged:
return None
return {
"error": (
+12
View File
@@ -43,6 +43,11 @@ def _canonical_digest(payload: dict[str, Any]) -> str:
return hashlib.sha256(encoded).hexdigest()
def document_content_digest(content: Any) -> str:
"""Return the stable server-side fingerprint used to seal a document."""
return hashlib.sha256(str(content or "").encode("utf-8")).hexdigest()
def _binding_payload(
*,
owner: Any,
@@ -53,6 +58,7 @@ def _binding_payload(
workspace: Any,
document_id: Any,
document_version: Any,
document_digest: Any,
external_untrusted_context_seen: bool,
effects: tuple[str, ...],
result_integrity: str,
@@ -68,6 +74,7 @@ def _binding_payload(
"document_version": (
int(document_version) if document_version is not None else None
),
"document_digest": str(document_digest or "").strip().lower(),
"external_untrusted_context_seen": bool(external_untrusted_context_seen),
"effects": list(effects),
"result_integrity": str(result_integrity),
@@ -85,6 +92,7 @@ class PendingToolApproval:
workspace: str
document_id: str
document_version: int | None
document_digest: str
external_untrusted_context_seen: bool
effects: tuple[str, ...]
result_integrity: str
@@ -163,6 +171,7 @@ class ExactToolApproval:
workspace=workspace,
document_id=self.pending.document_id,
document_version=self.pending.document_version,
document_digest=self.pending.document_digest,
external_untrusted_context_seen=(
self.pending.external_untrusted_context_seen
),
@@ -245,6 +254,7 @@ class ToolApprovalStore:
workspace: Any,
document_id: Any = None,
document_version: Any = None,
document_digest: Any = None,
external_untrusted_context_seen: bool,
capabilities: ToolCapabilities,
) -> PendingToolApproval:
@@ -260,6 +270,7 @@ class ToolApprovalStore:
workspace=workspace,
document_id=document_id,
document_version=document_version,
document_digest=document_digest,
external_untrusted_context_seen=external_untrusted_context_seen,
effects=effects,
result_integrity=result_integrity,
@@ -274,6 +285,7 @@ class ToolApprovalStore:
workspace=payload["workspace"],
document_id=payload["document_id"],
document_version=payload["document_version"],
document_digest=payload["document_digest"],
external_untrusted_context_seen=payload[
"external_untrusted_context_seen"
],
+10
View File
@@ -570,6 +570,7 @@ async def _document_tool_dispatch(
owner: Optional[str] = None,
document_id: Optional[str] = None,
document_version: Optional[int] = None,
document_digest: Optional[str] = None,
) -> Optional[Dict]:
"""Route a document tool through TOOL_HANDLERS with the right ctx shape."""
from src.agent_tools import TOOL_HANDLERS
@@ -578,6 +579,7 @@ async def _document_tool_dispatch(
"owner": owner,
"doc_id": document_id,
"expected_document_version": document_version,
"expected_document_digest": document_digest,
}
if tool in TOOL_HANDLERS:
return await TOOL_HANDLERS[tool](content, ctx)
@@ -645,6 +647,7 @@ async def execute_tool_block(
and (
not exact_approval.pending.document_id
or exact_approval.pending.document_version is None
or not exact_approval.pending.document_digest
)
):
return (
@@ -725,6 +728,11 @@ async def execute_tool_block(
if approval_claimed
else None
),
approved_document_digest=(
exact_approval.pending.document_digest
if approval_claimed
else None
),
)
if isinstance(security_context, ToolRunSecurityContext):
security_context.observe_tool_result(
@@ -746,6 +754,7 @@ async def _execute_tool_block_impl(
tool_policy: Optional[Any] = None,
approved_document_id: Optional[str] = None,
approved_document_version: Optional[int] = None,
approved_document_digest: Optional[str] = None,
) -> Tuple[str, Dict]:
"""Execute a single tool block. Returns (description, result_dict).
@@ -914,6 +923,7 @@ async def _execute_tool_block_impl(
owner,
document_id=approved_document_id,
document_version=approved_document_version,
document_digest=approved_document_digest,
) \
or {"error": f"{tool}: execution failed", "exit_code": 1}
if tool in ("edit_document", "suggest_document") and "title" in (result or {}):