agent-portal-2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.continue/agents/new-config.yaml +22 -0
- package/AGENT_STEERING.md +36 -0
- package/ARCHITECTURE.md +13 -0
- package/CHANGELOG.md +97 -0
- package/CLI.md +38 -0
- package/CONTRIBUTING.md +55 -0
- package/INSTALLATION.md +58 -0
- package/LICENSE +60 -0
- package/PLUGIN_SYSTEM.md +33 -0
- package/PYTHON_SDK.md +22 -0
- package/QUICKSTART.md +19 -0
- package/README.md +385 -0
- package/RELEASE_NOTES_v0.1.0.md +281 -0
- package/ROADMAP.md +3 -0
- package/RUNTIME.md +44 -0
- package/SAFETY_MODEL.md +24 -0
- package/TESTING.md +35 -0
- package/TROUBLESHOOTING.md +30 -0
- package/UPGRADE_GUIDE.md +288 -0
- package/VS_CODE_EXTENSION.md +47 -0
- package/agent-portal.config.json +20 -0
- package/apps/desktop/agent-portal-desktop.zip +0 -0
- package/apps/desktop/fixtures/local-workflow.html +151 -0
- package/apps/desktop/package.json +18 -0
- package/apps/desktop/src/main.ts +117 -0
- package/apps/desktop/tsconfig.json +8 -0
- package/apps/vscode-extension/LICENSE +60 -0
- package/apps/vscode-extension/README.md +20 -0
- package/apps/vscode-extension/media/agent-portal-logo.png +0 -0
- package/apps/vscode-extension/package.json +149 -0
- package/apps/vscode-extension/src/extension.ts +614 -0
- package/apps/vscode-extension/tsconfig.json +12 -0
- package/assets/branding/agent-portal-logo.png +0 -0
- package/connectors/chatgpt-tools/README.md +9 -0
- package/connectors/claude-mcp-server/README.md +9 -0
- package/connectors/gemini-connector/README.md +9 -0
- package/connectors/rest-websocket-api/README.md +9 -0
- package/docs/MCP_SERVER.md +68 -0
- package/docs/architecture.md +214 -0
- package/docs/roadmap.md +125 -0
- package/package.json +21 -0
- package/packages/agent-portal-mcp/README.md +12 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/__init__.py +3 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/bridge/__init__.py +1 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/bridge/runtime_client.py +180 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/cli.py +32 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/doctor.py +71 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/schemas/__init__.py +1 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/schemas/actions.py +17 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/schemas/results.py +24 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/schemas/risk.py +20 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/security/__init__.py +1 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/security/policy.py +27 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/server.py +148 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tool_registry.py +58 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/__init__.py +1 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/browser.py +89 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/common.py +98 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/inspection.py +93 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/navigation.py +93 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/reports.py +34 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/steering.py +93 -0
- package/packages/agent-portal-mcp/pyproject.toml +20 -0
- package/packages/agent-portal-mcp/tests/test_doctor.py +20 -0
- package/packages/agent-portal-mcp/tests/test_mcp_server.py +161 -0
- package/packages/core/package.json +15 -0
- package/packages/core/src/index.ts +1842 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/mcp-server/package.json +15 -0
- package/packages/mcp-server/src/index.ts +73 -0
- package/packages/mcp-server/tsconfig.json +8 -0
- package/packages/sdk/package.json +15 -0
- package/packages/sdk/src/index.ts +544 -0
- package/packages/sdk/tsconfig.json +8 -0
- package/plugins/README.md +16 -0
- package/plugins/agent-portal-browser/plugin.json +19 -0
- package/plugins/agent-portal-python/plugin.json +16 -0
- package/plugins/agent-portal-skills/plugin.json +19 -0
- package/plugins/agent-portal-vscode/plugin.json +27 -0
- package/plugins/example-runtime-plugin/README.md +3 -0
- package/plugins/example-runtime-plugin/plugin.json +20 -0
- package/plugins/plugin.schema.json +53 -0
- package/python/README.md +18 -0
- package/python/agent_portal/__init__.py +5 -0
- package/python/agent_portal/__main__.py +5 -0
- package/python/agent_portal/browser.py +393 -0
- package/python/agent_portal/cli.py +164 -0
- package/python/agent_portal/config.py +31 -0
- package/python/agent_portal/doctor.py +165 -0
- package/python/agent_portal/exceptions.py +39 -0
- package/python/agent_portal/logging_utils.py +33 -0
- package/python/agent_portal/metrics.py +309 -0
- package/python/agent_portal/models.py +160 -0
- package/python/agent_portal/plugin_system.py +42 -0
- package/python/agent_portal/rate_limit.py +253 -0
- package/python/agent_portal/runtime.py +739 -0
- package/python/agent_portal/server.py +351 -0
- package/python/agent_portal/validation.py +299 -0
- package/python/pyproject.toml +29 -0
- package/python/tests/test_config.py +24 -0
- package/python/tests/test_doctor.py +19 -0
- package/python/tests/test_metrics.py +180 -0
- package/python/tests/test_rate_limit.py +237 -0
- package/python/tests/test_runtime.py +122 -0
- package/python/tests/test_server.py +53 -0
- package/python/tests/test_validation.py +170 -0
- package/releases/desktop/agent-portal-desktop/README.md +378 -0
- package/releases/desktop/agent-portal-desktop/RELEASE_NOTES.md +14 -0
- package/releases/desktop/agent-portal-desktop/assets/branding/agent-portal-logo.png +0 -0
- package/releases/desktop/agent-portal-desktop/fixtures/local-workflow.html +151 -0
- package/releases/desktop/agent-portal-desktop/launch-agent-portal.bat +4 -0
- package/releases/desktop/agent-portal-desktop.zip +0 -0
- package/releases/python/agent_portal-0.0.2-py3-none-any.whl +0 -0
- package/releases/python/agent_portal-0.0.2.tar.gz +0 -0
- package/scripts/package_desktop.mjs +117 -0
- package/scripts/release_python.py +46 -0
- package/tests/plugin-manifest.test.mjs +26 -0
- package/tests/runtime.test.mjs +41 -0
- package/tests/vscode-extension.test.mjs +22 -0
- package/tsconfig.base.json +16 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient, RuntimeClientError
|
|
9
|
+
from agent_portal_mcp.tool_registry import build_tool_registry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class DoctorCheck:
|
|
14
|
+
name: str
|
|
15
|
+
status: Literal["passed", "warning", "failed"]
|
|
16
|
+
details: str
|
|
17
|
+
suggested_fix: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class DoctorResult:
|
|
22
|
+
checks: list[DoctorCheck] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_doctor(runtime_url: str = "http://127.0.0.1:8765") -> DoctorResult:
|
|
26
|
+
result = DoctorResult()
|
|
27
|
+
result.checks.append(_check_python_version())
|
|
28
|
+
result.checks.append(_check_package_installed())
|
|
29
|
+
result.checks.append(_check_localhost_binding(runtime_url))
|
|
30
|
+
result.checks.append(_check_tools_registered())
|
|
31
|
+
result.checks.append(_check_runtime(runtime_url))
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _check_python_version() -> DoctorCheck:
|
|
36
|
+
if sys.version_info >= (3, 10):
|
|
37
|
+
return DoctorCheck("python-version", "passed", sys.version.split()[0])
|
|
38
|
+
return DoctorCheck("python-version", "failed", sys.version.split()[0], "Use Python 3.10 or newer.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _check_package_installed() -> DoctorCheck:
|
|
42
|
+
if importlib.util.find_spec("agent_portal_mcp") is not None:
|
|
43
|
+
return DoctorCheck("mcp-package", "passed", "Installed")
|
|
44
|
+
return DoctorCheck("mcp-package", "warning", "Not installed into current environment", "Install with `pip install -e ./packages/agent-portal-mcp`.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check_localhost_binding(runtime_url: str) -> DoctorCheck:
|
|
48
|
+
if runtime_url.startswith("http://127.0.0.1") or runtime_url.startswith("http://localhost"):
|
|
49
|
+
return DoctorCheck("localhost-binding", "passed", runtime_url)
|
|
50
|
+
return DoctorCheck("localhost-binding", "warning", runtime_url, "Prefer localhost-only runtime URLs for MCP usage.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _check_tools_registered() -> DoctorCheck:
|
|
54
|
+
count = len(build_tool_registry())
|
|
55
|
+
return DoctorCheck("tool-registry", "passed", f"{count} tools registered")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _check_runtime(runtime_url: str) -> DoctorCheck:
|
|
59
|
+
client = AgentPortalRuntimeClient(runtime_url=runtime_url)
|
|
60
|
+
try:
|
|
61
|
+
health = client.health()
|
|
62
|
+
except RuntimeClientError:
|
|
63
|
+
return DoctorCheck(
|
|
64
|
+
"runtime-reachable",
|
|
65
|
+
"failed",
|
|
66
|
+
runtime_url,
|
|
67
|
+
"Agent Portal runtime is not running. Start it with: agent-portal start",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
version = str(health.get("runtimeVersion", "unknown"))
|
|
71
|
+
return DoctorCheck("runtime-reachable", "passed", f"{runtime_url} (runtime {version})")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .risk import RiskLevel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class McpAction:
|
|
11
|
+
tool: str
|
|
12
|
+
runtime_action_type: str
|
|
13
|
+
reason: str
|
|
14
|
+
target: str | None = None
|
|
15
|
+
payload: str | None = None
|
|
16
|
+
risk_hint: RiskLevel = "low"
|
|
17
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .risk import RiskLevel, ToolStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class ToolResult:
|
|
11
|
+
ok: bool
|
|
12
|
+
tool: str
|
|
13
|
+
session_id: str | None
|
|
14
|
+
action_id: str | None
|
|
15
|
+
status: ToolStatus
|
|
16
|
+
risk: RiskLevel
|
|
17
|
+
message: str
|
|
18
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
screenshot_before: str | None = None
|
|
20
|
+
screenshot_after: str | None = None
|
|
21
|
+
errors: list[str] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, Any]:
|
|
24
|
+
return asdict(self)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
RiskLevel = Literal["safe", "low", "medium", "high", "blocked"]
|
|
7
|
+
ToolStatus = Literal[
|
|
8
|
+
"completed",
|
|
9
|
+
"pending_approval",
|
|
10
|
+
"rejected",
|
|
11
|
+
"blocked",
|
|
12
|
+
"failed",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
RISK_ORDER: list[RiskLevel] = ["safe", "low", "medium", "high", "blocked"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compare_risk(left: RiskLevel, right: RiskLevel) -> int:
|
|
20
|
+
return RISK_ORDER.index(left) - RISK_ORDER.index(right)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from agent_portal_mcp.schemas.risk import RiskLevel, compare_risk
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class RuntimePolicySnapshot:
|
|
10
|
+
mode: str = "assisted"
|
|
11
|
+
approval_threshold: RiskLevel = "medium"
|
|
12
|
+
read_only: bool = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class McpPolicy:
|
|
16
|
+
def should_auto_execute(self, risk: RiskLevel, snapshot: RuntimePolicySnapshot) -> bool:
|
|
17
|
+
if risk == "blocked":
|
|
18
|
+
return False
|
|
19
|
+
if risk == "high":
|
|
20
|
+
return False
|
|
21
|
+
if snapshot.read_only and risk != "safe":
|
|
22
|
+
return False
|
|
23
|
+
if compare_risk(risk, snapshot.approval_threshold) >= 0 and risk != "safe":
|
|
24
|
+
return False
|
|
25
|
+
if snapshot.mode not in {"assisted", "autonomous", "manual-override", "read-only"}:
|
|
26
|
+
return False
|
|
27
|
+
return True
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient, RuntimeClientError
|
|
9
|
+
from agent_portal_mcp.schemas.results import ToolResult
|
|
10
|
+
from agent_portal_mcp.tool_registry import ToolDefinition, build_tool_registry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentPortalMcpServer:
|
|
14
|
+
def __init__(self, runtime_url: str = "http://127.0.0.1:8765") -> None:
|
|
15
|
+
self.client = AgentPortalRuntimeClient(runtime_url=runtime_url)
|
|
16
|
+
self.tools = build_tool_registry()
|
|
17
|
+
|
|
18
|
+
def serve_stdio(self) -> None:
|
|
19
|
+
while True:
|
|
20
|
+
payload = self._read_message()
|
|
21
|
+
if payload is None:
|
|
22
|
+
return
|
|
23
|
+
response = self.handle_request(payload)
|
|
24
|
+
if response is not None:
|
|
25
|
+
self._write_message(response)
|
|
26
|
+
|
|
27
|
+
def handle_request(self, payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
28
|
+
method = payload.get("method")
|
|
29
|
+
request_id = payload.get("id")
|
|
30
|
+
|
|
31
|
+
if method == "notifications/initialized":
|
|
32
|
+
return None
|
|
33
|
+
if method == "initialize":
|
|
34
|
+
return self._jsonrpc(
|
|
35
|
+
request_id,
|
|
36
|
+
{
|
|
37
|
+
"protocolVersion": "2024-11-05",
|
|
38
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
39
|
+
"serverInfo": {"name": "agent-portal-mcp", "version": "0.0.2"},
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
if method == "ping":
|
|
43
|
+
return self._jsonrpc(request_id, {})
|
|
44
|
+
if method == "tools/list":
|
|
45
|
+
return self._jsonrpc(
|
|
46
|
+
request_id,
|
|
47
|
+
{
|
|
48
|
+
"tools": [
|
|
49
|
+
{
|
|
50
|
+
"name": tool.name,
|
|
51
|
+
"description": tool.description,
|
|
52
|
+
"inputSchema": tool.input_schema,
|
|
53
|
+
}
|
|
54
|
+
for tool in self.tools.values()
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
if method == "tools/call":
|
|
59
|
+
params = payload.get("params", {})
|
|
60
|
+
name = str(params.get("name", ""))
|
|
61
|
+
arguments = params.get("arguments", {})
|
|
62
|
+
if not isinstance(arguments, dict):
|
|
63
|
+
arguments = {}
|
|
64
|
+
result = self.call_tool(name, arguments)
|
|
65
|
+
return self._jsonrpc(
|
|
66
|
+
request_id,
|
|
67
|
+
{
|
|
68
|
+
"content": [{"type": "text", "text": json.dumps(result.to_dict(), indent=2)}],
|
|
69
|
+
"structuredContent": result.to_dict(),
|
|
70
|
+
"isError": not result.ok,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return self._error(request_id, -32601, f"Method not found: {method}")
|
|
75
|
+
|
|
76
|
+
def call_tool(self, name: str, arguments: dict[str, Any]) -> ToolResult:
|
|
77
|
+
definition = self.tools.get(name)
|
|
78
|
+
if definition is None:
|
|
79
|
+
return ToolResult(
|
|
80
|
+
ok=False,
|
|
81
|
+
tool=name,
|
|
82
|
+
session_id=None,
|
|
83
|
+
action_id=None,
|
|
84
|
+
status="failed",
|
|
85
|
+
risk="blocked",
|
|
86
|
+
message=f"Unknown MCP tool: {name}",
|
|
87
|
+
errors=[f"Tool `{name}` is not registered."],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
return definition.handler(self.client, arguments)
|
|
92
|
+
except RuntimeClientError as exc:
|
|
93
|
+
return ToolResult(
|
|
94
|
+
ok=False,
|
|
95
|
+
tool=name,
|
|
96
|
+
session_id=None,
|
|
97
|
+
action_id=None,
|
|
98
|
+
status="failed",
|
|
99
|
+
risk="blocked",
|
|
100
|
+
message=str(exc),
|
|
101
|
+
errors=[str(exc)],
|
|
102
|
+
)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
return ToolResult(
|
|
105
|
+
ok=False,
|
|
106
|
+
tool=name,
|
|
107
|
+
session_id=None,
|
|
108
|
+
action_id=None,
|
|
109
|
+
status="failed",
|
|
110
|
+
risk="blocked",
|
|
111
|
+
message="MCP tool execution failed.",
|
|
112
|
+
errors=[self._redact(str(exc))],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _read_message(self) -> dict[str, Any] | None:
|
|
116
|
+
headers: dict[str, str] = {}
|
|
117
|
+
while True:
|
|
118
|
+
line = sys.stdin.buffer.readline()
|
|
119
|
+
if not line:
|
|
120
|
+
return None
|
|
121
|
+
if line in {b"\r\n", b"\n"}:
|
|
122
|
+
break
|
|
123
|
+
key, value = line.decode("utf8").split(":", 1)
|
|
124
|
+
headers[key.strip().lower()] = value.strip()
|
|
125
|
+
|
|
126
|
+
length = int(headers.get("content-length", "0"))
|
|
127
|
+
if length <= 0:
|
|
128
|
+
return None
|
|
129
|
+
body = sys.stdin.buffer.read(length)
|
|
130
|
+
return json.loads(body.decode("utf8"))
|
|
131
|
+
|
|
132
|
+
def _write_message(self, payload: dict[str, Any]) -> None:
|
|
133
|
+
encoded = json.dumps(payload).encode("utf8")
|
|
134
|
+
sys.stdout.buffer.write(f"Content-Length: {len(encoded)}\r\n\r\n".encode("utf8"))
|
|
135
|
+
sys.stdout.buffer.write(encoded)
|
|
136
|
+
sys.stdout.buffer.flush()
|
|
137
|
+
|
|
138
|
+
def _jsonrpc(self, request_id: Any, result: dict[str, Any]) -> dict[str, Any]:
|
|
139
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
140
|
+
|
|
141
|
+
def _error(self, request_id: Any, code: int, message: str) -> dict[str, Any]:
|
|
142
|
+
return {"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}}
|
|
143
|
+
|
|
144
|
+
def _redact(self, value: str) -> str:
|
|
145
|
+
token = self.client.token
|
|
146
|
+
if token:
|
|
147
|
+
return value.replace(token, "[REDACTED]")
|
|
148
|
+
return value
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient
|
|
7
|
+
from agent_portal_mcp.schemas.results import ToolResult
|
|
8
|
+
from agent_portal_mcp.tools import browser, inspection, navigation, reports, steering
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ToolHandler = Callable[[AgentPortalRuntimeClient, dict[str, Any]], ToolResult]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class ToolDefinition:
|
|
16
|
+
name: str
|
|
17
|
+
description: str
|
|
18
|
+
input_schema: dict[str, Any]
|
|
19
|
+
handler: ToolHandler
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_tool_registry() -> dict[str, ToolDefinition]:
|
|
23
|
+
definitions = [
|
|
24
|
+
ToolDefinition("browser_open", "Open a browser URL through Agent Portal.", {"type": "object", "properties": {"url": {"type": "string"}, "reason": {"type": "string"}}, "required": ["url"]}, browser.browser_open),
|
|
25
|
+
ToolDefinition("browser_close", "Close the current browser session.", {"type": "object", "properties": {"reason": {"type": "string"}}}, browser.browser_close),
|
|
26
|
+
ToolDefinition("browser_refresh", "Refresh the current page.", {"type": "object", "properties": {"reason": {"type": "string"}}}, browser.browser_refresh),
|
|
27
|
+
ToolDefinition("browser_back", "Navigate backward in history.", {"type": "object", "properties": {"reason": {"type": "string"}}}, browser.browser_back),
|
|
28
|
+
ToolDefinition("browser_forward", "Navigate forward in history.", {"type": "object", "properties": {"reason": {"type": "string"}}}, browser.browser_forward),
|
|
29
|
+
ToolDefinition("browser_status", "Return browser session status.", {"type": "object", "properties": {}}, browser.browser_status),
|
|
30
|
+
ToolDefinition("navigate_to_url", "Navigate to a URL through the runtime.", {"type": "object", "properties": {"url": {"type": "string"}, "reason": {"type": "string"}}, "required": ["url"]}, navigation.navigate_to_url),
|
|
31
|
+
ToolDefinition("click_element", "Click an element selected by CSS, role text, or label.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector"]}, navigation.click_element),
|
|
32
|
+
ToolDefinition("type_text", "Type text into an input element.", {"type": "object", "properties": {"selector": {"type": "string"}, "text": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector", "text"]}, navigation.type_text),
|
|
33
|
+
ToolDefinition("scroll_page", "Scroll the page or bring an element into view.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}}, navigation.scroll_page),
|
|
34
|
+
ToolDefinition("hover_element", "Hover over an element.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector"]}, navigation.hover_element),
|
|
35
|
+
ToolDefinition("wait_for_page", "Wait for a page element to appear.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector"]}, navigation.wait_for_page),
|
|
36
|
+
ToolDefinition("capture_screenshot", "Capture a screenshot with evidence metadata.", {"type": "object", "properties": {"label": {"type": "string"}}}, inspection.capture_screenshot),
|
|
37
|
+
ToolDefinition("read_page_text", "Read visible text from an element.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector"]}, inspection.read_page_text),
|
|
38
|
+
ToolDefinition("read_dom", "Read the current page DOM.", {"type": "object", "properties": {}}, inspection.read_dom),
|
|
39
|
+
ToolDefinition("read_accessibility_tree", "Read the page accessibility tree.", {"type": "object", "properties": {}}, inspection.read_accessibility_tree),
|
|
40
|
+
ToolDefinition("read_console_errors", "Read collected console errors.", {"type": "object", "properties": {}}, inspection.read_console_errors),
|
|
41
|
+
ToolDefinition("read_network_failures", "Read collected network failures.", {"type": "object", "properties": {}}, inspection.read_network_failures),
|
|
42
|
+
ToolDefinition("inspect_element", "Inspect a specific element.", {"type": "object", "properties": {"selector": {"type": "string"}}, "required": ["selector"]}, inspection.inspect_element),
|
|
43
|
+
ToolDefinition("propose_action", "Propose an action through the runtime policy engine.", {"type": "object", "properties": {"action_type": {"type": "string"}, "target": {"type": "string"}, "payload": {"type": "string"}, "reason": {"type": "string"}, "risk": {"type": "string"}}, "required": ["action_type", "reason"]}, steering.propose_action),
|
|
44
|
+
ToolDefinition("approve_action", "Approve and optionally execute a queued action.", {"type": "object", "properties": {"action_id": {"type": "string"}, "execute": {"type": "boolean"}}, "required": ["action_id"]}, steering.approve_action),
|
|
45
|
+
ToolDefinition("reject_action", "Reject a queued action.", {"type": "object", "properties": {"action_id": {"type": "string"}, "reason": {"type": "string"}}, "required": ["action_id"]}, steering.reject_action),
|
|
46
|
+
ToolDefinition("pause_agent", "Pause the agent runtime.", {"type": "object", "properties": {}}, steering.pause_agent),
|
|
47
|
+
ToolDefinition("resume_agent", "Resume the agent runtime.", {"type": "object", "properties": {}}, steering.resume_agent),
|
|
48
|
+
ToolDefinition("stop_agent", "Stop the agent runtime.", {"type": "object", "properties": {}}, steering.stop_agent),
|
|
49
|
+
ToolDefinition("set_goal", "Set the current runtime goal.", {"type": "object", "properties": {"goal": {"type": "string"}}, "required": ["goal"]}, steering.set_goal),
|
|
50
|
+
ToolDefinition("get_current_goal", "Get the current runtime goal.", {"type": "object", "properties": {}}, steering.get_current_goal),
|
|
51
|
+
ToolDefinition("get_action_queue", "Get the runtime action queue.", {"type": "object", "properties": {}}, steering.get_action_queue),
|
|
52
|
+
ToolDefinition("set_action_mode", "Set the runtime action mode.", {"type": "object", "properties": {"mode": {"type": "string"}}, "required": ["mode"]}, steering.set_action_mode),
|
|
53
|
+
ToolDefinition("generate_report", "Generate a runtime report.", {"type": "object", "properties": {}}, reports.generate_report),
|
|
54
|
+
ToolDefinition("list_reports", "List available runtime reports.", {"type": "object", "properties": {}}, reports.list_reports),
|
|
55
|
+
ToolDefinition("read_report", "Read a report by name.", {"type": "object", "properties": {"report": {"type": "string"}}, "required": ["report"]}, reports.read_report),
|
|
56
|
+
ToolDefinition("export_report", "Export a report to a destination path.", {"type": "object", "properties": {"report": {"type": "string"}, "destination": {"type": "string"}}, "required": ["report"]}, reports.export_report),
|
|
57
|
+
]
|
|
58
|
+
return {definition.name: definition for definition in definitions}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient
|
|
6
|
+
from agent_portal_mcp.schemas.actions import McpAction
|
|
7
|
+
from agent_portal_mcp.schemas.results import ToolResult
|
|
8
|
+
from agent_portal_mcp.tools.common import build_result, execute_or_queue
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def browser_open(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
|
|
12
|
+
return execute_or_queue(
|
|
13
|
+
client,
|
|
14
|
+
"browser_open",
|
|
15
|
+
McpAction(
|
|
16
|
+
tool="browser_open",
|
|
17
|
+
runtime_action_type="open_url",
|
|
18
|
+
reason=str(args.get("reason", "Open browser URL")),
|
|
19
|
+
target=str(args.get("url", "")),
|
|
20
|
+
risk_hint="low",
|
|
21
|
+
),
|
|
22
|
+
lambda _: {"url": args.get("url")},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def browser_close(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
|
|
27
|
+
return execute_or_queue(
|
|
28
|
+
client,
|
|
29
|
+
"browser_close",
|
|
30
|
+
McpAction(
|
|
31
|
+
tool="browser_close",
|
|
32
|
+
runtime_action_type="browser_close",
|
|
33
|
+
reason=str(args.get("reason", "Close browser session")),
|
|
34
|
+
risk_hint="medium",
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def browser_refresh(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
|
|
40
|
+
return execute_or_queue(
|
|
41
|
+
client,
|
|
42
|
+
"browser_refresh",
|
|
43
|
+
McpAction(
|
|
44
|
+
tool="browser_refresh",
|
|
45
|
+
runtime_action_type="browser_refresh",
|
|
46
|
+
reason=str(args.get("reason", "Refresh current page")),
|
|
47
|
+
risk_hint="low",
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def browser_back(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
|
|
53
|
+
return execute_or_queue(
|
|
54
|
+
client,
|
|
55
|
+
"browser_back",
|
|
56
|
+
McpAction(
|
|
57
|
+
tool="browser_back",
|
|
58
|
+
runtime_action_type="browser_back",
|
|
59
|
+
reason=str(args.get("reason", "Navigate backward")),
|
|
60
|
+
risk_hint="low",
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def browser_forward(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
|
|
66
|
+
return execute_or_queue(
|
|
67
|
+
client,
|
|
68
|
+
"browser_forward",
|
|
69
|
+
McpAction(
|
|
70
|
+
tool="browser_forward",
|
|
71
|
+
runtime_action_type="browser_forward",
|
|
72
|
+
reason=str(args.get("reason", "Navigate forward")),
|
|
73
|
+
risk_hint="low",
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def browser_status(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
|
|
79
|
+
status = client.status()
|
|
80
|
+
browser = client.browser_status()
|
|
81
|
+
return build_result(
|
|
82
|
+
"browser_status",
|
|
83
|
+
status,
|
|
84
|
+
None,
|
|
85
|
+
"completed",
|
|
86
|
+
"safe",
|
|
87
|
+
"Returned browser status.",
|
|
88
|
+
{"browser": browser},
|
|
89
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient
|
|
6
|
+
from agent_portal_mcp.schemas.actions import McpAction
|
|
7
|
+
from agent_portal_mcp.schemas.results import ToolResult
|
|
8
|
+
from agent_portal_mcp.schemas.risk import RiskLevel
|
|
9
|
+
from agent_portal_mcp.security.policy import McpPolicy, RuntimePolicySnapshot
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def runtime_policy_snapshot(status_payload: dict[str, Any]) -> RuntimePolicySnapshot:
|
|
13
|
+
policy = status_payload.get("policy", {})
|
|
14
|
+
return RuntimePolicySnapshot(
|
|
15
|
+
mode=str(policy.get("mode", "assisted")),
|
|
16
|
+
approval_threshold=str(policy.get("approval_threshold", "medium")), # type: ignore[arg-type]
|
|
17
|
+
read_only=bool(policy.get("read_only", False)),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_result(
|
|
22
|
+
tool: str,
|
|
23
|
+
status_payload: dict[str, Any],
|
|
24
|
+
action_payload: dict[str, Any] | None,
|
|
25
|
+
status: str,
|
|
26
|
+
risk: RiskLevel,
|
|
27
|
+
message: str,
|
|
28
|
+
data: dict[str, Any] | None = None,
|
|
29
|
+
errors: list[str] | None = None,
|
|
30
|
+
) -> ToolResult:
|
|
31
|
+
session = status_payload.get("session", {})
|
|
32
|
+
return ToolResult(
|
|
33
|
+
ok=status == "completed",
|
|
34
|
+
tool=tool,
|
|
35
|
+
session_id=session.get("session_id"),
|
|
36
|
+
action_id=action_payload.get("action_id") if action_payload else None,
|
|
37
|
+
status=status, # type: ignore[arg-type]
|
|
38
|
+
risk=risk,
|
|
39
|
+
message=message,
|
|
40
|
+
data=data or {},
|
|
41
|
+
screenshot_before=action_payload.get("before_screenshot") if action_payload else None,
|
|
42
|
+
screenshot_after=action_payload.get("after_screenshot") if action_payload else None,
|
|
43
|
+
errors=errors or [],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def execute_or_queue(
|
|
48
|
+
client: AgentPortalRuntimeClient,
|
|
49
|
+
tool: str,
|
|
50
|
+
action: McpAction,
|
|
51
|
+
data_builder: Callable[[dict[str, Any] | None], dict[str, Any]] | None = None,
|
|
52
|
+
) -> ToolResult:
|
|
53
|
+
status_payload = client.status()
|
|
54
|
+
proposed = client.propose_action(
|
|
55
|
+
action.runtime_action_type,
|
|
56
|
+
action.reason,
|
|
57
|
+
target=action.target,
|
|
58
|
+
payload=action.payload,
|
|
59
|
+
risk_level=action.risk_hint,
|
|
60
|
+
)
|
|
61
|
+
risk = str(proposed.get("risk_level", action.risk_hint)) # type: ignore[assignment]
|
|
62
|
+
policy = McpPolicy()
|
|
63
|
+
snapshot = runtime_policy_snapshot(status_payload)
|
|
64
|
+
|
|
65
|
+
if proposed.get("status") == "blocked":
|
|
66
|
+
return build_result(
|
|
67
|
+
tool,
|
|
68
|
+
status_payload,
|
|
69
|
+
proposed,
|
|
70
|
+
"blocked",
|
|
71
|
+
risk, # type: ignore[arg-type]
|
|
72
|
+
"Action was blocked by the Agent Portal runtime policy engine.",
|
|
73
|
+
data_builder(proposed) if data_builder else {},
|
|
74
|
+
[str(proposed.get("result") or "blocked")],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if not policy.should_auto_execute(risk, snapshot): # type: ignore[arg-type]
|
|
78
|
+
return build_result(
|
|
79
|
+
tool,
|
|
80
|
+
status_payload,
|
|
81
|
+
proposed,
|
|
82
|
+
"pending_approval",
|
|
83
|
+
risk, # type: ignore[arg-type]
|
|
84
|
+
"Action entered the approval queue.",
|
|
85
|
+
data_builder(proposed) if data_builder else {},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
completed = client.approve_action(str(proposed.get("action_id", "")), execute=True)
|
|
89
|
+
fresh_status = client.status()
|
|
90
|
+
return build_result(
|
|
91
|
+
tool,
|
|
92
|
+
fresh_status,
|
|
93
|
+
completed,
|
|
94
|
+
"completed",
|
|
95
|
+
risk, # type: ignore[arg-type]
|
|
96
|
+
"Action executed through the Agent Portal runtime.",
|
|
97
|
+
data_builder(completed) if data_builder else {},
|
|
98
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient
|
|
6
|
+
from agent_portal_mcp.schemas.results import ToolResult
|
|
7
|
+
from agent_portal_mcp.tools.common import build_result
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def capture_screenshot(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
|
|
11
|
+
status = client.status()
|
|
12
|
+
payload = client.capture(str(args.get("label", "mcp-capture")))
|
|
13
|
+
action = payload.get("action", {})
|
|
14
|
+
return build_result(
|
|
15
|
+
"capture_screenshot",
|
|
16
|
+
status,
|
|
17
|
+
action if isinstance(action, dict) else None,
|
|
18
|
+
"completed",
|
|
19
|
+
"safe",
|
|
20
|
+
"Captured screenshot.",
|
|
21
|
+
payload,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_page_text(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
|
|
26
|
+
status = client.status()
|
|
27
|
+
payload = client.read_text(
|
|
28
|
+
str(args.get("selector", "body")),
|
|
29
|
+
str(args.get("reason", "Read page text")),
|
|
30
|
+
)
|
|
31
|
+
return build_result("read_page_text", status, payload.get("action"), "completed", "safe", "Read page text.", payload)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read_dom(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
|
|
35
|
+
status = client.status()
|
|
36
|
+
payload = client.read_dom()
|
|
37
|
+
return build_result("read_dom", status, None, "completed", "safe", "Read page DOM.", payload)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_accessibility_tree(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
|
|
41
|
+
status = client.status()
|
|
42
|
+
payload = client.read_accessibility_tree()
|
|
43
|
+
return build_result(
|
|
44
|
+
"read_accessibility_tree",
|
|
45
|
+
status,
|
|
46
|
+
None,
|
|
47
|
+
"completed",
|
|
48
|
+
"safe",
|
|
49
|
+
"Read accessibility tree.",
|
|
50
|
+
payload,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_console_errors(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
|
|
55
|
+
status = client.status()
|
|
56
|
+
payload = client.read_console_errors()
|
|
57
|
+
return build_result(
|
|
58
|
+
"read_console_errors",
|
|
59
|
+
status,
|
|
60
|
+
None,
|
|
61
|
+
"completed",
|
|
62
|
+
"safe",
|
|
63
|
+
"Read console errors.",
|
|
64
|
+
payload,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def read_network_failures(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
|
|
69
|
+
status = client.status()
|
|
70
|
+
payload = client.read_network_failures()
|
|
71
|
+
return build_result(
|
|
72
|
+
"read_network_failures",
|
|
73
|
+
status,
|
|
74
|
+
None,
|
|
75
|
+
"completed",
|
|
76
|
+
"safe",
|
|
77
|
+
"Read network failures.",
|
|
78
|
+
payload,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def inspect_element(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
|
|
83
|
+
status = client.status()
|
|
84
|
+
payload = client.inspect_element(str(args.get("selector", "")))
|
|
85
|
+
return build_result(
|
|
86
|
+
"inspect_element",
|
|
87
|
+
status,
|
|
88
|
+
None,
|
|
89
|
+
"completed",
|
|
90
|
+
"safe",
|
|
91
|
+
"Inspected element.",
|
|
92
|
+
payload,
|
|
93
|
+
)
|