meshagent-cli 0.22.2__py3-none-any.whl
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.
Potentially problematic release.
This version of meshagent-cli might be problematic. Click here for more details.
- meshagent/cli/__init__.py +3 -0
- meshagent/cli/agent.py +273 -0
- meshagent/cli/api_keys.py +102 -0
- meshagent/cli/async_typer.py +79 -0
- meshagent/cli/auth.py +30 -0
- meshagent/cli/auth_async.py +295 -0
- meshagent/cli/call.py +215 -0
- meshagent/cli/chatbot.py +1983 -0
- meshagent/cli/cli.py +187 -0
- meshagent/cli/cli_mcp.py +408 -0
- meshagent/cli/cli_secrets.py +414 -0
- meshagent/cli/common_options.py +47 -0
- meshagent/cli/containers.py +725 -0
- meshagent/cli/database.py +997 -0
- meshagent/cli/developer.py +70 -0
- meshagent/cli/exec.py +397 -0
- meshagent/cli/helper.py +236 -0
- meshagent/cli/helpers.py +185 -0
- meshagent/cli/host.py +41 -0
- meshagent/cli/mailbot.py +1295 -0
- meshagent/cli/mailboxes.py +223 -0
- meshagent/cli/meeting_transcriber.py +138 -0
- meshagent/cli/messaging.py +157 -0
- meshagent/cli/multi.py +357 -0
- meshagent/cli/oauth2.py +341 -0
- meshagent/cli/participant_token.py +63 -0
- meshagent/cli/port.py +70 -0
- meshagent/cli/projects.py +105 -0
- meshagent/cli/queue.py +91 -0
- meshagent/cli/room.py +26 -0
- meshagent/cli/rooms.py +214 -0
- meshagent/cli/services.py +722 -0
- meshagent/cli/sessions.py +26 -0
- meshagent/cli/storage.py +813 -0
- meshagent/cli/sync.py +434 -0
- meshagent/cli/task_runner.py +1317 -0
- meshagent/cli/version.py +1 -0
- meshagent/cli/voicebot.py +624 -0
- meshagent/cli/webhook.py +100 -0
- meshagent/cli/worker.py +1403 -0
- meshagent_cli-0.22.2.dist-info/METADATA +49 -0
- meshagent_cli-0.22.2.dist-info/RECORD +45 -0
- meshagent_cli-0.22.2.dist-info/WHEEL +5 -0
- meshagent_cli-0.22.2.dist-info/entry_points.txt +2 -0
- meshagent_cli-0.22.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import secrets
|
|
7
|
+
import webbrowser
|
|
8
|
+
import asyncio
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.parse import urlencode
|
|
11
|
+
from aiohttp import web, ClientSession
|
|
12
|
+
|
|
13
|
+
# -----------------------------------------------------------------------------
|
|
14
|
+
# Config
|
|
15
|
+
# -----------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
CACHE_FILE = Path.home() / ".meshagent" / "session.json"
|
|
18
|
+
REDIRECT_PORT = 8765
|
|
19
|
+
REDIRECT_URL = f"http://localhost:{REDIRECT_PORT}/callback"
|
|
20
|
+
|
|
21
|
+
# Expected env vars:
|
|
22
|
+
# - MESHAGENT_API_URL (required): e.g., https://api.meshagent.com
|
|
23
|
+
# - MESHAGENT_OAUTH_CLIENT_ID (required)
|
|
24
|
+
# - MESHAGENT_OAUTH_CLIENT_SECRET (optional; only if your server requires it)
|
|
25
|
+
# - MESHAGENT_OAUTH_SCOPES (optional; defaults to "openid email profile")
|
|
26
|
+
|
|
27
|
+
# -----------------------------------------------------------------------------
|
|
28
|
+
# Helpers
|
|
29
|
+
# -----------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ensure_cache_dir():
|
|
33
|
+
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _now() -> int:
|
|
37
|
+
return int(time.time())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _b64url_no_pad(data: bytes) -> str:
|
|
41
|
+
return base64.urlsafe_b64encode(data).decode().rstrip("=")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _pkce_pair():
|
|
45
|
+
"""
|
|
46
|
+
Returns (code_verifier, code_challenge) using S256 per RFC 7636.
|
|
47
|
+
"""
|
|
48
|
+
verifier = _b64url_no_pad(secrets.token_bytes(32))
|
|
49
|
+
digest = hashlib.sha256(verifier.encode()).digest()
|
|
50
|
+
challenge = _b64url_no_pad(digest)
|
|
51
|
+
return verifier, challenge
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _api_base() -> str:
|
|
55
|
+
api = os.getenv("MESHAGENT_API_URL", "https://api.meshagent.com")
|
|
56
|
+
if not api:
|
|
57
|
+
raise RuntimeError("MESHAGENT_API_URL is not set")
|
|
58
|
+
return api.rstrip("/")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _authorization_url() -> str:
|
|
62
|
+
return f"{_api_base()}/oauth/authorize"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _token_url() -> str:
|
|
66
|
+
return f"{_api_base()}/oauth/token"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _client_id() -> str:
|
|
70
|
+
cid = os.getenv("MESHAGENT_OAUTH_CLIENT_ID", "p8xy1ZUi73jJUJbNfTg92HUSDpCSZJcc")
|
|
71
|
+
if not cid:
|
|
72
|
+
raise RuntimeError("MESHAGENT_OAUTH_CLIENT_ID is not set")
|
|
73
|
+
return cid
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _client_secret() -> str | None:
|
|
77
|
+
return os.getenv("MESHAGENT_OAUTH_CLIENT_SECRET")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _scopes() -> str:
|
|
81
|
+
return os.getenv("MESHAGENT_OAUTH_SCOPES", "admin")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _save(tokens: dict):
|
|
85
|
+
"""
|
|
86
|
+
Persist minimal token info to disk.
|
|
87
|
+
Expected keys: access_token, refresh_token (optional), expires_at (epoch int).
|
|
88
|
+
"""
|
|
89
|
+
_ensure_cache_dir()
|
|
90
|
+
CACHE_FILE.write_text(
|
|
91
|
+
json.dumps(
|
|
92
|
+
{
|
|
93
|
+
"access_token": tokens.get("access_token"),
|
|
94
|
+
"refresh_token": tokens.get("refresh_token"),
|
|
95
|
+
"expires_at": tokens.get("expires_at"),
|
|
96
|
+
"token_type": tokens.get("token_type", "Bearer"),
|
|
97
|
+
"scope": tokens.get("scope"),
|
|
98
|
+
"id_token": tokens.get("id_token"),
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _load() -> dict | None:
|
|
105
|
+
_ensure_cache_dir()
|
|
106
|
+
if CACHE_FILE.exists():
|
|
107
|
+
return json.loads(CACHE_FILE.read_text())
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _post_form(url: str, form: dict) -> dict:
|
|
111
|
+
"""
|
|
112
|
+
POST application/x-www-form-urlencoded and return parsed JSON or raise.
|
|
113
|
+
"""
|
|
114
|
+
headers = {"Accept": "application/json"}
|
|
115
|
+
async with ClientSession() as s:
|
|
116
|
+
async with s.post(url, data=form, headers=headers) as resp:
|
|
117
|
+
text = await resp.text()
|
|
118
|
+
if resp.status >= 400:
|
|
119
|
+
raise RuntimeError(f"Token endpoint error {resp.status}: {text}")
|
|
120
|
+
try:
|
|
121
|
+
return json.loads(text)
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
raise RuntimeError(
|
|
124
|
+
f"Unexpected non-JSON response from token endpoint: {text}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# -----------------------------------------------------------------------------
|
|
129
|
+
# Local HTTP callback
|
|
130
|
+
# -----------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _wait_for_code(expected_state: str) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Spin up a one-shot aiohttp server and await ?code=…&state=…
|
|
136
|
+
Validates 'state' if provided. Returns the 'code'.
|
|
137
|
+
"""
|
|
138
|
+
app = web.Application()
|
|
139
|
+
code_fut: asyncio.Future[str] = asyncio.get_event_loop().create_future()
|
|
140
|
+
|
|
141
|
+
async def callback(request):
|
|
142
|
+
code = request.query.get("code")
|
|
143
|
+
state = request.query.get("state")
|
|
144
|
+
if expected_state and state != expected_state:
|
|
145
|
+
return web.Response(status=400, text="State mismatch. Close this tab.")
|
|
146
|
+
if code:
|
|
147
|
+
if not code_fut.done():
|
|
148
|
+
code_fut.set_result(code)
|
|
149
|
+
return web.Response(text="You may close this tab.")
|
|
150
|
+
return web.Response(status=400, text="Missing 'code'.")
|
|
151
|
+
|
|
152
|
+
app.add_routes([web.get("/callback", callback)])
|
|
153
|
+
runner = web.AppRunner(app, access_log=None)
|
|
154
|
+
await runner.setup()
|
|
155
|
+
site = web.TCPSite(runner, "localhost", REDIRECT_PORT)
|
|
156
|
+
await site.start()
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
return await code_fut
|
|
160
|
+
finally:
|
|
161
|
+
await runner.cleanup()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# -----------------------------------------------------------------------------
|
|
165
|
+
# OAuth flows
|
|
166
|
+
# -----------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def _exchange_code_for_tokens(code: str, code_verifier: str) -> dict:
|
|
170
|
+
form = {
|
|
171
|
+
"grant_type": "authorization_code",
|
|
172
|
+
"code": code,
|
|
173
|
+
"redirect_uri": REDIRECT_URL,
|
|
174
|
+
"client_id": _client_id(),
|
|
175
|
+
"code_verifier": code_verifier,
|
|
176
|
+
}
|
|
177
|
+
# Include client_secret only if provided (public clients typically omit)
|
|
178
|
+
client_secret = _client_secret()
|
|
179
|
+
if client_secret:
|
|
180
|
+
form["client_secret"] = client_secret
|
|
181
|
+
|
|
182
|
+
token_json = await _post_form(_token_url(), form)
|
|
183
|
+
|
|
184
|
+
# Compute absolute expiry; default to 3600s if expires_in missing
|
|
185
|
+
expires_in = int(token_json.get("expires_in", 3600))
|
|
186
|
+
token_json["expires_at"] = _now() + max(0, expires_in - 30) # small safety skew
|
|
187
|
+
return token_json
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def _refresh_tokens(tokens: dict) -> dict:
|
|
191
|
+
if not tokens or not tokens.get("refresh_token"):
|
|
192
|
+
raise RuntimeError("No refresh token available to refresh access token.")
|
|
193
|
+
|
|
194
|
+
form = {
|
|
195
|
+
"grant_type": "refresh_token",
|
|
196
|
+
"refresh_token": tokens["refresh_token"],
|
|
197
|
+
"client_id": _client_id(),
|
|
198
|
+
}
|
|
199
|
+
client_secret = _client_secret()
|
|
200
|
+
if client_secret:
|
|
201
|
+
form["client_secret"] = client_secret
|
|
202
|
+
|
|
203
|
+
token_json = await _post_form(_token_url(), form)
|
|
204
|
+
|
|
205
|
+
# Some servers rotate refresh tokens; keep old one if none returned
|
|
206
|
+
token_json["refresh_token"] = token_json.get(
|
|
207
|
+
"refresh_token", tokens.get("refresh_token")
|
|
208
|
+
)
|
|
209
|
+
expires_in = int(token_json.get("expires_in", 3600))
|
|
210
|
+
token_json["expires_at"] = _now() + max(0, expires_in - 30)
|
|
211
|
+
return token_json
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# -----------------------------------------------------------------------------
|
|
215
|
+
# Public API (unchanged names)
|
|
216
|
+
# -----------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def login():
|
|
220
|
+
"""
|
|
221
|
+
Launches the system browser for OAuth 2.0 Authorization Code + PKCE.
|
|
222
|
+
Persists tokens to ~/.meshagent/session.json
|
|
223
|
+
"""
|
|
224
|
+
authz = _authorization_url()
|
|
225
|
+
client_id = _client_id()
|
|
226
|
+
scope = _scopes()
|
|
227
|
+
|
|
228
|
+
code_verifier, code_challenge = _pkce_pair()
|
|
229
|
+
state = _b64url_no_pad(secrets.token_bytes(16))
|
|
230
|
+
|
|
231
|
+
query = {
|
|
232
|
+
"response_type": "code",
|
|
233
|
+
"client_id": client_id,
|
|
234
|
+
"redirect_uri": REDIRECT_URL,
|
|
235
|
+
"scope": scope,
|
|
236
|
+
"code_challenge": code_challenge,
|
|
237
|
+
"code_challenge_method": "S256",
|
|
238
|
+
"state": state,
|
|
239
|
+
}
|
|
240
|
+
auth_url = f"{authz}?{urlencode(query)}"
|
|
241
|
+
|
|
242
|
+
# Kick user to browser without blocking the loop
|
|
243
|
+
await asyncio.to_thread(webbrowser.open, auth_url)
|
|
244
|
+
print(f"Waiting for auth redirect on {auth_url}…")
|
|
245
|
+
|
|
246
|
+
# Await the auth code, then exchange for tokens
|
|
247
|
+
auth_code = await _wait_for_code(state)
|
|
248
|
+
print("Got code, exchanging…")
|
|
249
|
+
|
|
250
|
+
tokens = await _exchange_code_for_tokens(auth_code, code_verifier)
|
|
251
|
+
_save(tokens)
|
|
252
|
+
print("✅ Logged in (tokens cached).")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def session():
|
|
256
|
+
"""
|
|
257
|
+
Returns a tuple (client, tokens_dict)
|
|
258
|
+
- client is None (kept for backward compatibility with prior signature).
|
|
259
|
+
- tokens_dict contains access_token, refresh_token, expires_at, token_type, scope, id_token.
|
|
260
|
+
Will auto-refresh if expired/near-expiry and update the cache.
|
|
261
|
+
"""
|
|
262
|
+
tokens = _load()
|
|
263
|
+
if not tokens:
|
|
264
|
+
return None, None
|
|
265
|
+
|
|
266
|
+
# Refresh if expired or within 5 min of expiry
|
|
267
|
+
if not tokens.get("expires_at") or tokens["expires_at"] <= _now() + 5 * 60:
|
|
268
|
+
try:
|
|
269
|
+
tokens = await _refresh_tokens(tokens)
|
|
270
|
+
_save(tokens)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
# If refresh fails, wipe session to force re-login
|
|
273
|
+
print(f"⚠️ Token refresh failed: {e}")
|
|
274
|
+
return None, None
|
|
275
|
+
|
|
276
|
+
return None, tokens
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def logout():
|
|
280
|
+
"""
|
|
281
|
+
Clears the cached tokens. (If your OAuth server supports revocation,
|
|
282
|
+
you can add a call here; not provided in the spec.)
|
|
283
|
+
"""
|
|
284
|
+
_, tokens = await session()
|
|
285
|
+
# Optional: call a revocation endpoint here if your server provides one.
|
|
286
|
+
CACHE_FILE.unlink(missing_ok=True)
|
|
287
|
+
print("👋 Signed out")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def get_access_token():
|
|
291
|
+
"""
|
|
292
|
+
Returns a fresh access token, refreshing if needed.
|
|
293
|
+
"""
|
|
294
|
+
_, tokens = await session()
|
|
295
|
+
return tokens["access_token"] if tokens else None
|
meshagent/cli/call.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import os
|
|
3
|
+
from rich import print
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
from meshagent.cli.common_options import ProjectIdOption, RoomOption
|
|
6
|
+
import json
|
|
7
|
+
import aiohttp
|
|
8
|
+
from meshagent.api import (
|
|
9
|
+
RoomClient,
|
|
10
|
+
ParticipantToken,
|
|
11
|
+
WebSocketClientProtocol,
|
|
12
|
+
ParticipantGrant,
|
|
13
|
+
ApiScope,
|
|
14
|
+
)
|
|
15
|
+
from meshagent.api.helpers import meshagent_base_url, websocket_room_url
|
|
16
|
+
from meshagent.api.services import send_webhook
|
|
17
|
+
from meshagent.cli import async_typer
|
|
18
|
+
from meshagent.cli.helper import get_client, resolve_project_id, resolve_key
|
|
19
|
+
from meshagent.cli.helper import resolve_room
|
|
20
|
+
from urllib.parse import urlparse
|
|
21
|
+
from pathlib import PurePath
|
|
22
|
+
import socket
|
|
23
|
+
import ipaddress
|
|
24
|
+
import pathlib
|
|
25
|
+
from pydantic_yaml import parse_yaml_raw_as
|
|
26
|
+
from meshagent.api.participant_token import ParticipantTokenSpec
|
|
27
|
+
|
|
28
|
+
app = async_typer.AsyncTyper(help="Trigger agent/tool calls via URL")
|
|
29
|
+
|
|
30
|
+
PRIVATE_NETS = (
|
|
31
|
+
ipaddress.ip_network("10.0.0.0/8"),
|
|
32
|
+
ipaddress.ip_network("172.16.0.0/12"),
|
|
33
|
+
ipaddress.ip_network("192.168.0.0/16"),
|
|
34
|
+
ipaddress.ip_network("169.254.0.0/16"), # IPv4 link-local
|
|
35
|
+
ipaddress.ip_network("fc00::/7"), # IPv6 unique-local
|
|
36
|
+
ipaddress.ip_network("fe80::/10"), # IPv6 link-local
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_local_url(url: str) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Return True if *url* points to the local machine or a private-LAN host.
|
|
43
|
+
"""
|
|
44
|
+
# 1. Handle bare paths and file://
|
|
45
|
+
if "://" not in url:
|
|
46
|
+
return PurePath(url).is_absolute() or not ("/" in url or "\\" in url)
|
|
47
|
+
parsed = urlparse(url)
|
|
48
|
+
if parsed.scheme == "file":
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
# 2. Quick loop-back check on hostname literal
|
|
52
|
+
hostname = parsed.hostname
|
|
53
|
+
if hostname in {"localhost", None}: # None ⇒ something like "http:///path"
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Accept both direct IP literals and DNS names
|
|
58
|
+
addr_info = socket.getaddrinfo(hostname, None)
|
|
59
|
+
except socket.gaierror:
|
|
60
|
+
return False # Unresolvable host ⇒ treat as non-local (or raise)
|
|
61
|
+
|
|
62
|
+
for *_, sockaddr in addr_info:
|
|
63
|
+
ip_str = sockaddr[0]
|
|
64
|
+
ip = ipaddress.ip_address(ip_str)
|
|
65
|
+
|
|
66
|
+
if ip.is_loopback:
|
|
67
|
+
return True
|
|
68
|
+
if any(ip in net for net in PRIVATE_NETS):
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.async_command("schema", help="Send a call request to a schema webhook URL")
|
|
73
|
+
@app.async_command("toolkit", help="Send a call request to a toolkit webhook URL")
|
|
74
|
+
@app.async_command("agent", help="Send a call request to an agent webhook URL")
|
|
75
|
+
@app.async_command("tool", help="Send a call request to a tool webhook URL")
|
|
76
|
+
async def make_call(
|
|
77
|
+
*,
|
|
78
|
+
project_id: ProjectIdOption,
|
|
79
|
+
room: RoomOption,
|
|
80
|
+
role: str = "agent",
|
|
81
|
+
local: Optional[bool] = None,
|
|
82
|
+
participant_name: Annotated[
|
|
83
|
+
Optional[str],
|
|
84
|
+
typer.Option(..., help="the participant name to be used by the callee"),
|
|
85
|
+
] = None,
|
|
86
|
+
url: Annotated[str, typer.Option(..., help="URL the agent should call")],
|
|
87
|
+
arguments: Annotated[
|
|
88
|
+
str, typer.Option(..., help="JSON string with arguments for the call")
|
|
89
|
+
] = {},
|
|
90
|
+
permissions: Annotated[
|
|
91
|
+
Optional[str],
|
|
92
|
+
typer.Option(
|
|
93
|
+
"--permissions",
|
|
94
|
+
"-p",
|
|
95
|
+
help="File path to a token definition, if not specified default agent permissions will be used",
|
|
96
|
+
),
|
|
97
|
+
] = None,
|
|
98
|
+
key: Annotated[
|
|
99
|
+
str,
|
|
100
|
+
typer.Option("--key", help="an api key to sign the token with"),
|
|
101
|
+
] = None,
|
|
102
|
+
):
|
|
103
|
+
"""Send a `room.call` request to a URL or in-room agent."""
|
|
104
|
+
|
|
105
|
+
key = await resolve_key(project_id=project_id, key=key)
|
|
106
|
+
|
|
107
|
+
if permissions is not None:
|
|
108
|
+
with open(str(pathlib.Path(permissions).expanduser().resolve()), "rb") as f:
|
|
109
|
+
spec = parse_yaml_raw_as(ParticipantTokenSpec, f.read())
|
|
110
|
+
|
|
111
|
+
token = ParticipantToken(
|
|
112
|
+
name=spec.identity,
|
|
113
|
+
)
|
|
114
|
+
token.add_role_grant(role=role)
|
|
115
|
+
token.add_room_grant(room)
|
|
116
|
+
token.add_api_grant(spec.api)
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
token = None
|
|
120
|
+
|
|
121
|
+
await _make_call(
|
|
122
|
+
project_id=project_id,
|
|
123
|
+
room=room,
|
|
124
|
+
role=role,
|
|
125
|
+
local=local,
|
|
126
|
+
participant_name=participant_name,
|
|
127
|
+
url=url,
|
|
128
|
+
arguments=arguments,
|
|
129
|
+
token=token,
|
|
130
|
+
key=key,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def _make_call(
|
|
135
|
+
*,
|
|
136
|
+
project_id: ProjectIdOption,
|
|
137
|
+
room: RoomOption,
|
|
138
|
+
role: str = "agent",
|
|
139
|
+
local: Optional[bool] = None,
|
|
140
|
+
participant_name: Annotated[
|
|
141
|
+
Optional[str],
|
|
142
|
+
typer.Option(..., help="the participant name to be used by the callee"),
|
|
143
|
+
] = None,
|
|
144
|
+
url: Annotated[str, typer.Option(..., help="URL the agent should call")],
|
|
145
|
+
arguments: Annotated[
|
|
146
|
+
str, typer.Option(..., help="JSON string with arguments for the call")
|
|
147
|
+
] = {},
|
|
148
|
+
token: Optional[ParticipantToken] = None,
|
|
149
|
+
permissions: Optional[ApiScope] = None,
|
|
150
|
+
key: str,
|
|
151
|
+
):
|
|
152
|
+
"""
|
|
153
|
+
Instruct an agent to 'call' a given URL with specific arguments.
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
if participant_name is None:
|
|
157
|
+
print("[red]--participant-name is required[/red]")
|
|
158
|
+
raise typer.Exit(1)
|
|
159
|
+
|
|
160
|
+
account_client = await get_client()
|
|
161
|
+
try:
|
|
162
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
163
|
+
|
|
164
|
+
room = resolve_room(room)
|
|
165
|
+
|
|
166
|
+
if token is None:
|
|
167
|
+
jwt = os.getenv("MESHAGENT_TOKEN")
|
|
168
|
+
if jwt is None:
|
|
169
|
+
token = ParticipantToken(
|
|
170
|
+
name=participant_name,
|
|
171
|
+
)
|
|
172
|
+
token.add_api_grant(permissions or ApiScope.agent_default())
|
|
173
|
+
token.add_role_grant(role=role)
|
|
174
|
+
token.add_room_grant(room)
|
|
175
|
+
token.grants.append(ParticipantGrant(name="tunnel_ports", scope="9000"))
|
|
176
|
+
jwt = token.to_jwt(api_key=key)
|
|
177
|
+
else:
|
|
178
|
+
jwt = token.to_jwt(api_key=key)
|
|
179
|
+
|
|
180
|
+
if local is None:
|
|
181
|
+
local = is_local_url(url)
|
|
182
|
+
|
|
183
|
+
if local:
|
|
184
|
+
async with aiohttp.ClientSession() as session:
|
|
185
|
+
event = "room.call"
|
|
186
|
+
data = {
|
|
187
|
+
"room_url": websocket_room_url(room_name=room),
|
|
188
|
+
"room_name": room,
|
|
189
|
+
"token": jwt,
|
|
190
|
+
"arguments": json.loads(arguments)
|
|
191
|
+
if isinstance(arguments, str)
|
|
192
|
+
else arguments,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await send_webhook(
|
|
196
|
+
session=session, url=url, event=event, data=data, secret=None
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
print("[bold green]Connecting to room...[/bold green]")
|
|
200
|
+
async with RoomClient(
|
|
201
|
+
protocol=WebSocketClientProtocol(
|
|
202
|
+
url=websocket_room_url(
|
|
203
|
+
room_name=room, base_url=meshagent_base_url()
|
|
204
|
+
),
|
|
205
|
+
token=jwt,
|
|
206
|
+
)
|
|
207
|
+
) as client:
|
|
208
|
+
print("[bold green]Making agent call...[/bold green]")
|
|
209
|
+
await client.agents.make_call(
|
|
210
|
+
name=participant_name, url=url, arguments=json.loads(arguments)
|
|
211
|
+
)
|
|
212
|
+
print("[bold cyan]Call request sent successfully.[/bold cyan]")
|
|
213
|
+
|
|
214
|
+
finally:
|
|
215
|
+
await account_client.close()
|