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.
Files changed (53) hide show
  1. agentnet_cli/__init__.py +6 -0
  2. agentnet_cli/agents/README.md +55 -0
  3. agentnet_cli/agents/__init__.py +0 -0
  4. agentnet_cli/agents/base.py +37 -0
  5. agentnet_cli/agents/claude.py +114 -0
  6. agentnet_cli/agents/codex.py +78 -0
  7. agentnet_cli/agents/copilot.py +89 -0
  8. agentnet_cli/agents/cursor.py +90 -0
  9. agentnet_cli/agents/hermes.py +171 -0
  10. agentnet_cli/agents/openclaw.py +54 -0
  11. agentnet_cli/agents/registry.py +28 -0
  12. agentnet_cli/agents/shims.py +9 -0
  13. agentnet_cli/agents/vscode.py +119 -0
  14. agentnet_cli/commands/__init__.py +0 -0
  15. agentnet_cli/commands/agent.py +32 -0
  16. agentnet_cli/commands/discover.py +34 -0
  17. agentnet_cli/commands/session.py +35 -0
  18. agentnet_cli/commands/wallet.py +48 -0
  19. agentnet_cli/config.py +68 -0
  20. agentnet_cli/connect.py +73 -0
  21. agentnet_cli/detect.py +23 -0
  22. agentnet_cli/disconnect.py +60 -0
  23. agentnet_cli/hermes_plugin/__init__.py +36 -0
  24. agentnet_cli/hermes_plugin/handlers.py +69 -0
  25. agentnet_cli/hermes_plugin/plugin.yaml +17 -0
  26. agentnet_cli/hermes_plugin/schemas.py +182 -0
  27. agentnet_cli/hermes_plugin/skills/agentnet/SKILL.md +46 -0
  28. agentnet_cli/main.py +263 -0
  29. agentnet_cli/manifest.py +58 -0
  30. agentnet_cli/marketplace.py +39 -0
  31. agentnet_cli/mcp/README.md +64 -0
  32. agentnet_cli/mcp/__init__.py +0 -0
  33. agentnet_cli/mcp/server.py +244 -0
  34. agentnet_cli/mcp/tools.py +67 -0
  35. agentnet_cli/paths.py +91 -0
  36. agentnet_cli/platform/README.md +79 -0
  37. agentnet_cli/platform/__init__.py +0 -0
  38. agentnet_cli/platform/client.py +134 -0
  39. agentnet_cli/register.py +146 -0
  40. agentnet_cli/shims/README.md +48 -0
  41. agentnet_cli/shims/SKILL.md +225 -0
  42. agentnet_cli/shims/codex/skill.md +8 -0
  43. agentnet_cli/shims/copilot/agentnet.agent.md +14 -0
  44. agentnet_cli/shims/cursor/agent.md +9 -0
  45. agentnet_cli/shims/cursor/agentnet.mdc +8 -0
  46. agentnet_cli/shims/shared/context.md +62 -0
  47. agentnet_cli/shims/vscode/instructions.md +3 -0
  48. agentnet_cli/status.py +65 -0
  49. agentnet_cli/updater.py +123 -0
  50. agentnet_cli-0.1.0.dist-info/METADATA +253 -0
  51. agentnet_cli-0.1.0.dist-info/RECORD +53 -0
  52. agentnet_cli-0.1.0.dist-info/WHEEL +4 -0
  53. 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)