prismnetwork 0.1.0__tar.gz
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.
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prismnetwork
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Headless GPU leasing on Prism Network for autonomous agents. Wallet-signature auth, on-chain USDG payment, SSH.
|
|
5
|
+
Project-URL: Homepage, https://prismnetwork.tech
|
|
6
|
+
Project-URL: Source, https://github.com/prismnetwork-tech/prism
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Keywords: agent,gpu,llm,prism,usdg,web3
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: eth-account>=0.11
|
|
11
|
+
Requires-Dist: requests>=2.31
|
|
12
|
+
Requires-Dist: web3>=6.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# prismnetwork
|
|
16
|
+
|
|
17
|
+
Headless GPU leasing on [Prism Network](https://prismnetwork.tech) for autonomous
|
|
18
|
+
agents — the Python counterpart to `@prismnetwork/agent-sdk`. Give it a wallet; it
|
|
19
|
+
authenticates with a signature, pays on-chain in USDG, provisions a GPU, and runs
|
|
20
|
+
over SSH. No browser, no dashboard.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pip install prismnetwork
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from prismnetwork import PrismAgent, DEFAULT_IMAGE
|
|
28
|
+
|
|
29
|
+
agent = PrismAgent(private_key=AGENT_KEY, escrow="0x71Df0eF3bc81022cB3bec0b1a05f52f12bAfcDeD")
|
|
30
|
+
agent.authenticate()
|
|
31
|
+
|
|
32
|
+
lease = agent.lease(image=DEFAULT_IMAGE, duration_seconds=600, min_vram_mib=16000)
|
|
33
|
+
out = agent.run(lease, "nvidia-smi")
|
|
34
|
+
print(out["stdout"])
|
|
35
|
+
agent.end_lease(lease)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What it does
|
|
39
|
+
|
|
40
|
+
`authenticate()` signs a challenge with the wallet and exchanges it for a bearer
|
|
41
|
+
session. `lease()` gets a quote, funds an on-chain USDG escrow bound to the quote
|
|
42
|
+
(`createLease`), waits for the GPU to provision, and returns SSH access. `run()`
|
|
43
|
+
executes a command over SSH, retrying through the host's sshd warmup. Metering is
|
|
44
|
+
per-second; each lease settles on-chain with a verifiable receipt.
|
|
45
|
+
|
|
46
|
+
Read-only helpers: `offers()`, `balances()`, `leases()`, `quote(...)`, `access(id)`.
|
|
47
|
+
|
|
48
|
+
The wallet needs USDG (`0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`, 6 decimals) and
|
|
49
|
+
native Robinhood-Chain gas.
|
|
50
|
+
|
|
51
|
+
Requires `ssh` and `ssh-keygen` on `PATH`. Chain id 4663, RPC
|
|
52
|
+
`https://rpc.mainnet.chain.robinhood.com`.
|
|
53
|
+
|
|
54
|
+
Prism is pre-production and unaudited. A permissionless supplier is not a trusted
|
|
55
|
+
computing environment; do not lease with a wallet or workload you cannot lose.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# prismnetwork
|
|
2
|
+
|
|
3
|
+
Headless GPU leasing on [Prism Network](https://prismnetwork.tech) for autonomous
|
|
4
|
+
agents — the Python counterpart to `@prismnetwork/agent-sdk`. Give it a wallet; it
|
|
5
|
+
authenticates with a signature, pays on-chain in USDG, provisions a GPU, and runs
|
|
6
|
+
over SSH. No browser, no dashboard.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
pip install prismnetwork
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from prismnetwork import PrismAgent, DEFAULT_IMAGE
|
|
14
|
+
|
|
15
|
+
agent = PrismAgent(private_key=AGENT_KEY, escrow="0x71Df0eF3bc81022cB3bec0b1a05f52f12bAfcDeD")
|
|
16
|
+
agent.authenticate()
|
|
17
|
+
|
|
18
|
+
lease = agent.lease(image=DEFAULT_IMAGE, duration_seconds=600, min_vram_mib=16000)
|
|
19
|
+
out = agent.run(lease, "nvidia-smi")
|
|
20
|
+
print(out["stdout"])
|
|
21
|
+
agent.end_lease(lease)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
`authenticate()` signs a challenge with the wallet and exchanges it for a bearer
|
|
27
|
+
session. `lease()` gets a quote, funds an on-chain USDG escrow bound to the quote
|
|
28
|
+
(`createLease`), waits for the GPU to provision, and returns SSH access. `run()`
|
|
29
|
+
executes a command over SSH, retrying through the host's sshd warmup. Metering is
|
|
30
|
+
per-second; each lease settles on-chain with a verifiable receipt.
|
|
31
|
+
|
|
32
|
+
Read-only helpers: `offers()`, `balances()`, `leases()`, `quote(...)`, `access(id)`.
|
|
33
|
+
|
|
34
|
+
The wallet needs USDG (`0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`, 6 decimals) and
|
|
35
|
+
native Robinhood-Chain gas.
|
|
36
|
+
|
|
37
|
+
Requires `ssh` and `ssh-keygen` on `PATH`. Chain id 4663, RPC
|
|
38
|
+
`https://rpc.mainnet.chain.robinhood.com`.
|
|
39
|
+
|
|
40
|
+
Prism is pre-production and unaudited. A permissionless supplier is not a trusted
|
|
41
|
+
computing environment; do not lease with a wallet or workload you cannot lose.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Authenticate a wallet, list GPUs, and optionally lease one and run a command.
|
|
2
|
+
|
|
3
|
+
PRISM_AGENT_KEY=0x<agent wallet private key> \\
|
|
4
|
+
PRISM_ESCROW=0x71Df0eF3bc81022cB3bec0b1a05f52f12bAfcDeD \\
|
|
5
|
+
python quickstart.py
|
|
6
|
+
|
|
7
|
+
By default this authenticates and lists GPUs without spending. Set PRISM_RUN_LEASE=1
|
|
8
|
+
to lease, run nvidia-smi, and release the lease (spends USDG + gas). Prism is
|
|
9
|
+
pre-production and unaudited; do not use funds you cannot lose.
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from prismnetwork import DEFAULT_IMAGE, PrismAgent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def require(name: str) -> str:
|
|
18
|
+
value = os.environ.get(name)
|
|
19
|
+
if not value:
|
|
20
|
+
sys.exit(f"missing {name}")
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
agent = PrismAgent(private_key=require("PRISM_AGENT_KEY"), escrow=require("PRISM_ESCROW"))
|
|
25
|
+
|
|
26
|
+
session = agent.authenticate()
|
|
27
|
+
print("authenticated as", session["subject"])
|
|
28
|
+
|
|
29
|
+
offers = agent.offers()
|
|
30
|
+
if not offers:
|
|
31
|
+
sys.exit("no GPUs online right now — try again shortly.")
|
|
32
|
+
print(f"{len(offers)} offer(s):", ", ".join(o["gpu"]["model"] for o in offers))
|
|
33
|
+
|
|
34
|
+
if os.environ.get("PRISM_RUN_LEASE") != "1":
|
|
35
|
+
print("\nauth OK. Set PRISM_RUN_LEASE=1 to lease + run (spends USDG + gas).")
|
|
36
|
+
sys.exit(0)
|
|
37
|
+
|
|
38
|
+
print("leasing a GPU (provisioning takes a few minutes)...")
|
|
39
|
+
lease = agent.lease(image=DEFAULT_IMAGE, duration_seconds=600, min_vram_mib=16000)
|
|
40
|
+
print("leased", lease.lease_id, "funded on-chain:", lease.funding_hash)
|
|
41
|
+
|
|
42
|
+
result = agent.run(lease, "nvidia-smi --query-gpu=name,memory.total --format=csv,noheader")
|
|
43
|
+
print(f"\nremote output (exit {result['code']}):\n{result['stdout'] or result['stderr']}")
|
|
44
|
+
|
|
45
|
+
agent.end_lease(lease)
|
|
46
|
+
print("lease released. Settlement and a public receipt follow on chain.")
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from eth_account import Account
|
|
12
|
+
from eth_account.messages import encode_defunct
|
|
13
|
+
from web3 import Web3
|
|
14
|
+
|
|
15
|
+
ROBINHOOD_RPC = "https://rpc.mainnet.chain.robinhood.com"
|
|
16
|
+
CHAIN_ID = 4663
|
|
17
|
+
USDG = Web3.to_checksum_address("0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168")
|
|
18
|
+
|
|
19
|
+
# A digest-pinned image, matching the Node SDK so a default can't drift.
|
|
20
|
+
DEFAULT_IMAGE = "docker.io/ollama/ollama@sha256:a61a8fd395dbb931cc8cb1b5da7a2510746575c87113fdc45b647ee59ef7f808"
|
|
21
|
+
|
|
22
|
+
CONFIRMATIONS = 12
|
|
23
|
+
FETCH_TIMEOUT = 30
|
|
24
|
+
|
|
25
|
+
_ERC20 = [
|
|
26
|
+
{"name": "approve", "type": "function", "stateMutability": "nonpayable",
|
|
27
|
+
"inputs": [{"name": "spender", "type": "address"}, {"name": "value", "type": "uint256"}],
|
|
28
|
+
"outputs": [{"type": "bool"}]},
|
|
29
|
+
{"name": "allowance", "type": "function", "stateMutability": "view",
|
|
30
|
+
"inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}],
|
|
31
|
+
"outputs": [{"type": "uint256"}]},
|
|
32
|
+
{"name": "balanceOf", "type": "function", "stateMutability": "view",
|
|
33
|
+
"inputs": [{"name": "owner", "type": "address"}], "outputs": [{"type": "uint256"}]},
|
|
34
|
+
]
|
|
35
|
+
_ESCROW = [
|
|
36
|
+
{"name": "createLease", "type": "function", "stateMutability": "nonpayable",
|
|
37
|
+
"inputs": [{"name": "nodeId", "type": "bytes32"}, {"name": "duration", "type": "uint32"},
|
|
38
|
+
{"name": "clientReference", "type": "bytes32"}],
|
|
39
|
+
"outputs": [{"type": "uint256"}]},
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
_DIGEST = re.compile(r"@sha256:[0-9a-f]{64}$")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PrismError(Exception):
|
|
46
|
+
def __init__(self, status: int, code: str, body=None):
|
|
47
|
+
super().__init__(f"prism {status}: {code}")
|
|
48
|
+
self.status = status
|
|
49
|
+
self.code = code
|
|
50
|
+
self.body = body
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Lease:
|
|
55
|
+
lease_id: int
|
|
56
|
+
access: dict
|
|
57
|
+
key_path: str
|
|
58
|
+
key_dir: str
|
|
59
|
+
public_key: str
|
|
60
|
+
funding_hash: str
|
|
61
|
+
quote: dict
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PrismAgent:
|
|
65
|
+
"""Headless GPU leasing for a wallet-holding agent. Authenticate with a wallet
|
|
66
|
+
signature, pay on-chain in USDG, provision, and run over SSH."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, private_key: str, escrow: str,
|
|
69
|
+
api_base: str = "https://prismnetwork.tech", rpc_url: str = ROBINHOOD_RPC):
|
|
70
|
+
if not escrow:
|
|
71
|
+
raise ValueError("escrow address is required")
|
|
72
|
+
self.api_base = api_base.rstrip("/")
|
|
73
|
+
self.escrow = Web3.to_checksum_address(escrow)
|
|
74
|
+
self.account = Account.from_key(private_key)
|
|
75
|
+
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
76
|
+
self._usdg = self.w3.eth.contract(address=USDG, abi=_ERC20)
|
|
77
|
+
self._escrow = self.w3.eth.contract(address=self.escrow, abi=_ESCROW)
|
|
78
|
+
self.session: str | None = None
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def address(self) -> str:
|
|
82
|
+
return self.account.address
|
|
83
|
+
|
|
84
|
+
def authenticate(self) -> dict:
|
|
85
|
+
challenge = self._json("GET", f"/api/agent/challenge?address={self.address}")
|
|
86
|
+
signed = self.account.sign_message(encode_defunct(text=challenge["message"]))
|
|
87
|
+
sig = signed.signature.hex()
|
|
88
|
+
session = self._json("POST", "/api/agent/session", {
|
|
89
|
+
"challenge": challenge["challenge"],
|
|
90
|
+
"address": self.address,
|
|
91
|
+
"signature": sig if sig.startswith("0x") else "0x" + sig,
|
|
92
|
+
})
|
|
93
|
+
self.session = session["session"]
|
|
94
|
+
return session
|
|
95
|
+
|
|
96
|
+
def offers(self) -> list:
|
|
97
|
+
return self._proxy("GET", ["offers"])
|
|
98
|
+
|
|
99
|
+
def balances(self) -> dict:
|
|
100
|
+
return {
|
|
101
|
+
"address": self.address,
|
|
102
|
+
"usdg": self._usdg.functions.balanceOf(self.address).call(),
|
|
103
|
+
"eth": self.w3.eth.get_balance(self.address),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def quote(self, image: str, duration_seconds: int, min_vram_mib: int = 16000,
|
|
107
|
+
preferred_node_id: str | None = None) -> dict:
|
|
108
|
+
if not isinstance(image, str) or not _DIGEST.search(image):
|
|
109
|
+
raise PrismError(400, "image_must_be_digest_pinned")
|
|
110
|
+
return self._proxy("POST", ["leases", "match"], {"request": {
|
|
111
|
+
"image": image,
|
|
112
|
+
"duration_seconds": duration_seconds,
|
|
113
|
+
"min_vram_mib": min_vram_mib,
|
|
114
|
+
"preferred_node_id": preferred_node_id,
|
|
115
|
+
}})
|
|
116
|
+
|
|
117
|
+
def confirm(self, quote_id: str, transaction_hash: str, ssh_authorized_key: str) -> dict:
|
|
118
|
+
return self._proxy("POST", ["leases", "confirm"], {
|
|
119
|
+
"quote_id": quote_id,
|
|
120
|
+
"transaction_hash": transaction_hash,
|
|
121
|
+
"ssh_authorized_key": ssh_authorized_key,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
def leases(self) -> list:
|
|
125
|
+
return self._proxy("GET", ["leases"])
|
|
126
|
+
|
|
127
|
+
def access(self, lease_id) -> dict:
|
|
128
|
+
return self._proxy("GET", ["leases", str(lease_id), "access"])
|
|
129
|
+
|
|
130
|
+
def wait_for_access(self, lease_id, timeout: int = 600, interval: int = 10) -> dict:
|
|
131
|
+
deadline = time.time() + timeout
|
|
132
|
+
while time.time() < deadline:
|
|
133
|
+
status, body = self._proxy("GET", ["leases", str(lease_id), "access"], raw=True)
|
|
134
|
+
if status == 200:
|
|
135
|
+
return body
|
|
136
|
+
if status != 404:
|
|
137
|
+
raise PrismError(status, (body or {}).get("error", "access_error"))
|
|
138
|
+
time.sleep(interval)
|
|
139
|
+
raise PrismError(408, "access_timeout")
|
|
140
|
+
|
|
141
|
+
def lease(self, image: str, duration_seconds: int, min_vram_mib: int = 16000,
|
|
142
|
+
preferred_node_id: str | None = None, max_deposit: int | None = None) -> Lease:
|
|
143
|
+
if not self.session:
|
|
144
|
+
self.authenticate()
|
|
145
|
+
quote = self.quote(image, duration_seconds, min_vram_mib, preferred_node_id)
|
|
146
|
+
if max_deposit is not None and int(quote["maximum_escrow"]) > int(max_deposit):
|
|
147
|
+
raise PrismError(402, "cost_exceeds_max",
|
|
148
|
+
{"required": quote["maximum_escrow"], "max": str(max_deposit)})
|
|
149
|
+
key = self._generate_ssh_key()
|
|
150
|
+
try:
|
|
151
|
+
funding = self._fund(quote)
|
|
152
|
+
record = self.confirm(quote["quote_id"], funding, key["public_key"])
|
|
153
|
+
lease_id = record["lease_id"]
|
|
154
|
+
return Lease(lease_id, self.wait_for_access(lease_id),
|
|
155
|
+
key["key_path"], key["dir"], key["public_key"], funding, quote)
|
|
156
|
+
except Exception:
|
|
157
|
+
shutil.rmtree(key["dir"], ignore_errors=True)
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
def run(self, lease: Lease, command: str, timeout: int = 120,
|
|
161
|
+
connect_retries: int = 24, connect_delay: int = 10) -> dict:
|
|
162
|
+
a = lease.access
|
|
163
|
+
args = ["ssh", "-i", lease.key_path, "-p", str(a["ssh_port"]),
|
|
164
|
+
"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
|
|
165
|
+
"-o", "BatchMode=yes", "-o", "ConnectTimeout=15",
|
|
166
|
+
f"{a.get('ssh_user', 'root')}@{a['ssh_host']}", command]
|
|
167
|
+
last = None
|
|
168
|
+
for attempt in range(connect_retries + 1):
|
|
169
|
+
try:
|
|
170
|
+
p = subprocess.run(args, capture_output=True, text=True, timeout=timeout + 20)
|
|
171
|
+
res = {"code": p.returncode, "stdout": p.stdout.strip(), "stderr": p.stderr.strip()}
|
|
172
|
+
except subprocess.TimeoutExpired:
|
|
173
|
+
res = {"code": -1, "stdout": "", "stderr": "timed out"}
|
|
174
|
+
if not _is_ssh_warmup(res):
|
|
175
|
+
return res
|
|
176
|
+
last = res
|
|
177
|
+
if attempt < connect_retries:
|
|
178
|
+
time.sleep(connect_delay)
|
|
179
|
+
return last
|
|
180
|
+
|
|
181
|
+
def end_lease(self, lease: Lease) -> None:
|
|
182
|
+
"""Release local key material. The on-chain lease settles at the end of its duration."""
|
|
183
|
+
if lease and lease.key_dir:
|
|
184
|
+
shutil.rmtree(lease.key_dir, ignore_errors=True)
|
|
185
|
+
|
|
186
|
+
def _fund(self, quote: dict) -> str:
|
|
187
|
+
deposit = int(quote["maximum_escrow"])
|
|
188
|
+
duration = int(quote["duration_seconds"])
|
|
189
|
+
client_ref = Web3.keccak(text=quote["quote_id"])
|
|
190
|
+
node_id = bytes.fromhex(quote["node_id"].removeprefix("0x"))
|
|
191
|
+
allowance = self._usdg.functions.allowance(self.address, self.escrow).call()
|
|
192
|
+
if allowance < deposit:
|
|
193
|
+
self._send(self._usdg.functions.approve(self.escrow, deposit))
|
|
194
|
+
return self._send(self._escrow.functions.createLease(node_id, duration, client_ref),
|
|
195
|
+
confirmations=CONFIRMATIONS)
|
|
196
|
+
|
|
197
|
+
def _send(self, call, confirmations: int = 1) -> str:
|
|
198
|
+
tx = call.build_transaction({
|
|
199
|
+
"from": self.address,
|
|
200
|
+
"nonce": self.w3.eth.get_transaction_count(self.address),
|
|
201
|
+
"chainId": CHAIN_ID,
|
|
202
|
+
})
|
|
203
|
+
signed = self.account.sign_transaction(tx)
|
|
204
|
+
h = self.w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
205
|
+
receipt = self.w3.eth.wait_for_transaction_receipt(h)
|
|
206
|
+
if receipt.status != 1:
|
|
207
|
+
raise PrismError(402, "tx_reverted", {"hash": h.hex()})
|
|
208
|
+
if confirmations > 1:
|
|
209
|
+
target = receipt.blockNumber + confirmations - 1
|
|
210
|
+
while self.w3.eth.block_number < target:
|
|
211
|
+
time.sleep(2)
|
|
212
|
+
return h.hex()
|
|
213
|
+
|
|
214
|
+
def _proxy(self, method: str, segments: list, body=None, raw: bool = False, reauthed: bool = False):
|
|
215
|
+
if not self.session:
|
|
216
|
+
self.authenticate()
|
|
217
|
+
res = self._request(f"/api/agent/proxy/{'/'.join(segments)}", method, body,
|
|
218
|
+
{"authorization": f"Bearer {self.session}"})
|
|
219
|
+
if res.status_code == 401 and not reauthed:
|
|
220
|
+
self.session = None
|
|
221
|
+
self.authenticate()
|
|
222
|
+
return self._proxy(method, segments, body, raw, True)
|
|
223
|
+
if raw:
|
|
224
|
+
return res.status_code, _safe_json(res)
|
|
225
|
+
return self._unwrap(res)
|
|
226
|
+
|
|
227
|
+
def _json(self, method: str, path: str, body=None):
|
|
228
|
+
return self._unwrap(self._request(path, method, body))
|
|
229
|
+
|
|
230
|
+
def _request(self, path: str, method: str, body=None, headers: dict | None = None):
|
|
231
|
+
try:
|
|
232
|
+
return requests.request(method, f"{self.api_base}{path}",
|
|
233
|
+
json=body, headers={"accept": "application/json", **(headers or {})},
|
|
234
|
+
timeout=FETCH_TIMEOUT)
|
|
235
|
+
except requests.RequestException as e:
|
|
236
|
+
raise PrismError(504, "control_plane_unreachable", {"cause": str(e)})
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _unwrap(res):
|
|
240
|
+
data = _safe_json(res)
|
|
241
|
+
if not res.ok:
|
|
242
|
+
raise PrismError(res.status_code, (data or {}).get("error") or (data or {}).get("code") or "request_failed", data)
|
|
243
|
+
return data
|
|
244
|
+
|
|
245
|
+
def _generate_ssh_key(self) -> dict:
|
|
246
|
+
d = tempfile.mkdtemp(prefix="prism-ssh-")
|
|
247
|
+
try:
|
|
248
|
+
key_path = f"{d}/id_ed25519"
|
|
249
|
+
subprocess.run(["ssh-keygen", "-t", "ed25519", "-N", "", "-q", "-f", key_path, "-C", "prism-agent"],
|
|
250
|
+
check=True, capture_output=True)
|
|
251
|
+
with open(f"{key_path}.pub") as f:
|
|
252
|
+
return {"dir": d, "key_path": key_path, "public_key": f.read().strip()}
|
|
253
|
+
except Exception as e:
|
|
254
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
255
|
+
raise PrismError(500, "ssh_keygen_failed", {"cause": str(e)})
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _safe_json(res):
|
|
259
|
+
try:
|
|
260
|
+
return res.json()
|
|
261
|
+
except ValueError:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _is_ssh_warmup(res: dict) -> bool:
|
|
266
|
+
if res["code"] != 255:
|
|
267
|
+
return False
|
|
268
|
+
e = res["stderr"]
|
|
269
|
+
return (e.startswith("ssh: ") or "\nssh: " in e
|
|
270
|
+
or "kex_exchange_identification" in e or "Connection reset by peer" in e
|
|
271
|
+
or ("Permission denied (publickey" in e and res["stdout"] == ""))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prismnetwork"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Headless GPU leasing on Prism Network for autonomous agents. Wallet-signature auth, on-chain USDG payment, SSH."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
keywords = ["prism", "gpu", "agent", "web3", "usdg", "llm"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"requests>=2.31",
|
|
15
|
+
"web3>=6.0",
|
|
16
|
+
"eth-account>=0.11",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://prismnetwork.tech"
|
|
21
|
+
Source = "https://github.com/prismnetwork-tech/prism"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["prismnetwork"]
|