lime-agents-sdk 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.
- lime_agents/__init__.py +25 -0
- lime_agents/_agent.py +87 -0
- lime_agents/_client.py +151 -0
- lime_agents/_errors.py +52 -0
- lime_agents/_pow.py +30 -0
- lime_agents/_types.py +53 -0
- lime_agents/py.typed +0 -0
- lime_agents_sdk-0.1.0.dist-info/METADATA +394 -0
- lime_agents_sdk-0.1.0.dist-info/RECORD +11 -0
- lime_agents_sdk-0.1.0.dist-info/WHEEL +4 -0
- lime_agents_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
lime_agents/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Official Python SDK for LIME AI agent workers."""
|
|
2
|
+
|
|
3
|
+
from lime_agents._agent import LimeAgent
|
|
4
|
+
from lime_agents._errors import (
|
|
5
|
+
ApiError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
LimeError,
|
|
8
|
+
PowTimeoutError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
)
|
|
11
|
+
from lime_agents._types import AgentProfile, ApprovalResult
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AgentProfile",
|
|
17
|
+
"ApiError",
|
|
18
|
+
"ApprovalResult",
|
|
19
|
+
"AuthenticationError",
|
|
20
|
+
"LimeAgent",
|
|
21
|
+
"LimeError",
|
|
22
|
+
"PowTimeoutError",
|
|
23
|
+
"RateLimitError",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
lime_agents/_agent.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from lime_agents._client import LimeClient
|
|
10
|
+
from lime_agents._errors import AuthenticationError
|
|
11
|
+
from lime_agents._pow import solve
|
|
12
|
+
from lime_agents._types import AgentProfile, ApprovalResult
|
|
13
|
+
|
|
14
|
+
_DEFAULT_BASE_URL = "https://lime.pics/api/v1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LimeAgent:
|
|
18
|
+
"""Async client for LIME agent workers."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
agent_token: str | None = None,
|
|
24
|
+
base_url: str | None = None,
|
|
25
|
+
timeout: float = 30.0,
|
|
26
|
+
max_retries: int = 3,
|
|
27
|
+
pow_timeout: float = 10.0,
|
|
28
|
+
http_client: httpx.AsyncClient | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
resolved_token = (agent_token or os.getenv("LIME_AGENT_TOKEN") or "").strip()
|
|
31
|
+
if not resolved_token:
|
|
32
|
+
raise AuthenticationError(
|
|
33
|
+
"Agent token is required. Pass agent_token= or set LIME_AGENT_TOKEN.",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
resolved_base = (
|
|
37
|
+
base_url
|
|
38
|
+
or os.getenv("LIME_API_BASE")
|
|
39
|
+
or _DEFAULT_BASE_URL
|
|
40
|
+
).rstrip("/")
|
|
41
|
+
|
|
42
|
+
self._pow_timeout = pow_timeout
|
|
43
|
+
self._client = LimeClient(
|
|
44
|
+
agent_token=resolved_token,
|
|
45
|
+
base_url=resolved_base,
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
max_retries=max_retries,
|
|
48
|
+
http_client=http_client,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def __aenter__(self) -> LimeAgent:
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
async def __aexit__(
|
|
55
|
+
self,
|
|
56
|
+
exc_type: type[BaseException] | None,
|
|
57
|
+
exc: BaseException | None,
|
|
58
|
+
tb: TracebackType | None,
|
|
59
|
+
) -> None:
|
|
60
|
+
await self.aclose()
|
|
61
|
+
|
|
62
|
+
async def aclose(self) -> None:
|
|
63
|
+
await self._client.aclose()
|
|
64
|
+
|
|
65
|
+
async def approve(self, request_id: str) -> ApprovalResult:
|
|
66
|
+
"""Fetch PoW challenge, solve it, and approve the login request."""
|
|
67
|
+
challenge_data = await self._client.get_public(f"/auth/requests/{request_id}")
|
|
68
|
+
pow_challenge = str(challenge_data["pow_challenge"])
|
|
69
|
+
pow_difficulty = int(challenge_data["pow_difficulty"])
|
|
70
|
+
|
|
71
|
+
pow_nonce = await asyncio.to_thread(
|
|
72
|
+
solve,
|
|
73
|
+
pow_challenge,
|
|
74
|
+
pow_difficulty,
|
|
75
|
+
max_timeout=self._pow_timeout,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
approve_data = await self._client.post(
|
|
79
|
+
f"/modules/agent-login/requests/{request_id}/approve",
|
|
80
|
+
{"pow_nonce": pow_nonce},
|
|
81
|
+
)
|
|
82
|
+
return ApprovalResult.from_api(approve_data)
|
|
83
|
+
|
|
84
|
+
async def get_profile(self) -> AgentProfile:
|
|
85
|
+
"""Return the authenticated agent's Core profile."""
|
|
86
|
+
profile_data = await self._client.get("/core/agents/me/profile")
|
|
87
|
+
return AgentProfile.from_api(profile_data)
|
lime_agents/_client.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import random
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from lime_agents._errors import ApiError, AuthenticationError, LimeError, RateLimitError
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("lime")
|
|
13
|
+
|
|
14
|
+
_RETRYABLE_STATUS = frozenset({408, 429, 500, 502, 503, 504})
|
|
15
|
+
_AUTH_CODES = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"MISSING_AGENT_TOKEN",
|
|
18
|
+
"INVALID_AGENT_TOKEN",
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LimeClient:
|
|
24
|
+
"""Internal HTTP client with envelope parsing and retries."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
agent_token: str,
|
|
30
|
+
base_url: str,
|
|
31
|
+
timeout: float,
|
|
32
|
+
max_retries: int,
|
|
33
|
+
http_client: httpx.AsyncClient | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._agent_token = agent_token
|
|
36
|
+
self._base_url = base_url.rstrip("/")
|
|
37
|
+
self._max_retries = max_retries
|
|
38
|
+
self._owns_client = http_client is None
|
|
39
|
+
self._client = http_client or httpx.AsyncClient(
|
|
40
|
+
timeout=timeout,
|
|
41
|
+
headers={"Accept": "application/json"},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def aclose(self) -> None:
|
|
45
|
+
if self._owns_client:
|
|
46
|
+
await self._client.aclose()
|
|
47
|
+
|
|
48
|
+
async def get_public(self, path: str) -> dict[str, Any]:
|
|
49
|
+
return await self._request("GET", path, authenticated=False)
|
|
50
|
+
|
|
51
|
+
async def get(self, path: str) -> dict[str, Any]:
|
|
52
|
+
return await self._request("GET", path, authenticated=True)
|
|
53
|
+
|
|
54
|
+
async def post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
return await self._request("POST", path, authenticated=True, json_body=body)
|
|
56
|
+
|
|
57
|
+
async def _request(
|
|
58
|
+
self,
|
|
59
|
+
method: str,
|
|
60
|
+
path: str,
|
|
61
|
+
*,
|
|
62
|
+
authenticated: bool,
|
|
63
|
+
json_body: dict[str, Any] | None = None,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
url = f"{self._base_url}/{path.lstrip('/')}"
|
|
66
|
+
headers: dict[str, str] = {}
|
|
67
|
+
if authenticated:
|
|
68
|
+
headers["X-Agent-Token"] = self._agent_token
|
|
69
|
+
headers["Content-Type"] = "application/json"
|
|
70
|
+
|
|
71
|
+
attempt = 0
|
|
72
|
+
while True:
|
|
73
|
+
try:
|
|
74
|
+
response = await self._send(method, url, headers=headers, json_body=json_body)
|
|
75
|
+
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
|
76
|
+
if attempt >= self._max_retries:
|
|
77
|
+
raise LimeError(str(exc)) from exc
|
|
78
|
+
await self._backoff(attempt, method, path)
|
|
79
|
+
attempt += 1
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if response.status_code in _RETRYABLE_STATUS and attempt < self._max_retries:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"Retrying %s %s after HTTP %s (attempt %s)",
|
|
85
|
+
method,
|
|
86
|
+
path,
|
|
87
|
+
response.status_code,
|
|
88
|
+
attempt + 1,
|
|
89
|
+
)
|
|
90
|
+
await self._backoff(attempt, method, path)
|
|
91
|
+
attempt += 1
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
return self._parse_envelope(response)
|
|
95
|
+
|
|
96
|
+
async def _send(
|
|
97
|
+
self,
|
|
98
|
+
method: str,
|
|
99
|
+
url: str,
|
|
100
|
+
*,
|
|
101
|
+
headers: dict[str, str],
|
|
102
|
+
json_body: dict[str, Any] | None,
|
|
103
|
+
) -> httpx.Response:
|
|
104
|
+
logger.debug("%s %s", method, url)
|
|
105
|
+
if method == "GET":
|
|
106
|
+
return await self._client.get(url, headers=headers)
|
|
107
|
+
return await self._client.post(url, headers=headers, json=json_body)
|
|
108
|
+
|
|
109
|
+
async def _backoff(self, attempt: int, method: str, path: str) -> None:
|
|
110
|
+
delay = (2**attempt) * 0.25 + random.uniform(0, 0.1)
|
|
111
|
+
logger.warning("Backing off %.2fs before retry %s %s", delay, method, path)
|
|
112
|
+
await asyncio.sleep(delay)
|
|
113
|
+
|
|
114
|
+
def _parse_envelope(self, response: httpx.Response) -> dict[str, Any]:
|
|
115
|
+
status = response.status_code
|
|
116
|
+
try:
|
|
117
|
+
payload = response.json()
|
|
118
|
+
except ValueError as exc:
|
|
119
|
+
raise LimeError(
|
|
120
|
+
f"Invalid JSON response (HTTP {status})",
|
|
121
|
+
http_status=status,
|
|
122
|
+
) from exc
|
|
123
|
+
|
|
124
|
+
if not isinstance(payload, dict):
|
|
125
|
+
raise LimeError(f"Unexpected response shape (HTTP {status})", http_status=status)
|
|
126
|
+
|
|
127
|
+
if payload.get("ok") is True:
|
|
128
|
+
data = payload.get("data")
|
|
129
|
+
if not isinstance(data, dict):
|
|
130
|
+
raise LimeError("Success envelope missing data object", http_status=status)
|
|
131
|
+
return data
|
|
132
|
+
|
|
133
|
+
error = payload.get("error")
|
|
134
|
+
if not isinstance(error, dict):
|
|
135
|
+
raise LimeError(
|
|
136
|
+
f"Error envelope missing error object (HTTP {status})",
|
|
137
|
+
http_status=status,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
code = str(error.get("code", "UNKNOWN_ERROR"))
|
|
141
|
+
message = str(error.get("message", "Unknown error"))
|
|
142
|
+
detail = error.get("detail")
|
|
143
|
+
detail_dict = detail if isinstance(detail, dict) else None
|
|
144
|
+
|
|
145
|
+
if status == 429 or code == "RATE_LIMIT_EXCEEDED":
|
|
146
|
+
raise RateLimitError(message, code=code, http_status=status, detail=detail_dict)
|
|
147
|
+
|
|
148
|
+
if status == 401 or code in _AUTH_CODES:
|
|
149
|
+
raise AuthenticationError(message, code=code, http_status=status, detail=detail_dict)
|
|
150
|
+
|
|
151
|
+
raise ApiError(code, message, http_status=status, detail=detail_dict)
|
lime_agents/_errors.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LimeError(Exception):
|
|
7
|
+
"""Base class for SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
*,
|
|
13
|
+
code: str | None = None,
|
|
14
|
+
http_status: int | None = None,
|
|
15
|
+
detail: dict[str, Any] | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
self.message = message
|
|
18
|
+
self.code = code
|
|
19
|
+
self.http_status = http_status
|
|
20
|
+
self.detail = detail
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthenticationError(LimeError):
|
|
25
|
+
"""Missing or invalid agent token."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PowTimeoutError(LimeError):
|
|
29
|
+
"""PoW was not solved within the allotted time."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RateLimitError(LimeError):
|
|
33
|
+
"""HTTP 429 — rate limit exceeded."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ApiError(LimeError):
|
|
37
|
+
"""General API error with code and message."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
code: str,
|
|
42
|
+
message: str,
|
|
43
|
+
*,
|
|
44
|
+
http_status: int,
|
|
45
|
+
detail: dict[str, Any] | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
super().__init__(
|
|
48
|
+
message,
|
|
49
|
+
code=code,
|
|
50
|
+
http_status=http_status,
|
|
51
|
+
detail=detail,
|
|
52
|
+
)
|
lime_agents/_pow.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from lime_agents._errors import PowTimeoutError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _is_valid_pow(challenge: str, nonce: str, difficulty: int) -> bool:
|
|
10
|
+
digest_hex = hashlib.sha256(f"{challenge}{nonce}".encode()).hexdigest()
|
|
11
|
+
threshold = 2 ** (256 - difficulty)
|
|
12
|
+
return bool(int(digest_hex, 16) < threshold)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def solve(challenge: str, difficulty: int, *, max_timeout: float = 10.0) -> str:
|
|
16
|
+
"""Find a nonce satisfying the LIME PoW policy."""
|
|
17
|
+
if difficulty <= 0:
|
|
18
|
+
return "0"
|
|
19
|
+
|
|
20
|
+
deadline = time.monotonic() + max_timeout
|
|
21
|
+
nonce = 0
|
|
22
|
+
while time.monotonic() < deadline:
|
|
23
|
+
candidate = str(nonce)
|
|
24
|
+
if _is_valid_pow(challenge, candidate, difficulty):
|
|
25
|
+
return candidate
|
|
26
|
+
nonce += 1
|
|
27
|
+
|
|
28
|
+
raise PowTimeoutError(
|
|
29
|
+
f"PoW not solved within {max_timeout}s (difficulty={difficulty})",
|
|
30
|
+
)
|
lime_agents/_types.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _parse_datetime(value: str) -> datetime:
|
|
9
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class ApprovalResult:
|
|
14
|
+
request_id: str
|
|
15
|
+
site_id: str
|
|
16
|
+
status: str
|
|
17
|
+
expires_at: datetime
|
|
18
|
+
approved_agent_id: str | None = None
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_api(cls, data: dict[str, Any]) -> ApprovalResult:
|
|
22
|
+
return cls(
|
|
23
|
+
request_id=str(data["request_id"]),
|
|
24
|
+
site_id=str(data["site_id"]),
|
|
25
|
+
status=str(data["status"]),
|
|
26
|
+
expires_at=_parse_datetime(str(data["expires_at"])),
|
|
27
|
+
approved_agent_id=(
|
|
28
|
+
str(data["approved_agent_id"]) if data.get("approved_agent_id") else None
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class AgentProfile:
|
|
35
|
+
agent_id: str
|
|
36
|
+
owner_id: str
|
|
37
|
+
display_name: str | None
|
|
38
|
+
avatar_url: str | None
|
|
39
|
+
description: str | None
|
|
40
|
+
owner_kyc_level: int | None
|
|
41
|
+
agent_reputation: int | None
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_api(cls, data: dict[str, Any]) -> AgentProfile:
|
|
45
|
+
return cls(
|
|
46
|
+
agent_id=str(data["agent_id"]),
|
|
47
|
+
owner_id=str(data["owner_id"]),
|
|
48
|
+
display_name=data.get("display_name"),
|
|
49
|
+
avatar_url=data.get("avatar_url"),
|
|
50
|
+
description=data.get("description"),
|
|
51
|
+
owner_kyc_level=data.get("owner_kyc_level"),
|
|
52
|
+
agent_reputation=data.get("agent_reputation"),
|
|
53
|
+
)
|
lime_agents/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lime-agents-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for LIME 2.0 agent authentication. Zero-config, Proof-of-Work auto-solve, async-first.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Mawyxx/lime-agents-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/Mawyxx/lime-agents-sdk
|
|
7
|
+
Project-URL: Documentation, https://lime.pics/docs
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: httpx<1,>=0.27.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# LIME Agents SDK
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/lime-agents-sdk/)
|
|
23
|
+
[](https://pypi.org/project/lime-agents-sdk/)
|
|
24
|
+
[](LICENSE)
|
|
25
|
+
[](https://github.com/Mawyxx/lime-agents-sdk/actions/workflows/ci.yml)
|
|
26
|
+
|
|
27
|
+
Official Python SDK for [LIME](https://lime.pics) agent workers. Async-first client that performs site-login approval end-to-end: fetch Proof-of-Work challenge, solve SHA-256 PoW, submit approve with retries.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install lime-agents-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Install the latest commit from GitHub:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install git+https://github.com/Mawyxx/lime-agents-sdk.git
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Requirements:** Python 3.10+
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
Examples use readable names (`Lime`, `login`) on top of the shipped API (`LimeAgent`, `approve`). See [API reference](#api-reference) for exact types and parameters.
|
|
46
|
+
|
|
47
|
+
### Minimal
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from lime_agents import LimeAgent as _LimeAgent
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Lime(_LimeAgent):
|
|
54
|
+
"""aiogram-style client: token first, login() instead of approve()."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, token: str):
|
|
57
|
+
super().__init__(agent_token=token)
|
|
58
|
+
|
|
59
|
+
async def login(self, request_id: str):
|
|
60
|
+
return await self.approve(request_id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
AGENT_TOKEN = "at_..." # LIME Owner Portal → agent token (copy once)
|
|
64
|
+
|
|
65
|
+
async def login_to_site(request_id: str) -> str:
|
|
66
|
+
"""Agent confirms sign-in to a site. Returns status."""
|
|
67
|
+
lime = Lime(AGENT_TOKEN)
|
|
68
|
+
try:
|
|
69
|
+
result = await lime.login(request_id)
|
|
70
|
+
return result.status # "DELIVERED"
|
|
71
|
+
finally:
|
|
72
|
+
await lime.aclose()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Production
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from lime_agents import LimeAgent as _LimeAgent, PowTimeoutError, ApiError
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Lime(_LimeAgent):
|
|
82
|
+
def __init__(self, token: str):
|
|
83
|
+
super().__init__(agent_token=token)
|
|
84
|
+
|
|
85
|
+
async def login(self, request_id: str):
|
|
86
|
+
return await self.approve(request_id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
AGENT_TOKEN = "at_..."
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Agent:
|
|
93
|
+
"""Autonomous worker that signs in to sites when asked."""
|
|
94
|
+
|
|
95
|
+
def __init__(self):
|
|
96
|
+
self.lime = Lime(AGENT_TOKEN)
|
|
97
|
+
|
|
98
|
+
async def on_login_required(self, request_id: str) -> str | None:
|
|
99
|
+
"""Site requires login — agent confirms."""
|
|
100
|
+
try:
|
|
101
|
+
result = await self.lime.login(request_id)
|
|
102
|
+
return result.status
|
|
103
|
+
except PowTimeoutError:
|
|
104
|
+
# Proof-of-Work exceeded pow_timeout (default 10s) — retry once
|
|
105
|
+
try:
|
|
106
|
+
result = await self.lime.login(request_id)
|
|
107
|
+
return result.status
|
|
108
|
+
except PowTimeoutError:
|
|
109
|
+
return None
|
|
110
|
+
except ApiError as exc:
|
|
111
|
+
print(f"[{exc.code}] {exc.message}")
|
|
112
|
+
return None
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Authentication
|
|
116
|
+
|
|
117
|
+
The SDK authenticates agent HTTP calls with the `X-Agent-Token` header.
|
|
118
|
+
|
|
119
|
+
**Resolution order:**
|
|
120
|
+
|
|
121
|
+
1. Constructor argument `agent_token="at_..."`
|
|
122
|
+
2. Environment variable `LIME_AGENT_TOKEN`
|
|
123
|
+
|
|
124
|
+
If neither is set (or the value is empty after trimming), construction raises `AuthenticationError`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from lime_agents import LimeAgent, AuthenticationError
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
agent = LimeAgent()
|
|
131
|
+
except AuthenticationError as exc:
|
|
132
|
+
print(exc.message)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Obtain the agent token once when registering an agent in the LIME owner portal. Store it as a server-side secret in your worker environment.
|
|
136
|
+
|
|
137
|
+
## Integration pattern: Headless agent
|
|
138
|
+
|
|
139
|
+
Typical embedding: hold one `LimeAgent` per worker process and call `approve()` when a login request arrives.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from lime_agents import LimeAgent
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TradingAgent:
|
|
146
|
+
def __init__(self, token: str):
|
|
147
|
+
self.lime = LimeAgent(agent_token=token)
|
|
148
|
+
|
|
149
|
+
async def on_login_required(self, request_id: str) -> str:
|
|
150
|
+
result = await self.lime.approve(request_id)
|
|
151
|
+
return result.status
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The site backend creates the request (`POST /modules/agent-login/requests`), delivers `login_request_id` to your worker, and long-polls events until status becomes `DELIVERED`. Your worker only runs the approve step above.
|
|
155
|
+
|
|
156
|
+
## API reference
|
|
157
|
+
|
|
158
|
+
### `LimeAgent`
|
|
159
|
+
|
|
160
|
+
Async client for agent-runtime operations (approve login, read profile).
|
|
161
|
+
|
|
162
|
+
#### Constructor
|
|
163
|
+
|
|
164
|
+
All arguments are keyword-only.
|
|
165
|
+
|
|
166
|
+
| Parameter | Type | Default | Description |
|
|
167
|
+
|-----------|------|---------|-------------|
|
|
168
|
+
| `agent_token` | `str \| None` | `None` | Agent secret. Falls back to `LIME_AGENT_TOKEN`. |
|
|
169
|
+
| `base_url` | `str \| None` | `None` | API root including `/api/v1`. Falls back to `LIME_API_BASE`, then `https://lime.pics/api/v1`. |
|
|
170
|
+
| `timeout` | `float` | `30.0` | Per-request HTTP timeout in seconds (httpx). |
|
|
171
|
+
| `max_retries` | `int` | `3` | Maximum retries on transient network errors and HTTP 408/429/5xx. |
|
|
172
|
+
| `pow_timeout` | `float` | `10.0` | Wall-clock budget in seconds for the PoW solver loop. |
|
|
173
|
+
| `http_client` | `httpx.AsyncClient \| None` | `None` | Inject a custom async HTTP client (tests, corporate proxy/TLS). When omitted, the SDK creates and owns a client. |
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
agent = LimeAgent(
|
|
177
|
+
agent_token="at_live_...",
|
|
178
|
+
base_url="https://lime.pics/api/v1",
|
|
179
|
+
timeout=60.0,
|
|
180
|
+
max_retries=5,
|
|
181
|
+
pow_timeout=15.0,
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Context manager:** `async with LimeAgent() as agent:` calls `aclose()` on exit. Call `await agent.aclose()` manually when not using a context manager.
|
|
186
|
+
|
|
187
|
+
#### `async approve(request_id: str) -> ApprovalResult`
|
|
188
|
+
|
|
189
|
+
Confirms a site login request on behalf of the agent.
|
|
190
|
+
|
|
191
|
+
**Internal steps:**
|
|
192
|
+
|
|
193
|
+
1. `GET /auth/requests/{request_id}` (public, no auth) — read `pow_challenge`, `pow_difficulty`
|
|
194
|
+
2. Solve PoW: find `nonce` such that `int(SHA256(challenge + nonce), 16) < 2**(256 - difficulty)`
|
|
195
|
+
3. `POST /modules/agent-login/requests/{request_id}/approve` with `X-Agent-Token` and body `{"pow_nonce": "<nonce>"}`
|
|
196
|
+
|
|
197
|
+
**Parameters:**
|
|
198
|
+
|
|
199
|
+
| Name | Type | Description |
|
|
200
|
+
|------|------|-------------|
|
|
201
|
+
| `request_id` | `str` | Login request ID from the site backend (`login_request_id` from create). |
|
|
202
|
+
|
|
203
|
+
**Returns:** `ApprovalResult` with FSM status (typically `DELIVERED` after successful approve).
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from lime_agents import LimeAgent, PowTimeoutError, ApiError
|
|
207
|
+
|
|
208
|
+
async with LimeAgent() as agent:
|
|
209
|
+
try:
|
|
210
|
+
result = await agent.approve("550e8400-e29b-41d4-a716-446655440000")
|
|
211
|
+
print(result.status, result.approved_agent_id)
|
|
212
|
+
except PowTimeoutError:
|
|
213
|
+
print("PoW not solved in time; increase pow_timeout or retry")
|
|
214
|
+
except ApiError as exc:
|
|
215
|
+
print(exc.code, exc.http_status, exc.message)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### `async get_profile() -> AgentProfile`
|
|
219
|
+
|
|
220
|
+
Returns the authenticated agent's Core profile.
|
|
221
|
+
|
|
222
|
+
**HTTP:** `GET /core/agents/me/profile` with `X-Agent-Token`.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
async with LimeAgent() as agent:
|
|
226
|
+
profile = await agent.get_profile()
|
|
227
|
+
print(profile.agent_id)
|
|
228
|
+
print(profile.owner_kyc_level)
|
|
229
|
+
print(profile.agent_reputation)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Types
|
|
233
|
+
|
|
234
|
+
### `ApprovalResult`
|
|
235
|
+
|
|
236
|
+
Frozen dataclass returned by `approve()`.
|
|
237
|
+
|
|
238
|
+
| Field | Type | Description |
|
|
239
|
+
|-------|------|-------------|
|
|
240
|
+
| `request_id` | `str` | Login request ID |
|
|
241
|
+
| `site_id` | `str` | Site that created the request |
|
|
242
|
+
| `status` | `str` | FSM value, e.g. `APPROVED`, `DELIVERED` |
|
|
243
|
+
| `expires_at` | `datetime` | Request expiry (timezone-aware when API sends offset) |
|
|
244
|
+
| `approved_agent_id` | `str \| None` | Agent that approved (set after approve) |
|
|
245
|
+
|
|
246
|
+
### `AgentProfile`
|
|
247
|
+
|
|
248
|
+
Frozen dataclass returned by `get_profile()`. Matches `GET /core/agents/me/profile` response fields.
|
|
249
|
+
|
|
250
|
+
| Field | Type | Description |
|
|
251
|
+
|-------|------|-------------|
|
|
252
|
+
| `agent_id` | `str` | Agent identifier |
|
|
253
|
+
| `owner_id` | `str` | Owning LIME user |
|
|
254
|
+
| `display_name` | `str \| None` | Public display name |
|
|
255
|
+
| `avatar_url` | `str \| None` | Avatar URL |
|
|
256
|
+
| `description` | `str \| None` | Public description |
|
|
257
|
+
| `owner_kyc_level` | `int \| None` | Owner KYC level synced from Foundation |
|
|
258
|
+
| `agent_reputation` | `int \| None` | Reputation score |
|
|
259
|
+
|
|
260
|
+
## Error handling
|
|
261
|
+
|
|
262
|
+
All SDK exceptions inherit from `LimeError`. Each carries `message`, and optionally `code`, `http_status`, and `detail` (API envelope).
|
|
263
|
+
|
|
264
|
+
| Exception | When |
|
|
265
|
+
|-----------|------|
|
|
266
|
+
| `LimeError` | Base class; transport failures after retries, malformed JSON |
|
|
267
|
+
| `AuthenticationError` | Missing/empty token at construct; HTTP 401; `MISSING_AGENT_TOKEN`, `INVALID_AGENT_TOKEN` |
|
|
268
|
+
| `PowTimeoutError` | PoW solver exceeded `pow_timeout` |
|
|
269
|
+
| `RateLimitError` | HTTP 429 / `RATE_LIMIT_EXCEEDED` |
|
|
270
|
+
| `ApiError` | Other API errors (`ok: false` envelope) |
|
|
271
|
+
|
|
272
|
+
`ApiError` attributes: `code`, `message`, `http_status`, `detail`.
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
import asyncio
|
|
276
|
+
|
|
277
|
+
from lime_agents import (
|
|
278
|
+
LimeAgent,
|
|
279
|
+
LimeError,
|
|
280
|
+
AuthenticationError,
|
|
281
|
+
PowTimeoutError,
|
|
282
|
+
RateLimitError,
|
|
283
|
+
ApiError,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def run() -> None:
|
|
287
|
+
try:
|
|
288
|
+
async with LimeAgent() as agent:
|
|
289
|
+
await agent.approve("lr_abc123")
|
|
290
|
+
except AuthenticationError as exc:
|
|
291
|
+
print("auth:", exc.message)
|
|
292
|
+
except PowTimeoutError as exc:
|
|
293
|
+
print("pow:", exc.message)
|
|
294
|
+
except RateLimitError as exc:
|
|
295
|
+
print("rate limit:", exc.http_status)
|
|
296
|
+
except ApiError as exc:
|
|
297
|
+
print(f"api [{exc.http_status}] {exc.code}: {exc.message}")
|
|
298
|
+
except LimeError as exc:
|
|
299
|
+
print("sdk:", exc.message)
|
|
300
|
+
|
|
301
|
+
asyncio.run(run())
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Non-retried HTTP statuses:** 400, 401, 403, 404, 409 (e.g. `INVALID_POW`, `SITE_LOGIN_CONFLICT`).
|
|
305
|
+
|
|
306
|
+
## Configuration
|
|
307
|
+
|
|
308
|
+
### Environment variables
|
|
309
|
+
|
|
310
|
+
| Variable | Required | Description |
|
|
311
|
+
|----------|----------|-------------|
|
|
312
|
+
| `LIME_AGENT_TOKEN` | Yes (unless `agent_token=` passed) | Agent secret (`at_...`) |
|
|
313
|
+
| `LIME_API_BASE` | No | API root, e.g. `https://lime.pics/api/v1` |
|
|
314
|
+
|
|
315
|
+
### Constructor tuning
|
|
316
|
+
|
|
317
|
+
| Use case | Suggestion |
|
|
318
|
+
|----------|------------|
|
|
319
|
+
| Slow network | Increase `timeout` (e.g. `60.0`) |
|
|
320
|
+
| Flaky upstream | Increase `max_retries` (e.g. `5`) |
|
|
321
|
+
| High PoW difficulty / slow CPU | Increase `pow_timeout` (e.g. `30.0`) |
|
|
322
|
+
| Staging / self-hosted API | Set `base_url` or `LIME_API_BASE` |
|
|
323
|
+
|
|
324
|
+
### Logging
|
|
325
|
+
|
|
326
|
+
HTTP and retry events are logged under the **`lime`** logger (not `lime_agents`):
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
import logging
|
|
330
|
+
|
|
331
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
332
|
+
logging.getLogger("lime").setLevel(logging.DEBUG)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
At `DEBUG`, the client logs request method and URL. Tokens, `pow_challenge`, and `pow_nonce` are never logged.
|
|
336
|
+
|
|
337
|
+
## Advanced usage
|
|
338
|
+
|
|
339
|
+
### Custom `httpx.AsyncClient`
|
|
340
|
+
|
|
341
|
+
Inject a client for custom TLS, proxies, or tests. **You own the client lifecycle** when injecting; the SDK does not close an injected client.
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
import httpx
|
|
345
|
+
from lime_agents import LimeAgent
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def approve_with_proxy() -> None:
|
|
349
|
+
client = httpx.AsyncClient(
|
|
350
|
+
timeout=60.0,
|
|
351
|
+
verify="/path/to/corporate-ca.pem",
|
|
352
|
+
proxy="http://proxy.corp.example:8080",
|
|
353
|
+
)
|
|
354
|
+
agent = LimeAgent(agent_token="at_...", http_client=client)
|
|
355
|
+
try:
|
|
356
|
+
await agent.approve("lr_abc123")
|
|
357
|
+
finally:
|
|
358
|
+
await client.aclose()
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Retries and timeouts
|
|
362
|
+
|
|
363
|
+
Retries use exponential backoff with jitter on connection errors, timeouts, and HTTP 408, 429, 500, 502, 503, 504. Each retry attempt is bounded by `max_retries` (default 3).
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
agent = LimeAgent(
|
|
367
|
+
agent_token="at_...",
|
|
368
|
+
max_retries=5,
|
|
369
|
+
timeout=45.0,
|
|
370
|
+
pow_timeout=20.0,
|
|
371
|
+
)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### PoW debugging
|
|
375
|
+
|
|
376
|
+
PoW runs in a thread pool (`asyncio.to_thread`) so the event loop stays responsive. To observe HTTP flow (not nonce values):
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
import logging
|
|
380
|
+
|
|
381
|
+
logging.getLogger("lime").setLevel(logging.DEBUG)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
If `PowTimeoutError` occurs, increase `pow_timeout` or verify `pow_difficulty` from `GET /auth/requests/{id}` (default 15 on production).
|
|
385
|
+
|
|
386
|
+
## Links
|
|
387
|
+
|
|
388
|
+
- **SDK repository:** [github.com/Mawyxx/lime-agents-sdk](https://github.com/Mawyxx/lime-agents-sdk)
|
|
389
|
+
- **LIME API docs:** [lime.pics/docs](https://lime.pics/docs)
|
|
390
|
+
- **LIME platform:** [github.com/Mawyxx/Lime](https://github.com/Mawyxx/Lime)
|
|
391
|
+
|
|
392
|
+
## License
|
|
393
|
+
|
|
394
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
lime_agents/__init__.py,sha256=Qq1LkGhxCL7H7rsEmqyEjj_ibYpiubiQ0Sti4vBRIQ4,507
|
|
2
|
+
lime_agents/_agent.py,sha256=Bs8K8C1-9buQEhamEq5ZZK2erARzrbWwLTKryJzWuUI,2679
|
|
3
|
+
lime_agents/_client.py,sha256=lLysm5Y9WCRQCSXJo43iUaCCzrNsC4aiUXkDf6Fj4QQ,5122
|
|
4
|
+
lime_agents/_errors.py,sha256=Ffrugaa_Acnl8xxKCljg3uZTueWwvm8qhjneR90Gb_4,1132
|
|
5
|
+
lime_agents/_pow.py,sha256=IjfmcfihScceC3CoNXqNF7IzaXgpvI78Haryt_qiLzk,880
|
|
6
|
+
lime_agents/_types.py,sha256=K7uOBH9_nbInTOEH7k46JSUa8ehOjYcQwYsxYgaugh8,1559
|
|
7
|
+
lime_agents/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
lime_agents_sdk-0.1.0.dist-info/METADATA,sha256=9FG2FNsJ4xBoLndth22ypvilscv9Ae7w4iPz0q01k0I,12606
|
|
9
|
+
lime_agents_sdk-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
10
|
+
lime_agents_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=oMe8Sdbwa3jl3bGG50j_6QicG-Zm_aQwFMq60XW3H9M,1061
|
|
11
|
+
lime_agents_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LIME
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|