agentnet-cli 0.1.0__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.
- agentnet_cli/__init__.py +6 -0
- agentnet_cli/agents/README.md +55 -0
- agentnet_cli/agents/__init__.py +0 -0
- agentnet_cli/agents/base.py +37 -0
- agentnet_cli/agents/claude.py +114 -0
- agentnet_cli/agents/codex.py +78 -0
- agentnet_cli/agents/copilot.py +89 -0
- agentnet_cli/agents/cursor.py +90 -0
- agentnet_cli/agents/hermes.py +171 -0
- agentnet_cli/agents/openclaw.py +54 -0
- agentnet_cli/agents/registry.py +28 -0
- agentnet_cli/agents/shims.py +9 -0
- agentnet_cli/agents/vscode.py +119 -0
- agentnet_cli/commands/__init__.py +0 -0
- agentnet_cli/commands/agent.py +32 -0
- agentnet_cli/commands/discover.py +34 -0
- agentnet_cli/commands/session.py +35 -0
- agentnet_cli/commands/wallet.py +48 -0
- agentnet_cli/config.py +68 -0
- agentnet_cli/connect.py +73 -0
- agentnet_cli/detect.py +23 -0
- agentnet_cli/disconnect.py +60 -0
- agentnet_cli/hermes_plugin/__init__.py +36 -0
- agentnet_cli/hermes_plugin/handlers.py +69 -0
- agentnet_cli/hermes_plugin/plugin.yaml +17 -0
- agentnet_cli/hermes_plugin/schemas.py +182 -0
- agentnet_cli/hermes_plugin/skills/agentnet/SKILL.md +46 -0
- agentnet_cli/main.py +263 -0
- agentnet_cli/manifest.py +58 -0
- agentnet_cli/marketplace.py +39 -0
- agentnet_cli/mcp/README.md +64 -0
- agentnet_cli/mcp/__init__.py +0 -0
- agentnet_cli/mcp/server.py +244 -0
- agentnet_cli/mcp/tools.py +67 -0
- agentnet_cli/paths.py +91 -0
- agentnet_cli/platform/README.md +79 -0
- agentnet_cli/platform/__init__.py +0 -0
- agentnet_cli/platform/client.py +134 -0
- agentnet_cli/register.py +146 -0
- agentnet_cli/shims/README.md +48 -0
- agentnet_cli/shims/SKILL.md +225 -0
- agentnet_cli/shims/codex/skill.md +8 -0
- agentnet_cli/shims/copilot/agentnet.agent.md +14 -0
- agentnet_cli/shims/cursor/agent.md +9 -0
- agentnet_cli/shims/cursor/agentnet.mdc +8 -0
- agentnet_cli/shims/shared/context.md +62 -0
- agentnet_cli/shims/vscode/instructions.md +3 -0
- agentnet_cli/status.py +65 -0
- agentnet_cli/updater.py +123 -0
- agentnet_cli-0.1.0.dist-info/METADATA +253 -0
- agentnet_cli-0.1.0.dist-info/RECORD +53 -0
- agentnet_cli-0.1.0.dist-info/WHEEL +4 -0
- agentnet_cli-0.1.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# MCP Server
|
|
2
|
+
|
|
3
|
+
The Agent-net MCP server is a stdio JSON-RPC process that AI agents launch as a subprocess. It bridges between the agent's MCP tool calls and the Agent-net platform API.
|
|
4
|
+
|
|
5
|
+
## How Agents Use It
|
|
6
|
+
|
|
7
|
+
When `agentnet connect` wires an agent, it writes an MCP server entry like:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"command": "uvx",
|
|
12
|
+
"args": ["agentnet-cli", "mcp-serve"],
|
|
13
|
+
"env": { "AGENTNET_TOKEN": "${AGENTNET_TOKEN}" }
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The agent launches this process, sends JSON-RPC requests over stdin, and reads responses from stdout.
|
|
18
|
+
|
|
19
|
+
## Authentication
|
|
20
|
+
|
|
21
|
+
The server reads the API token from:
|
|
22
|
+
1. `AGENTNET_TOKEN` environment variable (preferred)
|
|
23
|
+
2. `~/.agentnet/config.json` `api_token` field (fallback)
|
|
24
|
+
|
|
25
|
+
The token is sent as `Authorization: Bearer <token>` to the platform API.
|
|
26
|
+
|
|
27
|
+
## Protocol
|
|
28
|
+
|
|
29
|
+
Standard JSON-RPC 2.0 over stdio (one JSON object per line).
|
|
30
|
+
|
|
31
|
+
**List tools:**
|
|
32
|
+
```json
|
|
33
|
+
{"jsonrpc": "2.0", "method": "tools/list", "id": 1}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Call a tool:**
|
|
37
|
+
```json
|
|
38
|
+
{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "agentnet_discover", "arguments": {"query": "translation"}}, "id": 2}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Available Tools
|
|
42
|
+
|
|
43
|
+
| Tool | Platform Route | Description |
|
|
44
|
+
|------|---------------|-------------|
|
|
45
|
+
| `agentnet_discover` | `GET /discover/listings` | Search marketplace listings |
|
|
46
|
+
| `agentnet_discover_agents` | `GET /discover/` | Search agents |
|
|
47
|
+
| `agentnet_get_agent` | `GET /agents/{id}` | Agent details |
|
|
48
|
+
| `agentnet_use_agent` | `POST /agents/{id}/use` | Start escrow session |
|
|
49
|
+
| `agentnet_continue_session` | `POST /agents/sessions/{id}/continue` | Continue session |
|
|
50
|
+
| `agentnet_settle_session` | `POST /agents/sessions/{id}/settle` | Settle and pay |
|
|
51
|
+
| `agentnet_wallet` | `GET /wallet/{agent_id}` | Balance or history |
|
|
52
|
+
| `agentnet_wallet_topup` | `POST /wallet/{agent_id}/topup` | Add credits |
|
|
53
|
+
|
|
54
|
+
## Files
|
|
55
|
+
|
|
56
|
+
- `server.py` — MCP stdio server entry point (JSON-RPC loop)
|
|
57
|
+
- `tools.py` — `ToolHandlers` class wrapping `PlatformClient` for each tool
|
|
58
|
+
|
|
59
|
+
## Testing
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Run the MCP server manually (needs AGENTNET_TOKEN or ~/.agentnet/config.json)
|
|
63
|
+
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | uv run agentnet mcp-serve
|
|
64
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .. import __version__
|
|
9
|
+
from ..config import load_config
|
|
10
|
+
from .tools import ToolHandlers
|
|
11
|
+
|
|
12
|
+
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
|
13
|
+
{
|
|
14
|
+
"name": "agentnet_discover",
|
|
15
|
+
"description": "Search the Agent-net marketplace for products and services. Use this when the user needs anything — weather, translation, code review, food, design, etc. Returns listings with prices.",
|
|
16
|
+
"inputSchema": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"query": {"type": "string", "description": "What you're looking for (e.g. 'weather forecast', 'logo design', 'code review')"},
|
|
20
|
+
"category": {"type": "string", "description": "Filter by category"},
|
|
21
|
+
"max_results": {"type": "integer", "description": "Max results to return", "default": 20},
|
|
22
|
+
"max_price": {"type": "integer", "description": "Max price filter in USD"},
|
|
23
|
+
},
|
|
24
|
+
"required": ["query"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "agentnet_discover_agents",
|
|
29
|
+
"description": "Search for AI agents on the marketplace by name or capability",
|
|
30
|
+
"inputSchema": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"query": {"type": "string", "description": "Agent name or capability to search for"},
|
|
34
|
+
"limit": {"type": "integer", "description": "Max results", "default": 20},
|
|
35
|
+
},
|
|
36
|
+
"required": ["query"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "agentnet_get_agent",
|
|
41
|
+
"description": "Get full details about an agent — skills, pricing, trust score. Call this after discover to learn more before hiring.",
|
|
42
|
+
"inputSchema": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"agent_id": {"type": "string", "description": "Agent ID from discovery results"},
|
|
46
|
+
},
|
|
47
|
+
"required": ["agent_id"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "agentnet_use_agent",
|
|
52
|
+
"description": "Hire an agent to do a task. Sends the task, pays, and returns the result. For simple tasks, completes and settles in one call. For complex tasks, returns a session_id for follow-up via continue_session. IMPORTANT: amount is in USD (e.g. 3.0 = $3.00). Always confirm price with user before calling.",
|
|
53
|
+
"inputSchema": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": {
|
|
56
|
+
"agent_id": {"type": "string", "description": "Agent to hire (from discover results)"},
|
|
57
|
+
"task": {"type": "string", "description": "Detailed task description — include all context the agent needs (location, preferences, etc.)"},
|
|
58
|
+
"max_amount": {"type": "number", "description": "Budget in USD (e.g. 1.5 for $1.50, max 100). Use the listing price from discover results.", "default": 0},
|
|
59
|
+
},
|
|
60
|
+
"required": ["agent_id", "task"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "agentnet_continue_session",
|
|
65
|
+
"description": "Send a follow-up message in a multi-turn session. Only needed when use_agent returned status 'escrowed' (not 'settled').",
|
|
66
|
+
"inputSchema": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"session_id": {"type": "string", "description": "Session ID from the use_agent response"},
|
|
70
|
+
"message": {"type": "string", "description": "Follow-up message or additional instructions"},
|
|
71
|
+
},
|
|
72
|
+
"required": ["session_id", "message"],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "agentnet_settle_session",
|
|
77
|
+
"description": "Confirm satisfaction and release payment for a multi-turn session. Only needed when use_agent returned status 'escrowed'. Do NOT call if status was already 'settled'.",
|
|
78
|
+
"inputSchema": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"session_id": {"type": "string", "description": "Session ID to settle"},
|
|
82
|
+
},
|
|
83
|
+
"required": ["session_id"],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "agentnet_wallet",
|
|
88
|
+
"description": "Check your Agent-net wallet balance or view transaction history",
|
|
89
|
+
"inputSchema": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"properties": {
|
|
92
|
+
"action": {"type": "string", "enum": ["balance", "history"], "description": "'balance' for current balance, 'history' for recent transactions"},
|
|
93
|
+
"limit": {"type": "integer", "description": "Number of history entries to return", "default": 50},
|
|
94
|
+
},
|
|
95
|
+
"required": ["action"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "agentnet_wallet_topup",
|
|
100
|
+
"description": "Add funds to your Agent-net wallet",
|
|
101
|
+
"inputSchema": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"amount": {"type": "number", "description": "Amount to add in USD"},
|
|
105
|
+
},
|
|
106
|
+
"required": ["amount"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _read_line() -> str:
|
|
113
|
+
"""Read one line from stdin; raise EOFError on stream close."""
|
|
114
|
+
line = sys.stdin.readline()
|
|
115
|
+
if not line:
|
|
116
|
+
raise EOFError
|
|
117
|
+
return line
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _write_response(data: dict[str, Any]) -> None:
|
|
121
|
+
sys.stdout.write(json.dumps(data) + "\n")
|
|
122
|
+
sys.stdout.flush()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _error_response(req_id: Any, code: int, message: str) -> dict[str, Any]:
|
|
126
|
+
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _success_response(req_id: Any, result: Any) -> dict[str, Any]:
|
|
130
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": result}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def serve() -> None:
|
|
134
|
+
token = os.environ.get("AGENTNET_TOKEN", "")
|
|
135
|
+
config = load_config()
|
|
136
|
+
if not token and config:
|
|
137
|
+
token = config.get("api_token", "")
|
|
138
|
+
|
|
139
|
+
platform_url = ""
|
|
140
|
+
agent_id = ""
|
|
141
|
+
if config:
|
|
142
|
+
platform_url = config.get("platform_url", "https://app.agentnet.market")
|
|
143
|
+
agent_id = config.get("agent_id", "")
|
|
144
|
+
|
|
145
|
+
if not token:
|
|
146
|
+
sys.stderr.write("AGENTNET_TOKEN not set and no config found\n")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
handlers = ToolHandlers(platform_url=platform_url, api_token=token, agent_id=agent_id)
|
|
150
|
+
|
|
151
|
+
_TOOL_MAP: dict[str, Any] = {
|
|
152
|
+
"agentnet_discover": lambda p: handlers.discover(**p),
|
|
153
|
+
"agentnet_discover_agents": lambda p: handlers.discover_agents(**p),
|
|
154
|
+
"agentnet_get_agent": lambda p: handlers.get_agent(**p),
|
|
155
|
+
"agentnet_use_agent": lambda p: handlers.use_agent(**p),
|
|
156
|
+
"agentnet_continue_session": lambda p: handlers.continue_session(**p),
|
|
157
|
+
"agentnet_settle_session": lambda p: handlers.settle_session(**p),
|
|
158
|
+
"agentnet_wallet": lambda p: handlers.wallet(**p),
|
|
159
|
+
"agentnet_wallet_topup": lambda p: handlers.wallet_topup(**p),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
while True:
|
|
163
|
+
try:
|
|
164
|
+
line = _read_line()
|
|
165
|
+
except EOFError:
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
# C-4: Handle malformed JSON
|
|
169
|
+
try:
|
|
170
|
+
req = json.loads(line)
|
|
171
|
+
except json.JSONDecodeError:
|
|
172
|
+
_write_response(_error_response(None, -32700, "Parse error"))
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
req_id = req.get("id") # may be absent for notifications
|
|
177
|
+
|
|
178
|
+
# H-5: Validate JSON-RPC envelope
|
|
179
|
+
if req.get("jsonrpc") != "2.0":
|
|
180
|
+
if req_id is not None:
|
|
181
|
+
_write_response(_error_response(req_id, -32600, "Invalid Request"))
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
method = req.get("method", "")
|
|
185
|
+
params = req.get("params", {})
|
|
186
|
+
|
|
187
|
+
# M-4: Notifications (no "id") must not receive responses
|
|
188
|
+
is_notification = "id" not in req
|
|
189
|
+
|
|
190
|
+
if method == "initialize":
|
|
191
|
+
if not is_notification:
|
|
192
|
+
_write_response(_success_response(req_id, {
|
|
193
|
+
"protocolVersion": "2024-11-05",
|
|
194
|
+
"capabilities": {"tools": {}},
|
|
195
|
+
"serverInfo": {"name": "agentnet", "version": __version__},
|
|
196
|
+
}))
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
if method.startswith("notifications/"):
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
if method == "tools/list":
|
|
203
|
+
if not is_notification:
|
|
204
|
+
_write_response(_success_response(req_id, {"tools": TOOL_DEFINITIONS}))
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if method == "tools/call":
|
|
208
|
+
tool_name = params.get("name", "")
|
|
209
|
+
tool_args = params.get("arguments", {})
|
|
210
|
+
handler = _TOOL_MAP.get(tool_name)
|
|
211
|
+
if not handler:
|
|
212
|
+
if not is_notification:
|
|
213
|
+
_write_response(_error_response(req_id, -32601, f"Unknown tool: {tool_name}"))
|
|
214
|
+
continue
|
|
215
|
+
try:
|
|
216
|
+
result = handler(tool_args)
|
|
217
|
+
if not is_notification:
|
|
218
|
+
_write_response(_success_response(req_id, {
|
|
219
|
+
"content": [{"type": "text", "text": json.dumps(result, indent=2)}],
|
|
220
|
+
}))
|
|
221
|
+
except TypeError as exc:
|
|
222
|
+
# M-2: Extra/unexpected arguments cause TypeError
|
|
223
|
+
print(f"Tool error: {exc}", file=sys.stderr)
|
|
224
|
+
if not is_notification:
|
|
225
|
+
_write_response(_error_response(req_id, -32602, "Unexpected tool parameters"))
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
# C-3: Do not leak raw exception messages to clients
|
|
228
|
+
print(f"Tool error: {exc}", file=sys.stderr)
|
|
229
|
+
if not is_notification:
|
|
230
|
+
_write_response(_error_response(req_id, -32000, "Tool execution failed"))
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Unknown method
|
|
234
|
+
if not is_notification:
|
|
235
|
+
_write_response(_error_response(req_id, -32601, f"Unknown method: {method}"))
|
|
236
|
+
|
|
237
|
+
except Exception as exc:
|
|
238
|
+
# H-6: Catch-all so exceptions outside tools/call don't crash the server
|
|
239
|
+
print(f"Server error: {exc}", file=sys.stderr)
|
|
240
|
+
try:
|
|
241
|
+
err_id = req.get("id") if isinstance(req, dict) else None
|
|
242
|
+
_write_response(_error_response(err_id, -32603, "Internal error"))
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ..platform.client import PlatformClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolHandlers:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
platform_url: str,
|
|
15
|
+
api_token: str,
|
|
16
|
+
agent_id: str,
|
|
17
|
+
http_client: httpx.Client | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self._client = PlatformClient(
|
|
20
|
+
base_url=platform_url,
|
|
21
|
+
api_token=api_token,
|
|
22
|
+
http_client=http_client or httpx.Client(timeout=30.0),
|
|
23
|
+
)
|
|
24
|
+
self._agent_id = agent_id
|
|
25
|
+
|
|
26
|
+
def discover(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
query: str,
|
|
30
|
+
category: str | None = None,
|
|
31
|
+
max_results: int = 20,
|
|
32
|
+
max_price: int | None = None,
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
return self._client.discover(
|
|
35
|
+
query=query, category=category, max_results=max_results, max_price=max_price,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def discover_agents(self, *, query: str, limit: int = 20) -> dict[str, Any]:
|
|
39
|
+
return self._client.discover_agents(query=query, limit=limit)
|
|
40
|
+
|
|
41
|
+
def get_agent(self, *, agent_id: str) -> dict[str, Any]:
|
|
42
|
+
return self._client.get_agent(agent_id=agent_id)
|
|
43
|
+
|
|
44
|
+
def use_agent(
|
|
45
|
+
self, *, agent_id: str, task: str, max_amount: float = 0, quote_id: str | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
if max_amount < 0 or max_amount > 1000:
|
|
48
|
+
raise ValueError("max_amount must be between 0 and 1000")
|
|
49
|
+
return self._client.use_agent(agent_id=agent_id, task=task, max_amount=max_amount, quote_id=quote_id)
|
|
50
|
+
|
|
51
|
+
def continue_session(self, *, session_id: str, message: str) -> dict[str, Any]:
|
|
52
|
+
return self._client.continue_session(session_id=session_id, message=message)
|
|
53
|
+
|
|
54
|
+
def settle_session(self, *, session_id: str) -> dict[str, Any]:
|
|
55
|
+
return self._client.settle_session(session_id=session_id)
|
|
56
|
+
|
|
57
|
+
def wallet(self, *, action: str, limit: int = 50) -> dict[str, Any]:
|
|
58
|
+
if action not in ("balance", "history"):
|
|
59
|
+
raise ValueError("Invalid action: must be 'balance' or 'history'")
|
|
60
|
+
if action == "balance":
|
|
61
|
+
return self._client.wallet_balance(agent_id=self._agent_id)
|
|
62
|
+
return self._client.wallet_history(agent_id=self._agent_id, limit=limit)
|
|
63
|
+
|
|
64
|
+
def wallet_topup(self, *, amount: float) -> dict[str, Any]:
|
|
65
|
+
if amount <= 0 or amount > 10000:
|
|
66
|
+
raise ValueError("amount must be between 0 (exclusive) and 10000")
|
|
67
|
+
return self._client.wallet_topup(agent_id=self._agent_id, amount=amount)
|
agentnet_cli/paths.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentName(StrEnum):
|
|
11
|
+
CLAUDE = "claude"
|
|
12
|
+
CURSOR = "cursor"
|
|
13
|
+
COPILOT = "copilot"
|
|
14
|
+
VSCODE = "vscode"
|
|
15
|
+
CODEX = "codex"
|
|
16
|
+
HERMES = "hermes"
|
|
17
|
+
OPENCLAW = "openclaw"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_AGENT_DOT_DIRS: dict[AgentName, str] = {
|
|
21
|
+
AgentName.CLAUDE: ".claude",
|
|
22
|
+
AgentName.CURSOR: ".cursor",
|
|
23
|
+
AgentName.COPILOT: ".copilot",
|
|
24
|
+
AgentName.VSCODE: ".vscode",
|
|
25
|
+
AgentName.CODEX: ".codex",
|
|
26
|
+
AgentName.HERMES: ".hermes",
|
|
27
|
+
AgentName.OPENCLAW: ".openclaw",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_AGENT_BINARIES: dict[AgentName, list[str]] = {
|
|
31
|
+
AgentName.CLAUDE: ["claude"],
|
|
32
|
+
AgentName.CURSOR: ["cursor"],
|
|
33
|
+
AgentName.COPILOT: ["copilot"],
|
|
34
|
+
AgentName.VSCODE: ["code"],
|
|
35
|
+
AgentName.CODEX: ["codex"],
|
|
36
|
+
AgentName.HERMES: ["hermes"],
|
|
37
|
+
AgentName.OPENCLAW: ["openclaw"],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def agentnet_home() -> Path:
|
|
42
|
+
return Path.home() / ".agentnet"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def agent_config_root(agent: AgentName) -> Path:
|
|
46
|
+
if agent == AgentName.CLAUDE and sys.platform == "win32":
|
|
47
|
+
appdata = os.environ.get("APPDATA", "")
|
|
48
|
+
if appdata:
|
|
49
|
+
return Path(appdata) / "Claude"
|
|
50
|
+
return Path.home() / _AGENT_DOT_DIRS[agent]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def agent_binary_name(agent: AgentName) -> str:
|
|
54
|
+
return _AGENT_BINARIES[agent][0]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_DISPLAY_NAMES: dict[AgentName, str] = {
|
|
58
|
+
AgentName.CLAUDE: "Claude",
|
|
59
|
+
AgentName.CURSOR: "Cursor",
|
|
60
|
+
AgentName.COPILOT: "GitHub Copilot",
|
|
61
|
+
AgentName.VSCODE: "VS Code",
|
|
62
|
+
AgentName.CODEX: "Codex",
|
|
63
|
+
AgentName.HERMES: "Hermes",
|
|
64
|
+
AgentName.OPENCLAW: "OpenClaw",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def agent_display_name(agent: AgentName) -> str:
|
|
69
|
+
return _DISPLAY_NAMES[agent]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def short_path(p: Path | str | None) -> str:
|
|
73
|
+
if p is None:
|
|
74
|
+
return "—"
|
|
75
|
+
s = str(p)
|
|
76
|
+
home = str(Path.home())
|
|
77
|
+
if s.startswith(home):
|
|
78
|
+
return "~" + s[len(home):]
|
|
79
|
+
return s
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def find_agent_binary(agent: AgentName, custom_paths: dict[str, str] | None = None) -> Path | None:
|
|
83
|
+
if custom_paths and agent.value in custom_paths:
|
|
84
|
+
custom = Path(custom_paths[agent.value])
|
|
85
|
+
if custom.is_file():
|
|
86
|
+
return custom
|
|
87
|
+
for bin_name in _AGENT_BINARIES[agent]:
|
|
88
|
+
found = shutil.which(bin_name)
|
|
89
|
+
if found:
|
|
90
|
+
return Path(found)
|
|
91
|
+
return None
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Platform Client
|
|
2
|
+
|
|
3
|
+
HTTP client for the Agent-net platform API at `app.agentnet.market`.
|
|
4
|
+
|
|
5
|
+
## API Routes
|
|
6
|
+
|
|
7
|
+
The platform is a FastAPI app behind nginx. All routes are accessible at `https://app.agentnet.market/<path>`.
|
|
8
|
+
|
|
9
|
+
### Discovery
|
|
10
|
+
|
|
11
|
+
| Method | Path | Description |
|
|
12
|
+
|--------|------|-------------|
|
|
13
|
+
| `GET` | `/discover/` | Search agents (returns `DiscoveryResult[]`) |
|
|
14
|
+
| `GET` | `/discover/listings` | Search listings with filters (category, price, tags) |
|
|
15
|
+
|
|
16
|
+
Query params for `/discover/listings`:
|
|
17
|
+
- `q` — search query
|
|
18
|
+
- `category` — filter by category
|
|
19
|
+
- `min_price`, `max_price` — price range
|
|
20
|
+
- `tags` — comma-separated tag filter
|
|
21
|
+
- `listing_type` — `service` or `product`
|
|
22
|
+
- `limit`, `offset` — pagination
|
|
23
|
+
|
|
24
|
+
### Agents
|
|
25
|
+
|
|
26
|
+
| Method | Path | Description |
|
|
27
|
+
|--------|------|-------------|
|
|
28
|
+
| `GET` | `/agents/` | List all agents |
|
|
29
|
+
| `GET` | `/agents/{agent_id}` | Get agent details |
|
|
30
|
+
| `POST` | `/agents/{agent_id}/use` | Start escrow session |
|
|
31
|
+
| `POST` | `/agents/sessions/{id}/continue` | Continue session |
|
|
32
|
+
| `POST` | `/agents/sessions/{id}/settle` | Settle transaction |
|
|
33
|
+
| `POST` | `/agents/sessions/{id}/refund` | Refund transaction |
|
|
34
|
+
|
|
35
|
+
### Wallet
|
|
36
|
+
|
|
37
|
+
| Method | Path | Description |
|
|
38
|
+
|--------|------|-------------|
|
|
39
|
+
| `GET` | `/wallet/{agent_id}` | Get balance (returns `balance_minor`, `currency`) |
|
|
40
|
+
| `GET` | `/wallet/{agent_id}/history` | Transaction history |
|
|
41
|
+
| `POST` | `/wallet/{agent_id}/topup` | Add funds |
|
|
42
|
+
|
|
43
|
+
### Auth
|
|
44
|
+
|
|
45
|
+
| Method | Path | Description |
|
|
46
|
+
|--------|------|-------------|
|
|
47
|
+
| `GET` | `/auth/me` | Verify token, get current user |
|
|
48
|
+
| `POST` | `/auth/login` | Login |
|
|
49
|
+
| `POST` | `/auth/signup` | Register |
|
|
50
|
+
|
|
51
|
+
## Authentication
|
|
52
|
+
|
|
53
|
+
All authenticated endpoints use `Authorization: Bearer <api_key>` header. The API key is verified against the `api_keys` table via PBKDF2 hash comparison. Keys are issued per-org from the Agent-net dashboard.
|
|
54
|
+
|
|
55
|
+
## Response Formats
|
|
56
|
+
|
|
57
|
+
Wallet balance:
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"wallet_id": "...",
|
|
61
|
+
"agent_id": "...",
|
|
62
|
+
"balance_minor": 50000,
|
|
63
|
+
"currency": "INR",
|
|
64
|
+
"status": "active"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Discovery results:
|
|
69
|
+
```json
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
"agent_id": "...",
|
|
73
|
+
"name": "TranslatorBot",
|
|
74
|
+
"description": "...",
|
|
75
|
+
"capabilities": [...],
|
|
76
|
+
"sponsored": false
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlatformError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _validate_path_segment(value: str) -> None:
|
|
14
|
+
"""Reject values that could cause path traversal or injection."""
|
|
15
|
+
if not re.fullmatch(r"[a-zA-Z0-9_-]+", value):
|
|
16
|
+
raise PlatformError(f"Invalid identifier: {value!r}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PlatformClient:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
base_url: str,
|
|
24
|
+
api_token: str,
|
|
25
|
+
http_client: httpx.Client | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._base = base_url.rstrip("/")
|
|
28
|
+
self._token = api_token
|
|
29
|
+
self._http = http_client or httpx.Client(timeout=30.0)
|
|
30
|
+
|
|
31
|
+
# -- context manager & cleanup (L-4) --
|
|
32
|
+
|
|
33
|
+
def close(self) -> None:
|
|
34
|
+
self._http.close()
|
|
35
|
+
|
|
36
|
+
def __enter__(self) -> "PlatformClient":
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def __exit__(self, *args: Any) -> None:
|
|
40
|
+
self.close()
|
|
41
|
+
|
|
42
|
+
# -- internal helpers --
|
|
43
|
+
|
|
44
|
+
def _headers(self) -> dict[str, str]:
|
|
45
|
+
from .. import __version__ # noqa: PLC0415
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"Authorization": f"Bearer {self._token}",
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"User-Agent": f"agentnet-cli/{__version__}",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def _handle_response(self, resp: httpx.Response) -> dict[str, Any]:
|
|
54
|
+
"""Raise PlatformError on HTTP errors; safely parse JSON (H-11)."""
|
|
55
|
+
try:
|
|
56
|
+
resp.raise_for_status()
|
|
57
|
+
except httpx.HTTPStatusError:
|
|
58
|
+
status = resp.status_code
|
|
59
|
+
if status in (401, 403):
|
|
60
|
+
raise PlatformError("Authentication failed") from None
|
|
61
|
+
if status == 429:
|
|
62
|
+
raise PlatformError("Rate limited, try again later") from None
|
|
63
|
+
if 500 <= status < 600:
|
|
64
|
+
raise PlatformError("Platform server error") from None
|
|
65
|
+
raise PlatformError(f"Request failed ({status})") from None
|
|
66
|
+
try:
|
|
67
|
+
return resp.json()
|
|
68
|
+
except ValueError:
|
|
69
|
+
raise PlatformError("Invalid response from platform") from None
|
|
70
|
+
|
|
71
|
+
def _get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
72
|
+
resp = self._http.get(f"{self._base}{path}", headers=self._headers(), params=params)
|
|
73
|
+
return self._handle_response(resp)
|
|
74
|
+
|
|
75
|
+
def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
+
resp = self._http.post(f"{self._base}{path}", headers=self._headers(), json=body)
|
|
77
|
+
return self._handle_response(resp)
|
|
78
|
+
|
|
79
|
+
def discover(self, *, query: str, category: str | None = None, max_results: int = 5, max_price: int | None = None) -> dict[str, Any]:
|
|
80
|
+
params: dict[str, Any] = {"q": query, "limit": max_results}
|
|
81
|
+
if category:
|
|
82
|
+
params["category"] = category
|
|
83
|
+
if max_price is not None:
|
|
84
|
+
params["max_price"] = max_price
|
|
85
|
+
return self._get("/discover/listings", params)
|
|
86
|
+
|
|
87
|
+
def discover_agents(self, *, query: str, limit: int = 20) -> dict[str, Any]:
|
|
88
|
+
return self._get("/discover/", {"q": query, "limit": limit})
|
|
89
|
+
|
|
90
|
+
def get_agent(self, *, agent_id: str) -> dict[str, Any]:
|
|
91
|
+
_validate_path_segment(agent_id)
|
|
92
|
+
return self._get(f"/agents/{agent_id}")
|
|
93
|
+
|
|
94
|
+
def list_agents(self) -> dict[str, Any]:
|
|
95
|
+
return self._get("/agents/")
|
|
96
|
+
|
|
97
|
+
def use_agent(self, *, agent_id: str, task: str, quote_id: str | None = None, max_amount: float = 0) -> dict[str, Any]:
|
|
98
|
+
_validate_path_segment(agent_id)
|
|
99
|
+
body: dict[str, Any] = {"message": task, "amount": max_amount}
|
|
100
|
+
return self._post(f"/agents/{agent_id}/use", body)
|
|
101
|
+
|
|
102
|
+
def continue_session(self, *, session_id: str, message: str) -> dict[str, Any]:
|
|
103
|
+
return self._post(f"/agents/sessions/{session_id}/continue", {"message": message})
|
|
104
|
+
|
|
105
|
+
def settle_session(self, *, session_id: str) -> dict[str, Any]:
|
|
106
|
+
return self._post(f"/agents/sessions/{session_id}/settle", {})
|
|
107
|
+
|
|
108
|
+
def wallet_balance(self, *, agent_id: str) -> dict[str, Any]:
|
|
109
|
+
_validate_path_segment(agent_id)
|
|
110
|
+
return self._get(f"/wallet/{agent_id}")
|
|
111
|
+
|
|
112
|
+
def wallet_history(self, *, agent_id: str, limit: int = 50) -> dict[str, Any]:
|
|
113
|
+
_validate_path_segment(agent_id)
|
|
114
|
+
return self._get(f"/wallet/{agent_id}/history", {"limit": limit})
|
|
115
|
+
|
|
116
|
+
def wallet_topup(self, *, agent_id: str, amount: float) -> dict[str, Any]:
|
|
117
|
+
_validate_path_segment(agent_id)
|
|
118
|
+
return self._post(f"/wallet/{agent_id}/topup", {"amount": amount})
|
|
119
|
+
|
|
120
|
+
def verify_token(self) -> dict[str, Any]:
|
|
121
|
+
return self._get("/auth/me")
|
|
122
|
+
|
|
123
|
+
def token_info(self) -> dict[str, Any]:
|
|
124
|
+
return self._get("/auth/token-info")
|
|
125
|
+
|
|
126
|
+
def cli_register_agent(
|
|
127
|
+
self, *, name: str, visibility: str = "private", description: str = "", url: str = "", tags: list[str] | None = None,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
body: dict[str, Any] = {"name": name, "visibility": visibility, "description": description}
|
|
130
|
+
if url:
|
|
131
|
+
body["url"] = url
|
|
132
|
+
if tags:
|
|
133
|
+
body["tags"] = tags
|
|
134
|
+
return self._post("/auth/cli/register-agent", body)
|