evolvingmachines-evolve 0.0.55.dev1355__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.
- bridge/__init__.py +5 -0
- bridge/dist/bridge.bundle.cjs +1275 -0
- evolve/__init__.py +819 -0
- evolve/_http.py +71 -0
- evolve/agent.py +896 -0
- evolve/bridge.py +509 -0
- evolve/browser_credentials.py +265 -0
- evolve/browser_profiles.py +95 -0
- evolve/config.py +600 -0
- evolve/hosted.py +8958 -0
- evolve/integrations.py +173 -0
- evolve/managed_secrets.py +175 -0
- evolve/pipeline/__init__.py +59 -0
- evolve/pipeline/pipeline.py +512 -0
- evolve/pipeline/types.py +286 -0
- evolve/prompts/__init__.py +132 -0
- evolve/prompts/agent_md/judge.md +30 -0
- evolve/prompts/agent_md/reduce.md +7 -0
- evolve/prompts/agent_md/verify.md +33 -0
- evolve/prompts/user/judge.md +1 -0
- evolve/prompts/user/retry_feedback.md +9 -0
- evolve/prompts/user/verify.md +1 -0
- evolve/py.typed +0 -0
- evolve/results.py +315 -0
- evolve/retry.py +133 -0
- evolve/schema.py +107 -0
- evolve/sessions_client.py +167 -0
- evolve/storage_client.py +178 -0
- evolve/swarm/__init__.py +75 -0
- evolve/swarm/results.py +140 -0
- evolve/swarm/swarm.py +2116 -0
- evolve/swarm/types.py +241 -0
- evolve/utils.py +227 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/METADATA +52 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/RECORD +38 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/WHEEL +5 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/licenses/LICENSE +201 -0
- evolvingmachines_evolve-0.0.55.dev1355.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Standalone browser credentials client."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from . import _http
|
|
13
|
+
from .config import BrowserCredentialsClientConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_DASHBOARD_URL = 'https://dashboard.evolvingmachines.ai'
|
|
17
|
+
BROWSER_AUTH_ALGORITHM = 'RSA-OAEP-256'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class BrowserCredentialMetadata:
|
|
22
|
+
id: str
|
|
23
|
+
website: str
|
|
24
|
+
account_label: str
|
|
25
|
+
email: str
|
|
26
|
+
enabled: bool
|
|
27
|
+
created_by: str
|
|
28
|
+
created_at: str
|
|
29
|
+
updated_at: str
|
|
30
|
+
last_used_at: Optional[str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class BrowserCredentialsPage:
|
|
35
|
+
credentials: List[BrowserCredentialMetadata]
|
|
36
|
+
total: int
|
|
37
|
+
count: int
|
|
38
|
+
offset: int
|
|
39
|
+
has_more: bool
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _metadata_from_dict(data: Dict[str, Any]) -> BrowserCredentialMetadata:
|
|
43
|
+
return BrowserCredentialMetadata(
|
|
44
|
+
id=data['id'],
|
|
45
|
+
website=data['website'],
|
|
46
|
+
account_label=data['account_label'],
|
|
47
|
+
email=data['email'],
|
|
48
|
+
enabled=bool(data.get('enabled', True)),
|
|
49
|
+
created_by=data.get('createdBy') or data.get('created_by') or 'user',
|
|
50
|
+
created_at=data.get('createdAt') or data.get('created_at') or '',
|
|
51
|
+
updated_at=data.get('updatedAt') or data.get('updated_at') or '',
|
|
52
|
+
last_used_at=data.get('lastUsedAt') or data.get('last_used_at'),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BrowserCredentialsClient:
|
|
57
|
+
"""List, create, and delete saved browser logins without returning passwords."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
config: Optional[BrowserCredentialsClientConfig] = None,
|
|
62
|
+
):
|
|
63
|
+
self.config = config or BrowserCredentialsClientConfig()
|
|
64
|
+
|
|
65
|
+
async def __aenter__(self):
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
69
|
+
await self.close()
|
|
70
|
+
|
|
71
|
+
async def close(self):
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
async def list(
|
|
75
|
+
self,
|
|
76
|
+
website: Optional[str] = None,
|
|
77
|
+
limit: Optional[int] = None,
|
|
78
|
+
offset: Optional[int] = None,
|
|
79
|
+
) -> BrowserCredentialsPage:
|
|
80
|
+
params = {}
|
|
81
|
+
if website is not None:
|
|
82
|
+
params['website'] = website
|
|
83
|
+
if limit is not None:
|
|
84
|
+
params['limit'] = str(limit)
|
|
85
|
+
if offset is not None:
|
|
86
|
+
params['offset'] = str(offset)
|
|
87
|
+
query = urllib.parse.urlencode(params)
|
|
88
|
+
result = await self._request_json(f'/api/browser-credentials{("?" + query) if query else ""}')
|
|
89
|
+
credentials = [_metadata_from_dict(item) for item in result.get('credentials', [])]
|
|
90
|
+
return BrowserCredentialsPage(
|
|
91
|
+
credentials=credentials,
|
|
92
|
+
total=int(result.get('total', len(credentials))),
|
|
93
|
+
count=int(result.get('count', len(credentials))),
|
|
94
|
+
offset=int(result.get('offset', offset or 0)),
|
|
95
|
+
has_more=bool(result.get('hasMore', result.get('has_more', False))),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def create(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
website: str,
|
|
102
|
+
account_label: str,
|
|
103
|
+
email: str,
|
|
104
|
+
password: str,
|
|
105
|
+
) -> Dict[str, Any]:
|
|
106
|
+
encrypted_password = await self._encrypt_password(password)
|
|
107
|
+
result = await self._request_json('/api/browser-credentials', method='POST', body={
|
|
108
|
+
'website': website,
|
|
109
|
+
'account_label': account_label,
|
|
110
|
+
'email': email,
|
|
111
|
+
'encryptedPassword': encrypted_password,
|
|
112
|
+
})
|
|
113
|
+
return {
|
|
114
|
+
'status': result['status'],
|
|
115
|
+
'credential': _metadata_from_dict(result['credential']),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async def delete(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
id: Optional[str] = None,
|
|
122
|
+
website: Optional[str] = None,
|
|
123
|
+
account_label: Optional[str] = None,
|
|
124
|
+
) -> Dict[str, bool]:
|
|
125
|
+
if id:
|
|
126
|
+
body = {'id': id}
|
|
127
|
+
elif website and account_label:
|
|
128
|
+
body = {'website': website, 'account_label': account_label}
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError('delete requires either id or website and account_label')
|
|
131
|
+
return await self._request_json('/api/browser-credentials', method='DELETE', body=body)
|
|
132
|
+
|
|
133
|
+
async def _encrypt_password(self, password: str) -> Dict[str, str]:
|
|
134
|
+
key = await self._request_json('/api/browser-credentials/public-key')
|
|
135
|
+
if key.get('algorithm') != BROWSER_AUTH_ALGORITHM:
|
|
136
|
+
raise ValueError('Unsupported browser credential encryption algorithm')
|
|
137
|
+
ciphertext = _rsa_oaep_sha256_encrypt(key['publicKey'], password.encode('utf-8'))
|
|
138
|
+
return {
|
|
139
|
+
'algorithm': BROWSER_AUTH_ALGORITHM,
|
|
140
|
+
'keyId': key['id'],
|
|
141
|
+
'ciphertext': _base64url(ciphertext),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async def _request_json(
|
|
145
|
+
self,
|
|
146
|
+
path: str,
|
|
147
|
+
method: str = 'GET',
|
|
148
|
+
body: Optional[Dict[str, Any]] = None,
|
|
149
|
+
) -> Dict[str, Any]:
|
|
150
|
+
return await asyncio.to_thread(
|
|
151
|
+
_http.request_json,
|
|
152
|
+
f'{_dashboard_base_url(self.config)}{path}',
|
|
153
|
+
api_key=_resolve_api_key(self.config),
|
|
154
|
+
error_prefix='Browser credentials',
|
|
155
|
+
method=method,
|
|
156
|
+
body=body,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _dashboard_base_url(config: BrowserCredentialsClientConfig) -> str:
|
|
161
|
+
return (config.dashboard_url or os.environ.get('EVOLVE_DASHBOARD_URL') or DEFAULT_DASHBOARD_URL).rstrip('/')
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _resolve_api_key(config: BrowserCredentialsClientConfig) -> str:
|
|
165
|
+
api_key = config.api_key or os.environ.get('EVOLVE_API_KEY')
|
|
166
|
+
if not api_key:
|
|
167
|
+
raise ValueError('Browser credentials require EVOLVE_API_KEY or BrowserCredentialsClientConfig(api_key=...)')
|
|
168
|
+
return api_key
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _base64url(data: bytes) -> str:
|
|
172
|
+
return base64.urlsafe_b64encode(data).decode('ascii').rstrip('=')
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _read_der_length(data: bytes, offset: int) -> Tuple[int, int]:
|
|
176
|
+
first = data[offset]
|
|
177
|
+
offset += 1
|
|
178
|
+
if first < 0x80:
|
|
179
|
+
return first, offset
|
|
180
|
+
count = first & 0x7F
|
|
181
|
+
if count == 0 or count > 4:
|
|
182
|
+
raise ValueError('Unsupported DER length')
|
|
183
|
+
length = int.from_bytes(data[offset:offset + count], 'big')
|
|
184
|
+
return length, offset + count
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _read_der_tlv(data: bytes, offset: int, expected_tag: int) -> Tuple[bytes, int]:
|
|
188
|
+
if offset >= len(data) or data[offset] != expected_tag:
|
|
189
|
+
raise ValueError('Unexpected public key format')
|
|
190
|
+
length, value_start = _read_der_length(data, offset + 1)
|
|
191
|
+
value_end = value_start + length
|
|
192
|
+
if value_end > len(data):
|
|
193
|
+
raise ValueError('Invalid public key length')
|
|
194
|
+
return data[value_start:value_end], value_end
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# DER encoding of OID 1.2.840.113549.1.1.1 (rsaEncryption), the only
|
|
198
|
+
# SubjectPublicKeyInfo algorithm this encryptor may read: the BIT STRING that
|
|
199
|
+
# follows is parsed as an RSAPublicKey, and under any other algorithm those
|
|
200
|
+
# bytes mean something else entirely.
|
|
201
|
+
_RSA_ENCRYPTION_OID = b'\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _parse_rsa_public_key(pem: str) -> Tuple[int, int, int]:
|
|
205
|
+
body = ''.join(
|
|
206
|
+
line.strip()
|
|
207
|
+
for line in pem.splitlines()
|
|
208
|
+
if line and not line.startswith('-----')
|
|
209
|
+
)
|
|
210
|
+
der = base64.b64decode(body)
|
|
211
|
+
spki, offset = _read_der_tlv(der, 0, 0x30)
|
|
212
|
+
if offset != len(der):
|
|
213
|
+
raise ValueError('Unexpected trailing public key data')
|
|
214
|
+
algorithm, offset = _read_der_tlv(spki, 0, 0x30)
|
|
215
|
+
oid, _ = _read_der_tlv(algorithm, 0, 0x06)
|
|
216
|
+
if oid != _RSA_ENCRYPTION_OID:
|
|
217
|
+
raise ValueError('Public key algorithm is not rsaEncryption')
|
|
218
|
+
bit_string, offset = _read_der_tlv(spki, offset, 0x03)
|
|
219
|
+
if offset != len(spki) or not bit_string or bit_string[0] != 0:
|
|
220
|
+
raise ValueError('Invalid RSA public key')
|
|
221
|
+
rsa_key, offset = _read_der_tlv(bit_string[1:], 0, 0x30)
|
|
222
|
+
if offset != len(bit_string) - 1:
|
|
223
|
+
raise ValueError('Unexpected RSA key data')
|
|
224
|
+
modulus_bytes, offset = _read_der_tlv(rsa_key, 0, 0x02)
|
|
225
|
+
exponent_bytes, offset = _read_der_tlv(rsa_key, offset, 0x02)
|
|
226
|
+
if offset != len(rsa_key):
|
|
227
|
+
raise ValueError('Unexpected RSA integer data')
|
|
228
|
+
n = int.from_bytes(modulus_bytes.lstrip(b'\x00'), 'big')
|
|
229
|
+
e = int.from_bytes(exponent_bytes, 'big')
|
|
230
|
+
k = (n.bit_length() + 7) // 8
|
|
231
|
+
return n, e, k
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _mgf1(seed: bytes, length: int) -> bytes:
|
|
235
|
+
output = bytearray()
|
|
236
|
+
counter = 0
|
|
237
|
+
while len(output) < length:
|
|
238
|
+
output.extend(hashlib.sha256(seed + counter.to_bytes(4, 'big')).digest())
|
|
239
|
+
counter += 1
|
|
240
|
+
return bytes(output[:length])
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _xor_bytes(left: bytes, right: bytes) -> bytes:
|
|
244
|
+
# zip() silently truncates to the shorter operand, and a truncated XOR in
|
|
245
|
+
# OAEP is a malformed encoding, not an error anyone sees — refuse instead.
|
|
246
|
+
if len(left) != len(right):
|
|
247
|
+
raise ValueError('XOR operands must be the same length')
|
|
248
|
+
return bytes(a ^ b for a, b in zip(left, right))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _rsa_oaep_sha256_encrypt(public_key_pem: str, plaintext: bytes) -> bytes:
|
|
252
|
+
n, e, k = _parse_rsa_public_key(public_key_pem)
|
|
253
|
+
h_len = hashlib.sha256().digest_size
|
|
254
|
+
if len(plaintext) > k - 2 * h_len - 2:
|
|
255
|
+
raise ValueError('Browser credential password is too long for RSA-OAEP-256')
|
|
256
|
+
|
|
257
|
+
label_hash = hashlib.sha256(b'').digest()
|
|
258
|
+
padding = b'\x00' * (k - len(plaintext) - 2 * h_len - 2)
|
|
259
|
+
data_block = label_hash + padding + b'\x01' + plaintext
|
|
260
|
+
seed = secrets.token_bytes(h_len)
|
|
261
|
+
masked_data_block = _xor_bytes(data_block, _mgf1(seed, k - h_len - 1))
|
|
262
|
+
masked_seed = _xor_bytes(seed, _mgf1(masked_data_block, h_len))
|
|
263
|
+
encoded = b'\x00' + masked_seed + masked_data_block
|
|
264
|
+
cipher_int = pow(int.from_bytes(encoded, 'big'), e, n)
|
|
265
|
+
return cipher_int.to_bytes(k, 'big')
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Standalone browser profiles client."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from . import _http
|
|
9
|
+
from .config import BrowserProfilesClientConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_DASHBOARD_URL = 'https://dashboard.evolvingmachines.ai'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class BrowserProfileMetadata:
|
|
17
|
+
id: str
|
|
18
|
+
profile: str
|
|
19
|
+
created_at: str
|
|
20
|
+
updated_at: str
|
|
21
|
+
last_used_at: Optional[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class BrowserProfilesPage:
|
|
26
|
+
profiles: List[BrowserProfileMetadata]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _metadata_from_dict(data: Dict[str, Any]) -> BrowserProfileMetadata:
|
|
30
|
+
return BrowserProfileMetadata(
|
|
31
|
+
id=data['id'],
|
|
32
|
+
profile=data['profile'],
|
|
33
|
+
created_at=data.get('createdAt') or data.get('created_at') or '',
|
|
34
|
+
updated_at=data.get('updatedAt') or data.get('updated_at') or '',
|
|
35
|
+
last_used_at=data.get('lastUsedAt') or data.get('last_used_at'),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BrowserProfilesClient:
|
|
40
|
+
"""List and delete reusable browser profiles."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
config: Optional[BrowserProfilesClientConfig] = None,
|
|
45
|
+
):
|
|
46
|
+
self.config = config or BrowserProfilesClientConfig()
|
|
47
|
+
|
|
48
|
+
async def __aenter__(self):
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
52
|
+
await self.close()
|
|
53
|
+
|
|
54
|
+
async def close(self):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
async def list(self) -> BrowserProfilesPage:
|
|
58
|
+
result = await self._request_json('/api/browser-profiles')
|
|
59
|
+
return BrowserProfilesPage(
|
|
60
|
+
profiles=[_metadata_from_dict(item) for item in result.get('profiles', [])],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def delete(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
profile: str,
|
|
67
|
+
) -> Dict[str, bool]:
|
|
68
|
+
body: Dict[str, Any] = {'profile': profile}
|
|
69
|
+
return await self._request_json('/api/browser-profiles', method='DELETE', body=body)
|
|
70
|
+
|
|
71
|
+
async def _request_json(
|
|
72
|
+
self,
|
|
73
|
+
path: str,
|
|
74
|
+
method: str = 'GET',
|
|
75
|
+
body: Optional[Dict[str, Any]] = None,
|
|
76
|
+
) -> Dict[str, Any]:
|
|
77
|
+
return await asyncio.to_thread(
|
|
78
|
+
_http.request_json,
|
|
79
|
+
f'{_dashboard_base_url(self.config)}{path}',
|
|
80
|
+
api_key=_resolve_api_key(self.config),
|
|
81
|
+
error_prefix='Browser profiles',
|
|
82
|
+
method=method,
|
|
83
|
+
body=body,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _dashboard_base_url(config: BrowserProfilesClientConfig) -> str:
|
|
88
|
+
return (config.dashboard_url or os.environ.get('EVOLVE_DASHBOARD_URL') or DEFAULT_DASHBOARD_URL).rstrip('/')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _resolve_api_key(config: BrowserProfilesClientConfig) -> str:
|
|
92
|
+
api_key = config.api_key or os.environ.get('EVOLVE_API_KEY')
|
|
93
|
+
if not api_key:
|
|
94
|
+
raise ValueError('Browser profiles require EVOLVE_API_KEY or BrowserProfilesClientConfig(api_key=...)')
|
|
95
|
+
return api_key
|