mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 17:23:25 +08:00
feat(channel): deliver generated files on WhatsApp and honor user tool policy
Add save_deliverable_attachment for explicit document outbound, pass channel user_id into tool risk gating so write_file/run_command respect per-user allow_high settings, and document the workflow in channel-file-delivery skill. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
61d54b10fd
commit
19f4d285ce
9 changed files with 346 additions and 37 deletions
44
tests/test_channel_tool_policy.py
Normal file
44
tests/test_channel_tool_policy.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from runtime.tools.catalog import materialize_tool_specs
|
||||
from runtime.tools.public_registry import clear_public_tool_cache
|
||||
|
||||
|
||||
def test_materialize_tools_respects_user_allow_high_risk_public_tools(monkeypatch) -> None:
|
||||
clear_public_tool_cache()
|
||||
monkeypatch.delenv("AIA_PUBLIC_TOOLS_ALLOW_HIGH", raising=False)
|
||||
|
||||
class _Store:
|
||||
def get_user_workspace_path_allowlist(self, *, tenant_id: str, user_id: str):
|
||||
assert tenant_id == "tenant-a"
|
||||
assert user_id == "user-a"
|
||||
return {"allow_high_risk_public_tools": True}
|
||||
|
||||
names = {
|
||||
t.name
|
||||
for t in materialize_tool_specs(
|
||||
store=_Store(),
|
||||
path_policy_tenant_id="tenant-a",
|
||||
path_policy_user_id="user-a",
|
||||
)
|
||||
}
|
||||
assert "write_file" in names
|
||||
assert "run_command" in names
|
||||
assert "save_deliverable_attachment" in names
|
||||
|
||||
|
||||
def test_materialize_tools_blocks_high_risk_without_user_policy(monkeypatch) -> None:
|
||||
clear_public_tool_cache()
|
||||
monkeypatch.delenv("AIA_PUBLIC_TOOLS_ALLOW_HIGH", raising=False)
|
||||
|
||||
names = {
|
||||
t.name
|
||||
for t in materialize_tool_specs(
|
||||
store=None,
|
||||
path_policy_tenant_id="tenant-a",
|
||||
path_policy_user_id=None,
|
||||
)
|
||||
}
|
||||
assert "write_file" not in names
|
||||
assert "run_command" not in names
|
||||
assert "save_deliverable_attachment" in names
|
||||
|
|
@ -188,6 +188,37 @@ def test_collect_recent_tool_attachments_ignores_text_ref_from_lookup_tools() ->
|
|||
assert out == []
|
||||
|
||||
|
||||
def test_collect_recent_tool_attachments_includes_deliverable_binary_ref() -> None:
|
||||
rows = [
|
||||
_Row(role="user", content="export", attachments=None),
|
||||
_Row(
|
||||
role="tool",
|
||||
content="{}",
|
||||
attachments='[{"type":"binary_ref","attachment_id":"gen-xlsx","name":"report.xlsx","mime":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","deliverable":true}]',
|
||||
),
|
||||
_Row(role="assistant", content="done", attachments=None),
|
||||
]
|
||||
out = _collect_recent_tool_attachments(store=_FakeStore(rows), session_id="s1")
|
||||
assert len(out) == 1
|
||||
assert out[0].get("attachment_id") == "gen-xlsx"
|
||||
assert out[0].get("deliverable") is True
|
||||
|
||||
|
||||
def test_collect_recent_tool_attachments_includes_deliverable_text_ref() -> None:
|
||||
rows = [
|
||||
_Row(role="user", content="export", attachments=None),
|
||||
_Row(
|
||||
role="tool",
|
||||
content="{}",
|
||||
attachments='[{"type":"text_ref","attachment_id":"gen-txt","name":"out.txt","mime":"text/plain","deliverable":true}]',
|
||||
),
|
||||
_Row(role="assistant", content="done", attachments=None),
|
||||
]
|
||||
out = _collect_recent_tool_attachments(store=_FakeStore(rows), session_id="s1")
|
||||
assert len(out) == 1
|
||||
assert out[0].get("attachment_id") == "gen-txt"
|
||||
|
||||
|
||||
def test_maybe_add_media_path_for_wechat_reply_sets_media_path(monkeypatch) -> None:
|
||||
# Avoid touching disk: stub AttachmentAssetStore.get_local_path.
|
||||
from pathlib import Path
|
||||
|
|
|
|||
30
tests/test_save_deliverable_attachment_tool.py
Normal file
30
tests/test_save_deliverable_attachment_tool.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from runtime.tools.public.save_deliverable_attachment_tool import save_deliverable_attachment_tool
|
||||
|
||||
|
||||
def test_save_deliverable_attachment_registers_file(tmp_path, monkeypatch) -> None:
|
||||
from svc.files.attachment_assets import AttachmentAssetStore
|
||||
|
||||
store = AttachmentAssetStore(root_dir=tmp_path / "att")
|
||||
monkeypatch.setattr(
|
||||
"runtime.tools.public.save_deliverable_attachment_tool.AttachmentAssetStore",
|
||||
lambda root_dir=None: store if root_dir is None else AttachmentAssetStore(root_dir=root_dir),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"runtime.tools.public.save_deliverable_attachment_tool.resolve_workspace_path",
|
||||
lambda raw: tmp_path / "report.txt",
|
||||
)
|
||||
report = tmp_path / "report.txt"
|
||||
report.write_text("hello deliverable\n", encoding="utf-8")
|
||||
|
||||
spec = save_deliverable_attachment_tool()
|
||||
out = spec.handler({"path": "report.txt"})
|
||||
assert out.get("ok") is True
|
||||
assert out.get("deliverable") is True
|
||||
assert out.get("attachment_id")
|
||||
assert out.get("mime") == "text/plain"
|
||||
blob, meta = store.load_bytes(str(out["attachment_id"]))
|
||||
assert blob is not None
|
||||
assert blob.decode("utf-8").replace("\r\n", "\n") == "hello deliverable\n"
|
||||
assert meta is not None
|
||||
|
|
@ -3,6 +3,21 @@ from __future__ import annotations
|
|||
from runtime.chat.tool_runtime import _attachments_from_tool_result
|
||||
|
||||
|
||||
def test_attachments_from_tool_result_preserves_deliverable_flag() -> None:
|
||||
result = {
|
||||
"ok": True,
|
||||
"attachment_id": "att-doc-1",
|
||||
"mime": "text/plain",
|
||||
"name": "out.txt",
|
||||
"bytes": 12,
|
||||
"deliverable": True,
|
||||
}
|
||||
out = _attachments_from_tool_result(result)
|
||||
assert len(out) == 1
|
||||
assert out[0]["type"] == "text_ref"
|
||||
assert out[0].get("deliverable") is True
|
||||
|
||||
|
||||
def test_attachments_from_tool_result_preserves_non_image_ref_types() -> None:
|
||||
result = {
|
||||
"attachments": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue