Harden attachment access controls and stabilize the Weixin bridge.

This persists referenced media safely, adds explicit attachment ACL/backfill flows, and fixes the Weixin sidecar so official login state can bridge reliably into oclaw without duplicate replays or dropped replies.

Made-with: Cursor
This commit is contained in:
oliver 2026-04-28 15:38:55 +08:00
parent 6cfaff06f6
commit 31e3962c74
22 changed files with 1885 additions and 55 deletions

View file

@ -5,6 +5,7 @@ Persistence is untouched; callers use copies when building model context."""
from __future__ import annotations
import base64
import os
from typing import Any
from oclaw.platform.files.attachment_assets import AttachmentAssetStore
@ -12,6 +13,7 @@ _IMAGE_CONTENT_TYPES = frozenset({"image", "input_image"})
_BASE64_PAYLOAD_KEYS = ("data", "image_base64", "base64", "content_base64", "body_base64")
# Below this length we keep values (tiny icons / markers).
_MIN_B64_CHARS = 200
_DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024
def redact_embedded_image_blobs(obj: Any) -> Any:
@ -89,6 +91,19 @@ def ingest_embedded_image_blobs_as_refs(
if raw:
blob = _decode_image_bytes(raw)
if blob:
max_bytes = _max_attachment_bytes()
if max_bytes > 0 and len(blob) > max_bytes:
redacted = _redact_dict(node)
redacted["type"] = _ref_type_for_mime(
str(node.get("mime") or node.get("mime_type") or "application/octet-stream"),
typ,
)
redacted["error"] = "attachment_too_large"
redacted["max_bytes"] = int(max_bytes)
redacted["actual_bytes"] = int(len(blob))
redacted.setdefault("name", str(node.get("name") or "attachment"))
redacted.setdefault("mime", str(node.get("mime") or node.get("mime_type") or "application/octet-stream"))
return redacted
idx_seed[0] += 1
mime = str(node.get("mime") or node.get("mime_type") or "image/png").strip() or "image/png"
ext = _filename_ext_for_mime(mime)
@ -195,4 +210,14 @@ def _safe_int(raw: Any) -> int | None:
return None
def _max_attachment_bytes() -> int:
raw = str(os.getenv("AIA_MAX_ATTACHMENT_BYTES") or "").strip()
if raw.isdigit():
n = int(raw)
if n <= 0:
return 0
return min(n, 500 * 1024 * 1024)
return _DEFAULT_MAX_ATTACHMENT_BYTES
__all__ = ["redact_embedded_image_blobs", "ingest_embedded_image_blobs_as_refs"]

View file

@ -889,6 +889,28 @@ class ToolExecutor:
event_type="tool_result",
event_payload={"tool_name": tc.name, "observed_rows": int(observed_rows_this_call)},
)
try:
owner = ctx.store.get_ui_session_owner(session_id=ctx.session_id) or {}
tid = str(owner.get("tenant_id") or "").strip()
uid = str(owner.get("user_id") or "").strip()
atts = msg_row.attachments
if tid and uid and atts:
raw = json.loads(atts) if isinstance(atts, str) else atts
items = raw if isinstance(raw, list) else ([raw] if isinstance(raw, dict) else [])
for a in items:
if not isinstance(a, dict):
continue
aid = str(a.get("attachment_id") or "").strip().lower()
if aid:
ctx.store.link_attachment_acl(
tenant_id=tid,
user_id=uid,
session_id=ctx.session_id,
attachment_id=aid,
source=f"tool:{str(tc.name or '')}",
)
except Exception:
pass
tool_msg_write_ms = int((time.perf_counter() - t_db2) * 1000)
tool_messages.append({"role": "tool", "tool_call_id": tc.id, "content": tool_content, "name": tc.name})
_trace(