mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
core/tools: add READ_ONLY tool attribute
New ToolTag.READ_ONLY marks tools that never mutate host state, plus ToolRegistry.clone(read_only_only=True) to select just those (for safe side turns / planning). Builtin read-only set: read_file, get_truncated, glob_paths, grep_files, listdir, tree, todo_list, wait, ask_user_*, vcs_* and fetch. fetch is tagged READ_ONLY but can mutate via method; a new read_only_context ContextVar makes it accept GET only while set.
This commit is contained in:
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from plyngent.agent import ToolTag, tool
|
||||
from plyngent.agent import ToolRegistry, ToolTag, tool
|
||||
from plyngent.tools import default_tool_definitions, register_builtin_tools
|
||||
from plyngent.tools.catalog import ToolCatalog, ToolSource, catalog_scope, get_catalog, registration_source
|
||||
from plyngent.tools.chat import CHAT_TOOLS
|
||||
@@ -42,6 +42,45 @@ def test_public_surface_is_subset() -> None:
|
||||
assert "read_file" not in public
|
||||
|
||||
|
||||
def test_read_only_classification() -> None:
|
||||
"""READ_ONLY marks exactly the non-mutating builtins."""
|
||||
register_builtin_tools()
|
||||
read_only = {t.name for t in default_tool_definitions(surface="local") if t.tags & ToolTag.READ_ONLY}
|
||||
assert read_only == {
|
||||
"read_file",
|
||||
"get_truncated",
|
||||
"glob_paths",
|
||||
"grep_files",
|
||||
"listdir",
|
||||
"tree",
|
||||
"todo_list",
|
||||
"wait",
|
||||
"ask_user_line",
|
||||
"ask_user_choice",
|
||||
"ask_user_form",
|
||||
"vcs_kind",
|
||||
"vcs_status",
|
||||
"vcs_diff",
|
||||
"vcs_log",
|
||||
"vcs_branch",
|
||||
"fetch",
|
||||
}
|
||||
|
||||
|
||||
def test_registry_clone_read_only_only() -> None:
|
||||
"""clone(read_only_only=True) keeps only READ_ONLY tools, nothing else."""
|
||||
register_builtin_tools()
|
||||
full = ToolRegistry(default_tool_definitions(surface="local"))
|
||||
ro = full.clone(read_only_only=True)
|
||||
assert 0 < len(ro) < len(full)
|
||||
assert {t.name for t in ro.definitions()} == {t.name for t in full.definitions() if t.tags & ToolTag.READ_ONLY}
|
||||
assert all(t.tags & ToolTag.READ_ONLY for t in ro.definitions())
|
||||
assert "run_argv" not in ro
|
||||
assert "edit_replace" not in ro
|
||||
# Plain clone keeps every tool.
|
||||
assert len(full.clone()) == len(full)
|
||||
|
||||
|
||||
def test_catalog_scope_empty_isolates_registration() -> None:
|
||||
with catalog_scope(empty=True) as catalog:
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, override
|
||||
|
||||
import pytest
|
||||
|
||||
from plyngent.tools.context import read_only_context
|
||||
from plyngent.tools.danger import classify_danger
|
||||
from plyngent.tools.net import fetch, grant_private_host
|
||||
from plyngent.tools.net.policy import (
|
||||
@@ -199,6 +200,23 @@ async def test_fetch_get_post_put_delete(workspace: Path, http_server: str) -> N
|
||||
assert "status=204" in out_del
|
||||
|
||||
|
||||
async def test_fetch_read_only_context_blocks_non_get(workspace: Path, http_server: str) -> None:
|
||||
"""In a read-only context fetch only allows GET; outside it POST works."""
|
||||
del workspace
|
||||
port = int(http_server.rsplit(":", 1)[1])
|
||||
grant_private_host("127.0.0.1", port)
|
||||
|
||||
with read_only_context():
|
||||
out = await call_async(fetch, f"{http_server}/hello")
|
||||
assert "status=200" in out # GET is fine in a read-only context
|
||||
blocked = await call_async(fetch, f"{http_server}/echo", method="POST", body="hi")
|
||||
assert blocked.startswith("error:")
|
||||
assert "GET" in blocked
|
||||
|
||||
out_post = await call_async(fetch, f"{http_server}/echo", method="POST", body="hi")
|
||||
assert "POST:hi" in out_post
|
||||
|
||||
|
||||
async def test_fetch_user_agent_passthrough(workspace: Path, http_server: str) -> None:
|
||||
del workspace
|
||||
port = int(http_server.rsplit(":", 1)[1])
|
||||
|
||||
Reference in New Issue
Block a user