Files
OpenMesh/tests/test_run_grant.py
zhaolei 6f402ffcee
Some checks failed
CI / pytest (push) Has been cancelled
CI / gui-unit (push) Has been cancelled
CI / gui-e2e (push) Has been cancelled
feat: OpenMesh 基础平台与 MD/PDF 转换技能
- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理
- 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类)
- 技能: md-to-office (pandoc + wkhtmltopdf)
- 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/
- 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
2026-09-13 23:41:04 +08:00

203 lines
8.0 KiB
Python

"""OPE-136 "Allow for this request" — the run grant: covers the exact tool for the
remainder of the current run, in memory only, cleared at the run boundary.
The design ladder it slots into: once → THIS RUN → (mode switch) → always.
It exists because the EXTERNAL ladder was once-or-forever, and approval fatigue
drained into permanent trust rules (observed in owner testing 2026-08-30).
"""
from __future__ import annotations
from types import SimpleNamespace
from coworker.permissions import Mode, PermissionEngine
MCP_META = SimpleNamespace(requires_approval=True, category="mcp")
CONNECTOR_META = SimpleNamespace(requires_approval=True, category="connector")
MCP_TOOL = "mcp__atlassian__searchJiraIssuesUsingJql"
def engine(mode: Mode, tmp_path) -> PermissionEngine:
return PermissionEngine(workspace_root=tmp_path, mode=mode)
# -- the grant covers the tool, and only until cleared ----------------------------
def test_run_grant_allows_then_dies_at_the_boundary(tmp_path):
eng = engine(Mode.INTERACTIVE, tmp_path)
assert not eng.evaluate(MCP_TOOL, {}, MCP_META).allowed # asks before the grant
eng.allow_tool_for_run(MCP_TOOL)
d = eng.evaluate(MCP_TOOL, {}, MCP_META)
assert d.allowed and d.reason == "tool allowed for this request"
# The run boundary IS the expiry: after clearing, the same call asks again.
eng.clear_run_allowances()
assert not eng.evaluate(MCP_TOOL, {}, MCP_META).allowed
def test_run_grant_covers_connectors_unlike_the_session_grant(tmp_path):
# The session grant excludes connectors; the run grant exists FOR the EXTERNAL
# family — connector loops ("email these five people") are its main scenario.
eng = engine(Mode.INTERACTIVE, tmp_path)
eng.allow_tool_for_session("gmail_send_email")
assert not eng.evaluate("gmail_send_email", {}, CONNECTOR_META).allowed
eng.allow_tool_for_run("gmail_send_email")
d = eng.evaluate("gmail_send_email", {}, CONNECTOR_META)
assert d.allowed and d.reason == "tool allowed for this request"
def test_run_grant_never_skips_the_auto_approve_judge(tmp_path):
# §1.5: an in-flow click may not skip the reviewer — same law as session grants.
eng = engine(Mode.AUTO_APPROVE, tmp_path)
eng.allow_tool_for_run(MCP_TOOL)
d = eng.evaluate(MCP_TOOL, {}, MCP_META)
assert not d.allowed and d.needs_user and not d.human_only
def test_run_grant_cannot_override_a_read_only_mode(tmp_path):
# Grants only ever turn an "ask" into an "allow" — Discuss's fence stands.
eng = engine(Mode.DISCUSS, tmp_path)
eng.allow_tool_for_run(MCP_TOOL)
d = eng.evaluate(MCP_TOOL, {}, MCP_META)
assert not d.allowed and not d.needs_user
# -- server side: offered ONLY for the EXTERNAL family ----------------------------
def test_this_run_grant_is_external_only():
from coworker.engine import ApprovalOutcome
from coworker.server.manager import _grant_offered
def req(name: str, category: str = "", approval: bool = False, args: dict | None = None):
return SimpleNamespace(
tool_name=name,
metadata=SimpleNamespace(requires_approval=approval, category=category)
if category or approval
else None,
arguments=args or {},
)
offered = ApprovalOutcome.THIS_RUN
assert _grant_offered(offered, req("mcp__srv__tool", "mcp", approval=True))
assert _grant_offered(offered, req("gmail_send_email", "connector", approval=True))
# Core messaging classifies EXTERNAL via its metadata, as wired in production.
assert _grant_offered(offered, req("send_message", approval=True))
# EXEC: tool-wide shell is a blank check at any duration.
assert not _grant_offered(offered, req("run_shell"))
# EGRESS: the domain-scoped grant is the honest instrument there.
assert not _grant_offered(offered, req("web_fetch", args={"url": "https://x.y"}))
# Local writes have the session grant; the run rung isn't theirs.
assert not _grant_offered(offered, req("write_file"))
def test_approval_outcome_downgrades_unoffered_grants_to_once():
# POST /v1/inbox/{id}/resolve takes a raw string — the server validates every
# grant vocabulary, including this_run and (gap closed) always_trust.
from coworker.engine import ApprovalOutcome
from coworker.server.manager import SessionManager
refused: list[str] = []
fake = SimpleNamespace(
_audit_grant_refused=lambda _s, _r, res: refused.append(res),
mint_task_rule=lambda *_a, **_k: False,
)
shell_req = SimpleNamespace(tool_name="run_shell", metadata=None, arguments={})
out = SessionManager.approval_outcome(fake, "this_run", shell_req, "s1")
assert out is ApprovalOutcome.ONCE and refused == ["this_run"]
trust_req = SimpleNamespace(tool_name="write_file", metadata=None, arguments={})
out = SessionManager.approval_outcome(fake, "always_trust", trust_req, "s1")
assert out is ApprovalOutcome.ONCE and refused[-1] == "always_trust"
mcp_req = SimpleNamespace(
tool_name="mcp__srv__tool",
metadata=SimpleNamespace(requires_approval=True, category="mcp"),
arguments={},
)
assert (
SessionManager.approval_outcome(fake, "this_run", mcp_req, "s1")
is ApprovalOutcome.THIS_RUN
)
# -- end to end: one card per run, covered calls chip-annotated, boundary honored --
def test_engine_end_to_end_one_card_per_run(tmp_path):
"""The whole promise in one flow: THIS_RUN from the card covers the SAME tool's
later calls in the same run (no second card), stamps the covered call with the
run_grant origin for the transcript chip, and the next run() asks again."""
import asyncio
import aisuite as ai
from coworker.engine import ApprovalOutcome, TurnEngine
from coworker.providers import (
AssistantTurn,
ModelCapabilities,
ProviderClient,
ToolCall,
)
from coworker.tools import ToolRegistry
class Scripted(ProviderClient):
def __init__(self, turns):
self._turns = list(turns)
def complete(self, *, model, messages, tools=None, **settings):
return self._turns.pop(0)
def capabilities(self, model):
return ModelCapabilities()
def tool_turn(call_id: str) -> AssistantTurn:
return AssistantTurn(
tool_calls=[
ToolCall(id=call_id, name="mcp__srv__search", arguments={"q": "x"})
],
finish_reason="tool_calls",
)
def done(text: str) -> AssistantTurn:
return AssistantTurn(text=text, finish_reason="stop")
asks: list[str] = []
async def approver(req):
asks.append(req.tool_name)
return ApprovalOutcome.THIS_RUN
def mcp__srv__search(q: str = ""):
"""Fake MCP search."""
return {"ok": True}
registry = ToolRegistry()
registry.register(
mcp__srv__search,
metadata=ai.ToolMetadata(category="mcp", requires_approval=True),
)
engine = TurnEngine(
provider=Scripted(
# Run 1: the tool twice, then done. Run 2: the tool once more, then done.
[tool_turn("c1"), tool_turn("c2"), done("done"), tool_turn("c3"), done("ok")]
),
registry=registry,
permissions=PermissionEngine(workspace_root=tmp_path),
model="gpt-5.5",
approver=approver,
)
async def scenario():
events_one = [ev async for ev in engine.run("find things")]
# ONE card for two calls: c1 asked, c2 rode the grant.
assert asks == ["mcp__srv__search"]
# The covered call is chip-annotated — silent to attention, never invisible.
origins = [
m.get("_display", {}).get("approval_origin")
for m in engine.messages
if m.get("role") == "tool"
]
assert "run_grant" in origins
events_two = [ev async for ev in engine.run("find more")]
# The run boundary held: the very same tool asks again in the next run.
assert asks == ["mcp__srv__search", "mcp__srv__search"]
return events_one, events_two
asyncio.run(scenario())