mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:03:15 +08:00
- Rename platform/ to svc/ to avoid shadowing stdlib platform. - Replace from oclaw.* with from svc/runtime/interfaces; update -m CLI paths. - tests/conftest: prepend repo root to sys.path (no parent-folder package name). - CI: paths and offline_eval script under repo root. - Ops scripts: PYTHONPATH must be repo root for python -m runtime.* (fixes gateway/WhatsApp sidecar startup). - Fix default oclaw.json path in tabular/file attachment limits; stabilize attachment test config. Co-authored-by: Cursor <cursoragent@cursor.com>
228 lines
7.3 KiB
Python
228 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import binascii
|
|
import hashlib
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from runtime.types import SharedAttachmentManifest, SharedFilePointer
|
|
|
|
_SAFE_FILE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
|
|
_SAFE_SCOPE_ID = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
|
|
_SAFE_FILE_ID = re.compile(r"^[a-f0-9]{8,64}$")
|
|
|
|
|
|
class RelayPointerError(ValueError):
|
|
"""Raised when relay pointer payload or path is invalid."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RelayPointerLimits:
|
|
max_files: int = 32
|
|
max_file_bytes: int = 8 * 1024 * 1024
|
|
max_total_bytes: int = 64 * 1024 * 1024
|
|
|
|
|
|
def validate_file_name(name: str) -> str:
|
|
s = str(name or "").strip()
|
|
if not s:
|
|
raise RelayPointerError("file_name_required")
|
|
if s in {".", ".."}:
|
|
raise RelayPointerError("file_name_invalid")
|
|
if "/" in s or "\\" in s:
|
|
raise RelayPointerError("file_name_invalid")
|
|
if not _SAFE_FILE_NAME.match(s):
|
|
raise RelayPointerError("file_name_invalid")
|
|
return s
|
|
|
|
|
|
def normalize_rel_path(rel_path: str) -> str:
|
|
raw = str(rel_path or "").strip()
|
|
if not raw:
|
|
raise RelayPointerError("rel_path_required")
|
|
p = Path(raw.replace("\\", "/"))
|
|
if p.is_absolute():
|
|
raise RelayPointerError("rel_path_absolute")
|
|
parts = [x for x in p.parts if str(x or "").strip()]
|
|
if not parts:
|
|
raise RelayPointerError("rel_path_required")
|
|
if any(x in {".", ".."} for x in parts):
|
|
raise RelayPointerError("rel_path_traversal")
|
|
return "/".join(parts)
|
|
|
|
|
|
def safe_join_under_root(root: Path, rel_path: str) -> Path:
|
|
root_resolved = Path(root).resolve()
|
|
normalized = normalize_rel_path(rel_path)
|
|
out = (root_resolved / normalized).resolve()
|
|
try:
|
|
out.relative_to(root_resolved)
|
|
except Exception as exc:
|
|
raise RelayPointerError("path_outside_root") from exc
|
|
return out
|
|
|
|
|
|
def decode_base64_payload(payload: str, *, max_bytes: int) -> bytes:
|
|
raw = str(payload or "").strip()
|
|
if not raw:
|
|
raise RelayPointerError("payload_required")
|
|
try:
|
|
data = base64.b64decode(raw, validate=True)
|
|
except (binascii.Error, ValueError) as exc:
|
|
raise RelayPointerError("payload_base64_invalid") from exc
|
|
if len(data) > int(max_bytes):
|
|
raise RelayPointerError("payload_too_large")
|
|
return data
|
|
|
|
|
|
def sha256_hex(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def build_pointer_uri(scope_id: str, file_id: str) -> str:
|
|
s = str(scope_id or "").strip()
|
|
f = str(file_id or "").strip().lower()
|
|
if not _SAFE_SCOPE_ID.match(s):
|
|
raise RelayPointerError("scope_id_invalid")
|
|
if not _SAFE_FILE_ID.match(f):
|
|
raise RelayPointerError("file_id_invalid")
|
|
return f"relay://attachments/{s}/{f}"
|
|
|
|
|
|
def parse_pointer_uri(pointer_uri: str) -> tuple[str, str]:
|
|
s = str(pointer_uri or "").strip()
|
|
prefix = "relay://attachments/"
|
|
if not s.startswith(prefix):
|
|
raise RelayPointerError("pointer_uri_invalid")
|
|
rest = s[len(prefix) :]
|
|
chunks = rest.split("/")
|
|
if len(chunks) != 2:
|
|
raise RelayPointerError("pointer_uri_invalid")
|
|
scope_id, file_id = chunks[0].strip(), chunks[1].strip().lower()
|
|
if not _SAFE_SCOPE_ID.match(scope_id):
|
|
raise RelayPointerError("scope_id_invalid")
|
|
if not _SAFE_FILE_ID.match(file_id):
|
|
raise RelayPointerError("file_id_invalid")
|
|
return scope_id, file_id
|
|
|
|
|
|
def build_manifest_from_attachment_refs(
|
|
attachments: list[dict[str, Any]] | None,
|
|
*,
|
|
scope_id: str,
|
|
source_agent: str,
|
|
created_at: str = "",
|
|
ttl_policy: str = "turn",
|
|
) -> SharedAttachmentManifest:
|
|
items = attachments if isinstance(attachments, list) else []
|
|
pointers: list[SharedFilePointer] = []
|
|
total = 0
|
|
for a in items:
|
|
if not isinstance(a, dict):
|
|
continue
|
|
aid = str(a.get("attachment_id") or "").strip().lower()
|
|
if not aid:
|
|
continue
|
|
try:
|
|
uri = build_pointer_uri(scope_id, aid)
|
|
except RelayPointerError:
|
|
continue
|
|
b = int(a.get("bytes") or 0)
|
|
total += max(0, b)
|
|
rel = str(a.get("rel_path") or f"attachments/{aid}")
|
|
pointers.append(
|
|
SharedFilePointer(
|
|
schema_version="v1",
|
|
pointer_uri=uri,
|
|
rel_path=rel,
|
|
mime_type=str(a.get("mime") or ""),
|
|
bytes=max(0, b),
|
|
sha256=str(a.get("sha256") or aid),
|
|
source_agent=str(source_agent or ""),
|
|
created_at=str(created_at or ""),
|
|
ttl_policy=str(ttl_policy or "turn"),
|
|
extra={
|
|
"attachment_id": aid,
|
|
"name": str(a.get("name") or ""),
|
|
},
|
|
)
|
|
)
|
|
return SharedAttachmentManifest(
|
|
scope_id=str(scope_id or ""),
|
|
count=len(pointers),
|
|
total_bytes=total,
|
|
pointers=tuple(pointers),
|
|
)
|
|
|
|
|
|
def validate_relay_share_envelope(raw: dict[str, Any] | None) -> tuple[bool, str, dict[str, Any]]:
|
|
if not isinstance(raw, dict):
|
|
return (False, "relay_envelope_invalid", {})
|
|
env = dict(raw)
|
|
schema = str(env.get("schema_version") or "").strip().lower()
|
|
if schema != "v1":
|
|
return (False, "relay_envelope_unsupported_version", {})
|
|
attachments = env.get("attachments")
|
|
if not isinstance(attachments, dict):
|
|
return (False, "relay_envelope_invalid", {})
|
|
manifest = SharedAttachmentManifest.from_dict(attachments)
|
|
return (True, "", {"schema_version": "v1", "attachments": manifest.to_dict()})
|
|
|
|
|
|
def summarize_relay_ttl(relay_envelope: dict[str, Any] | None) -> dict[str, int]:
|
|
out = {"total": 0, "turn": 0, "session": 0, "keep": 0, "unknown": 0}
|
|
ok, _err, norm = validate_relay_share_envelope(relay_envelope)
|
|
if not ok:
|
|
return out
|
|
ad = norm.get("attachments") if isinstance(norm, dict) else {}
|
|
ps = ad.get("pointers") if isinstance(ad, dict) else []
|
|
for p in ps if isinstance(ps, list) else []:
|
|
if not isinstance(p, dict):
|
|
continue
|
|
out["total"] += 1
|
|
ttl = str(p.get("ttl_policy") or "").strip().lower()
|
|
if ttl in ("turn", "session", "keep"):
|
|
out[ttl] += 1
|
|
else:
|
|
out["unknown"] += 1
|
|
return out
|
|
|
|
|
|
def build_acp_relay_result(
|
|
*,
|
|
parent_run_id: str,
|
|
child_run_id: str,
|
|
relay_envelope: dict[str, Any] | None,
|
|
) -> dict[str, Any]:
|
|
ok, err, norm = validate_relay_share_envelope(relay_envelope)
|
|
out: dict[str, Any] = {
|
|
"acp_parent_run_id": str(parent_run_id or ""),
|
|
"acp_child_run_id": str(child_run_id or ""),
|
|
"relay_ok": bool(ok),
|
|
"relay_error_code": str(err or ""),
|
|
"relay_share_envelope": norm if ok else {},
|
|
}
|
|
ad = norm.get("attachments") if isinstance(norm, dict) else {}
|
|
ps = ad.get("pointers") if isinstance(ad, dict) else []
|
|
out["relay_pointer_count"] = len(ps) if isinstance(ps, list) else 0
|
|
return out
|
|
|
|
|
|
__all__ = [
|
|
"RelayPointerError",
|
|
"RelayPointerLimits",
|
|
"validate_file_name",
|
|
"normalize_rel_path",
|
|
"safe_join_under_root",
|
|
"decode_base64_payload",
|
|
"sha256_hex",
|
|
"build_pointer_uri",
|
|
"parse_pointer_uri",
|
|
"build_manifest_from_attachment_refs",
|
|
"validate_relay_share_envelope",
|
|
"summarize_relay_ttl",
|
|
"build_acp_relay_result",
|
|
]
|