mycelium-cli 0.3.0__tar.gz → 0.4.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.
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/PKG-INFO +41 -1
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/README.md +40 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/deal.py +31 -25
- mycelium_cli-0.4.0/mycelium_cli/commands/jobs.py +373 -0
- mycelium_cli-0.4.0/mycelium_cli/commands/verifier.py +142 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/main.py +5 -1
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/PKG-INFO +41 -1
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/SOURCES.txt +1 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/pyproject.toml +1 -1
- mycelium_cli-0.3.0/mycelium_cli/commands/jobs.py +0 -241
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/__init__.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/__init__.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/agent.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/call.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/check.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/compile.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/deploy.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/discover.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/doctor.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/events.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/fund.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/init.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/memory.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/newwallet.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/pay.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/register.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/resolve.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/run.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/status.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/test.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli/config.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/dependency_links.txt +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/entry_points.txt +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/requires.txt +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/top_level.txt +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/setup.cfg +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/tests/test_agent.py +0 -0
- {mycelium_cli-0.3.0 → mycelium_cli-0.4.0}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycelium-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Mycelium CLI — init, newwallet, compile, deploy, register
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -230,6 +230,46 @@ Performs a simulation dry-run of the agent loop. It intercepts all state-changin
|
|
|
230
230
|
mycelium test
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
### 16. `mycelium job` (Proof Layer — v0.4.0)
|
|
234
|
+
Posts, performs, judges, and settles verifiable bounties. A job is self-describing
|
|
235
|
+
on-chain (title, description, weighted checks, judge panel); release is gated on a
|
|
236
|
+
multi-LLM panel verdict, not a hash.
|
|
237
|
+
|
|
238
|
+
* **Syntax**:
|
|
239
|
+
```bash
|
|
240
|
+
# Post a self-describing bounty + judge panel
|
|
241
|
+
mycelium job post \
|
|
242
|
+
--title "Write a sales-report SQL query" \
|
|
243
|
+
--description "Aggregate revenue by region, last 12 months." \
|
|
244
|
+
--check correct:70:"returns correct rows" \
|
|
245
|
+
--check style:30:"readable, indexed" \
|
|
246
|
+
--judge-model nvidia:meta/llama-3.1-70b \
|
|
247
|
+
--judge-model groq:llama-3.3-70b \
|
|
248
|
+
--threshold 75
|
|
249
|
+
|
|
250
|
+
mycelium job do <job_id> --model groq:llama-3.3-70b # worker produces + submits evidence
|
|
251
|
+
mycelium job judge <job_id> # run the job's panel → verdict → settle
|
|
252
|
+
mycelium job models --provider nvidia # list available judge/worker models
|
|
253
|
+
mycelium job status <job_id> # on-chain title/checks/panel/score
|
|
254
|
+
```
|
|
255
|
+
* **Details**: `--check` is `id:weight:text` (repeatable); `--judge-model` is
|
|
256
|
+
`provider:model` (repeatable). A single agent is paid the full bounty on a
|
|
257
|
+
passing verdict; a swarm is paid a balanced split — both gated on the same panel.
|
|
258
|
+
|
|
259
|
+
### 17. `mycelium verifier` (Staked judge pool — v0.4.0)
|
|
260
|
+
Manages the `VerifierRegistry`: judges stake an XLM bond to become eligible and are
|
|
261
|
+
slashed for outlier verdicts; per-judge accuracy (verifier reputation) is tracked.
|
|
262
|
+
|
|
263
|
+
* **Syntax**:
|
|
264
|
+
```bash
|
|
265
|
+
mycelium verifier register --model nvidia:meta/llama-3.1-70b
|
|
266
|
+
mycelium verifier stake 100 # lock an XLM bond
|
|
267
|
+
mycelium verifier info <address> # stake, jobs judged, accuracy, active
|
|
268
|
+
mycelium verifier eligible <model> # judges staked + tagged for a model
|
|
269
|
+
mycelium verifier slash <address> # market-only: penalize an outlier verdict
|
|
270
|
+
mycelium verifier accuracy <address> # verifier reputation (within-tolerance rate)
|
|
271
|
+
```
|
|
272
|
+
|
|
233
273
|
---
|
|
234
274
|
|
|
235
275
|
## 🔐 Environment Variables
|
|
@@ -220,6 +220,46 @@ Performs a simulation dry-run of the agent loop. It intercepts all state-changin
|
|
|
220
220
|
mycelium test
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
+
### 16. `mycelium job` (Proof Layer — v0.4.0)
|
|
224
|
+
Posts, performs, judges, and settles verifiable bounties. A job is self-describing
|
|
225
|
+
on-chain (title, description, weighted checks, judge panel); release is gated on a
|
|
226
|
+
multi-LLM panel verdict, not a hash.
|
|
227
|
+
|
|
228
|
+
* **Syntax**:
|
|
229
|
+
```bash
|
|
230
|
+
# Post a self-describing bounty + judge panel
|
|
231
|
+
mycelium job post \
|
|
232
|
+
--title "Write a sales-report SQL query" \
|
|
233
|
+
--description "Aggregate revenue by region, last 12 months." \
|
|
234
|
+
--check correct:70:"returns correct rows" \
|
|
235
|
+
--check style:30:"readable, indexed" \
|
|
236
|
+
--judge-model nvidia:meta/llama-3.1-70b \
|
|
237
|
+
--judge-model groq:llama-3.3-70b \
|
|
238
|
+
--threshold 75
|
|
239
|
+
|
|
240
|
+
mycelium job do <job_id> --model groq:llama-3.3-70b # worker produces + submits evidence
|
|
241
|
+
mycelium job judge <job_id> # run the job's panel → verdict → settle
|
|
242
|
+
mycelium job models --provider nvidia # list available judge/worker models
|
|
243
|
+
mycelium job status <job_id> # on-chain title/checks/panel/score
|
|
244
|
+
```
|
|
245
|
+
* **Details**: `--check` is `id:weight:text` (repeatable); `--judge-model` is
|
|
246
|
+
`provider:model` (repeatable). A single agent is paid the full bounty on a
|
|
247
|
+
passing verdict; a swarm is paid a balanced split — both gated on the same panel.
|
|
248
|
+
|
|
249
|
+
### 17. `mycelium verifier` (Staked judge pool — v0.4.0)
|
|
250
|
+
Manages the `VerifierRegistry`: judges stake an XLM bond to become eligible and are
|
|
251
|
+
slashed for outlier verdicts; per-judge accuracy (verifier reputation) is tracked.
|
|
252
|
+
|
|
253
|
+
* **Syntax**:
|
|
254
|
+
```bash
|
|
255
|
+
mycelium verifier register --model nvidia:meta/llama-3.1-70b
|
|
256
|
+
mycelium verifier stake 100 # lock an XLM bond
|
|
257
|
+
mycelium verifier info <address> # stake, jobs judged, accuracy, active
|
|
258
|
+
mycelium verifier eligible <model> # judges staked + tagged for a model
|
|
259
|
+
mycelium verifier slash <address> # market-only: penalize an outlier verdict
|
|
260
|
+
mycelium verifier accuracy <address> # verifier reputation (within-tolerance rate)
|
|
261
|
+
```
|
|
262
|
+
|
|
223
263
|
---
|
|
224
264
|
|
|
225
265
|
## 🔐 Environment Variables
|
|
@@ -4,20 +4,22 @@
|
|
|
4
4
|
Where `mycelium pay` is an *unconditional* transfer, a deal is *conditional*
|
|
5
5
|
x402 commerce between two agents: the payer locks funds into a fresh escrow
|
|
6
6
|
payable to a provider (resolved by Hive Registry unique name or raw address),
|
|
7
|
-
and the provider only collects once
|
|
8
|
-
|
|
7
|
+
and the provider only collects once a `judge` authorizes release on a passing
|
|
8
|
+
verdict of the delivered work. If no release happens, the payer reclaims the
|
|
9
|
+
funds after the timeout.
|
|
9
10
|
|
|
10
11
|
This is the CLI front door to what `a2a_demo.py` / `EscrowPaymentRouter` do by
|
|
11
|
-
hand — two sovereign agents transacting purely through on-chain state
|
|
12
|
+
hand — two sovereign agents transacting purely through on-chain state, with a
|
|
13
|
+
judge as the impartial release authority (see PROOF_SYSTEM.md):
|
|
12
14
|
|
|
13
|
-
open payer locks `amount` XLM to a provider
|
|
14
|
-
release
|
|
15
|
+
open payer locks `amount` XLM to a provider, naming a judge → escrow id
|
|
16
|
+
release the judge authorizes payout (passing verdict) → funds disburse
|
|
15
17
|
refund payer reclaims the locked funds after the deadline passes
|
|
16
|
-
status read the escrow's current state (amount, provider,
|
|
18
|
+
status read the escrow's current state (amount, provider, judge, settled)
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
default from `mycelium.toml`, mirroring `deploy` / `
|
|
20
|
+
Release follows a judge's verdict rather than a SHA-256 preimage: a hash only
|
|
21
|
+
proved the claimant could echo the agreed bytes, never that the work was done.
|
|
22
|
+
The board/registry default from `mycelium.toml`, mirroring `deploy` / `job`.
|
|
21
23
|
|
|
22
24
|
Commands: open, release, refund, status.
|
|
23
25
|
"""
|
|
@@ -77,38 +79,40 @@ def _resolve_agent_address(context, agent: str, registry: Optional[str]) -> str:
|
|
|
77
79
|
return addr
|
|
78
80
|
|
|
79
81
|
|
|
80
|
-
def
|
|
81
|
-
"""
|
|
82
|
-
if os.path.isfile(
|
|
83
|
-
with open(
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
def _evidence_root(evidence: str) -> bytes:
|
|
83
|
+
"""The 32-byte evidence_root: SHA-256 of a file's bytes or a literal string."""
|
|
84
|
+
if os.path.isfile(evidence):
|
|
85
|
+
with open(evidence, "rb") as f:
|
|
86
|
+
data = f.read()
|
|
87
|
+
else:
|
|
88
|
+
data = evidence.encode("utf-8")
|
|
89
|
+
return hashlib.sha256(data).digest()
|
|
86
90
|
|
|
87
91
|
|
|
88
92
|
@deal_app.command("open")
|
|
89
93
|
def open_deal(
|
|
90
94
|
to: str = typer.Option(..., "--to", help="Provider: Hive Registry unique name or G/C address to pay"),
|
|
91
95
|
amount: str = typer.Option(..., "--amount", help="Amount in XLM to lock for the provider"),
|
|
92
|
-
|
|
96
|
+
judge: str = typer.Option(..., "--judge", help="Judge: unique name or G/C address — the release authority"),
|
|
93
97
|
token: str = typer.Option(None, "--token", help="Payment token contract (defaults to native XLM SAC)"),
|
|
94
98
|
timeout: int = typer.Option(DEFAULT_TIMEOUT_SECONDS, "--timeout", help="Refund deadline in seconds"),
|
|
95
99
|
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
96
100
|
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
97
101
|
registry: str = typer.Option(None, "--registry", help="Hive Registry id override (for name resolution)"),
|
|
98
102
|
):
|
|
99
|
-
"""Payer locks `amount` XLM to a provider
|
|
103
|
+
"""Payer locks `amount` XLM to a provider, naming a judge as the release authority."""
|
|
100
104
|
from mycelium_sdk.x402.settlement import EscrowPaymentRouter
|
|
101
105
|
|
|
102
106
|
context = _context(network, wallet, signing=True)
|
|
103
107
|
provider = _resolve_agent_address(context, to, registry)
|
|
104
|
-
|
|
108
|
+
judge_addr = _resolve_agent_address(context, judge, registry)
|
|
105
109
|
|
|
106
|
-
typer.echo(f"[deal] Locking {amount} XLM to provider {to} ({provider[:8]}…) for {timeout}s...")
|
|
110
|
+
typer.echo(f"[deal] Locking {amount} XLM to provider {to} ({provider[:8]}…), judge {judge_addr[:8]}…, for {timeout}s...")
|
|
107
111
|
try:
|
|
108
112
|
escrow_id = EscrowPaymentRouter(context).create_locked_escrow(
|
|
109
113
|
provider_id=provider,
|
|
110
114
|
amount_xlm=Decimal(amount),
|
|
111
|
-
|
|
115
|
+
judge=judge_addr,
|
|
112
116
|
token=token,
|
|
113
117
|
timeout_seconds=timeout,
|
|
114
118
|
)
|
|
@@ -116,23 +120,23 @@ def open_deal(
|
|
|
116
120
|
typer.echo(f"❌ deal open failed: {e}")
|
|
117
121
|
raise typer.Exit(code=1)
|
|
118
122
|
typer.echo(f"✓ Deal opened. Escrow {escrow_id}")
|
|
119
|
-
typer.echo(f"
|
|
123
|
+
typer.echo(f" Judge releases with: mycelium deal release {escrow_id} --evidence <bundle>")
|
|
120
124
|
typer.echo(f" Payer refunds after {timeout}s with: mycelium deal refund {escrow_id}")
|
|
121
125
|
|
|
122
126
|
|
|
123
127
|
@deal_app.command("release")
|
|
124
128
|
def release(
|
|
125
129
|
escrow_id: str = typer.Argument(..., help="Escrow contract id from `deal open`"),
|
|
126
|
-
|
|
130
|
+
evidence: str = typer.Option(..., "--evidence", help="Evidence bundle file/string (its SHA-256 is recorded for audit)"),
|
|
127
131
|
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
128
|
-
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="
|
|
132
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Judge wallet path (the escrow's release authority)"),
|
|
129
133
|
):
|
|
130
|
-
"""
|
|
134
|
+
"""Judge disburses the locked funds to the provider on a passing verdict."""
|
|
131
135
|
from mycelium_sdk.x402.settlement import EscrowPaymentRouter
|
|
132
136
|
|
|
133
137
|
context = _context(network, wallet, signing=True)
|
|
134
138
|
try:
|
|
135
|
-
EscrowPaymentRouter(context).release_funds(escrow_id,
|
|
139
|
+
EscrowPaymentRouter(context).release_funds(escrow_id, _evidence_root(evidence))
|
|
136
140
|
except Exception as e: # noqa: BLE001
|
|
137
141
|
typer.echo(f"❌ deal release failed: {e}")
|
|
138
142
|
raise typer.Exit(code=1)
|
|
@@ -177,10 +181,12 @@ def status(
|
|
|
177
181
|
|
|
178
182
|
amount = _get("amount")
|
|
179
183
|
provider = _get("provider")
|
|
184
|
+
judge = _get("judge")
|
|
180
185
|
deadline = _get("deadline")
|
|
181
186
|
settled = _get("settled")
|
|
182
187
|
typer.echo(f"Deal escrow {escrow_id}")
|
|
183
188
|
typer.echo(f" amount : {int(amount) / 10_000_000:.7f} XLM" if amount is not None else " amount : —")
|
|
184
189
|
typer.echo(f" provider : {getattr(provider, 'address', provider)}")
|
|
190
|
+
typer.echo(f" judge : {getattr(judge, 'address', judge)}")
|
|
185
191
|
typer.echo(f" deadline : {int(deadline)} (unix)" if deadline is not None else " deadline : —")
|
|
186
192
|
typer.echo(f" settled : {bool(settled)}")
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`mycelium job …` — drive the Sovereign Job Boards from the console.
|
|
3
|
+
|
|
4
|
+
A Typer sub-app (registered as the `job` command group in `main.py`) that thin-
|
|
5
|
+
wraps `mycelium_sdk.JobBoardClient`, reusing the same wallet load + passphrase
|
|
6
|
+
resolution as `deploy` / `register`. The JobBoard contract address defaults from
|
|
7
|
+
`mycelium.toml` (`[jobs].board_address`); override with `--board`.
|
|
8
|
+
|
|
9
|
+
Commands: post, list, claim, assign, join, submit, verdict, finalize, status.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from mycelium_cli.config import get_value
|
|
20
|
+
|
|
21
|
+
DEFAULT_WALLET_PATH = os.path.join(".mycelium", "wallet.json")
|
|
22
|
+
PASSPHRASE_ENV_VAR = "MYCELIUM_DECRYPT_KEY"
|
|
23
|
+
|
|
24
|
+
job_app = typer.Typer(help="Post, claim, and settle on-chain jobs (Sovereign Job Boards).")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_passphrase(label: str = "Wallet passphrase") -> str:
|
|
28
|
+
"""MYCELIUM_DECRYPT_KEY if set, else prompt — matches the rest of the CLI."""
|
|
29
|
+
env_value = os.environ.get(PASSPHRASE_ENV_VAR)
|
|
30
|
+
if env_value:
|
|
31
|
+
return env_value
|
|
32
|
+
return typer.prompt(label, hide_input=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _board_address(override: str | None) -> str:
|
|
36
|
+
board = override or get_value("jobs", "board_address")
|
|
37
|
+
if not board:
|
|
38
|
+
typer.echo(
|
|
39
|
+
"Error: no JobBoard address. Set [jobs].board_address in mycelium.toml "
|
|
40
|
+
"or pass --board <contract_id>."
|
|
41
|
+
)
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
return board
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _client(network: str | None, wallet: str, board: str | None, *, signing: bool):
|
|
47
|
+
"""Build a JobBoardClient. Read-only commands skip wallet + passphrase."""
|
|
48
|
+
from mycelium_sdk import AgentContext, JobBoardClient
|
|
49
|
+
|
|
50
|
+
network = network or get_value("onchain", "network", "testnet")
|
|
51
|
+
if signing:
|
|
52
|
+
if not os.path.exists(wallet):
|
|
53
|
+
typer.echo(f"Error: wallet {wallet} not found. Run `mycelium newwallet` first.")
|
|
54
|
+
raise typer.Exit(code=1)
|
|
55
|
+
ctx = AgentContext(keypair_path=wallet, network_type=network, passphrase=_resolve_passphrase())
|
|
56
|
+
else:
|
|
57
|
+
ctx = AgentContext.read_only(network_type=network)
|
|
58
|
+
return JobBoardClient(ctx, _board_address(board))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_check(spec: str) -> dict:
|
|
62
|
+
"""Parse a --check 'id:weight:description' into a check dict (LLM-judged)."""
|
|
63
|
+
parts = spec.split(":", 2)
|
|
64
|
+
if len(parts) != 3:
|
|
65
|
+
raise typer.BadParameter(f"--check must be 'id:weight:description' (got {spec!r}).")
|
|
66
|
+
cid, weight, text = parts
|
|
67
|
+
return {"id": cid.strip(), "weight": int(weight), "check": text.strip()}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@job_app.command("post")
|
|
71
|
+
def post(
|
|
72
|
+
title: str = typer.Option(..., "--title", help="Job heading (required)"),
|
|
73
|
+
description: str = typer.Option(..., "--description", help="What the work is (required)"),
|
|
74
|
+
check: list[str] = typer.Option(..., "--check", help="A check 'id:weight:description' (repeatable, ≥1)"),
|
|
75
|
+
judge_model: list[str] = typer.Option(..., "--judge-model", help="Judge model 'provider:model' (repeatable; the panel)"),
|
|
76
|
+
bounty: str = typer.Option(..., "--bounty", help="Bounty in XLM"),
|
|
77
|
+
judge: str = typer.Option(..., "--judge", help="Judge address (G…) — on-chain verdict authority that releases the escrow"),
|
|
78
|
+
threshold: int = typer.Option(70, "--threshold", help="Pass score (0-100); payout only at/above this"),
|
|
79
|
+
deliverable_type: str = typer.Option("any", "--type", help="Freeform deliverable type, e.g. text/sql, file/pptx"),
|
|
80
|
+
mode: str = typer.Option("single", "--mode", help="single | swarm"),
|
|
81
|
+
token: str = typer.Option(None, "--token", help="Payment token contract (defaults to native XLM SAC)"),
|
|
82
|
+
deadline: int = typer.Option(86400, "--deadline", help="Refund deadline in seconds"),
|
|
83
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
84
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
85
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
86
|
+
):
|
|
87
|
+
"""
|
|
88
|
+
Post a self-describing bounty for ANY job. Title, description, checks, and the
|
|
89
|
+
chosen judge panel are stored ON-CHAIN, so the bounty is fully readable from
|
|
90
|
+
the contract. Example:
|
|
91
|
+
|
|
92
|
+
mycelium job post --title "Promo script" --description "60s TigerGraph video" \\
|
|
93
|
+
--check "hook:30:strong opening" --check "clarity:40:explains the bounty" \\
|
|
94
|
+
--check "cta:30:clear call to action" \\
|
|
95
|
+
--judge-model nvidia:deepseek-ai/deepseek-v4-pro \\
|
|
96
|
+
--judge-model groq:llama-3.3-70b-versatile \\
|
|
97
|
+
--bounty 5 --judge G... --threshold 70
|
|
98
|
+
"""
|
|
99
|
+
client = _client(network, wallet, board, signing=True)
|
|
100
|
+
try:
|
|
101
|
+
checks = [_parse_check(c) for c in check]
|
|
102
|
+
job_id = client.post_bounty(
|
|
103
|
+
title=title, description=description, checks=checks, judge_models=judge_model,
|
|
104
|
+
bounty_xlm=Decimal(bounty), judge=judge, pass_threshold=threshold,
|
|
105
|
+
deliverable_type=deliverable_type, mode=mode, token=token, deadline_seconds=deadline,
|
|
106
|
+
)
|
|
107
|
+
except Exception as e: # noqa: BLE001
|
|
108
|
+
typer.echo(f"❌ post failed: {e}")
|
|
109
|
+
raise typer.Exit(code=1)
|
|
110
|
+
typer.echo(f"✓ Posted job #{job_id}: '{title}' (bounty {bounty} XLM, {len(checks)} checks, "
|
|
111
|
+
f"{len(judge_model)}-model panel, threshold {threshold}).")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@job_app.command("list")
|
|
115
|
+
def list_jobs(
|
|
116
|
+
status: str = typer.Option(None, "--status", help="Filter: open | claimed | submitted | done | cancelled"),
|
|
117
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
118
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
119
|
+
):
|
|
120
|
+
"""List jobs (read-only, no wallet)."""
|
|
121
|
+
client = _client(network, DEFAULT_WALLET_PATH, board, signing=False)
|
|
122
|
+
jobs = client.list_open_jobs(status=status)
|
|
123
|
+
if not jobs:
|
|
124
|
+
typer.echo("No jobs found." if status is None else f"No jobs with status '{status}'.")
|
|
125
|
+
return
|
|
126
|
+
for j in jobs:
|
|
127
|
+
bounty_xlm = j["bounty_stroops"] / 10_000_000
|
|
128
|
+
typer.echo(
|
|
129
|
+
f" #{j['job_id']:>3} [{j['status']:<9}] {j['mode']:<6} "
|
|
130
|
+
f"{bounty_xlm:>10.4f} XLM poster={j['poster'][:8]}… escrow={(j['escrow'] or '')[:8]}…"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@job_app.command("claim")
|
|
135
|
+
def claim(
|
|
136
|
+
job_id: int = typer.Argument(..., help="Job id to claim"),
|
|
137
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
138
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
139
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
140
|
+
):
|
|
141
|
+
"""Single-agent self-claim of an open job."""
|
|
142
|
+
client = _client(network, wallet, board, signing=True)
|
|
143
|
+
client.claim_job(job_id)
|
|
144
|
+
typer.echo(f"✓ Claimed job #{job_id}.")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@job_app.command("assign")
|
|
148
|
+
def assign(
|
|
149
|
+
job_id: int = typer.Argument(..., help="Job id to assign"),
|
|
150
|
+
agent: str = typer.Option(..., "--agent", help="Agent unique name (Hive Registry) or G/C address"),
|
|
151
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
152
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
153
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
154
|
+
registry: str = typer.Option(None, "--registry", help="Hive Registry id override (for name resolution)"),
|
|
155
|
+
):
|
|
156
|
+
"""Poster-side: assign a specific agent (by name or address) to an open job."""
|
|
157
|
+
client = _client(network, wallet, board, signing=True)
|
|
158
|
+
agent_addr = _resolve_agent_address(client.context, agent, registry)
|
|
159
|
+
client.assign_agent(job_id, agent_addr)
|
|
160
|
+
typer.echo(f"✓ Assigned {agent} ({agent_addr[:8]}…) to job #{job_id}.")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@job_app.command("join")
|
|
164
|
+
def join(
|
|
165
|
+
job_id: int = typer.Argument(..., help="Job id to join"),
|
|
166
|
+
capability: str = typer.Option(..., "--capability", help="Capability tag you bring to the swarm"),
|
|
167
|
+
share: int = typer.Option(..., "--share", help="Agreed bounty share in basis points (sum to 10000)"),
|
|
168
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
169
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
170
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
171
|
+
):
|
|
172
|
+
"""Join a swarm job with an agreed bounty share."""
|
|
173
|
+
if not 0 < share <= 10000:
|
|
174
|
+
typer.echo(f"Error: --share must be between 1 and 10000 basis points (got {share}).")
|
|
175
|
+
raise typer.Exit(code=1)
|
|
176
|
+
client = _client(network, wallet, board, signing=True)
|
|
177
|
+
client.join_swarm(job_id, capability, share)
|
|
178
|
+
typer.echo(f"✓ Joined swarm for job #{job_id} ({share} bps, capability '{capability}').")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@job_app.command("submit")
|
|
182
|
+
def submit(
|
|
183
|
+
job_id: int = typer.Argument(..., help="Job id"),
|
|
184
|
+
evidence: str = typer.Option(..., "--evidence", help="Deliverable file/string; its SHA-256 is anchored as the evidence_root"),
|
|
185
|
+
uri: str = typer.Option("", "--uri", help="Optional pointer to the full bundle (recorded on-chain)"),
|
|
186
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
187
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Agent wallet path"),
|
|
188
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
189
|
+
):
|
|
190
|
+
"""Agent: anchor a pre-made deliverable's evidence on-chain (manual alternative to `do`)."""
|
|
191
|
+
client = _client(network, wallet, board, signing=True)
|
|
192
|
+
client.submit_evidence(job_id, _evidence_root(evidence), uri)
|
|
193
|
+
typer.echo(f"✓ Submitted evidence for job #{job_id} — awaiting the judge panel.")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@job_app.command("do")
|
|
197
|
+
def do_job(
|
|
198
|
+
job_id: int = typer.Argument(..., help="Job id to work on"),
|
|
199
|
+
model: str = typer.Option(..., "--model", help="The agent's model 'provider:model' (nvidia/groq)"),
|
|
200
|
+
claim: bool = typer.Option(True, "--claim/--no-claim", help="Self-claim the job first (single mode)"),
|
|
201
|
+
no_revise: bool = typer.Option(False, "--no-revise", help="Single pass (skip the self-review pass)"),
|
|
202
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
203
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Agent wallet path"),
|
|
204
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
205
|
+
):
|
|
206
|
+
"""Agent: read the job from chain, do the actual work with your model, and submit real evidence."""
|
|
207
|
+
client = _client(network, wallet, board, signing=True)
|
|
208
|
+
try:
|
|
209
|
+
bundle, content = client.execute_job(
|
|
210
|
+
job_id, model, claim=claim, revise=not no_revise, evidence_uri="inline://deliverable")
|
|
211
|
+
except Exception as e: # noqa: BLE001
|
|
212
|
+
typer.echo(f"❌ do failed: {e}")
|
|
213
|
+
raise typer.Exit(code=1)
|
|
214
|
+
typer.echo(f"✓ Job #{job_id}: produced {len(content.split())} words, evidence anchored "
|
|
215
|
+
f"(root {bundle.evidence_root().hex()[:12]}…). Awaiting the panel.")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@job_app.command("judge")
|
|
219
|
+
def judge_job(
|
|
220
|
+
job_id: int = typer.Argument(..., help="Job id to judge"),
|
|
221
|
+
deliverable: str = typer.Option(..., "--deliverable", help="The deliverable text/file to score (what the agent produced)"),
|
|
222
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
223
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Judge wallet path"),
|
|
224
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
225
|
+
):
|
|
226
|
+
"""Judge: run the panel the JOB prescribes (from its on-chain spec) over the deliverable, record the score, and settle."""
|
|
227
|
+
from mycelium_sdk.proof.evidence import EvidenceBundle, Artifact, Claim
|
|
228
|
+
|
|
229
|
+
client = _client(network, wallet, board, signing=True)
|
|
230
|
+
text = open(deliverable).read() if os.path.isfile(deliverable) else deliverable
|
|
231
|
+
job = client.get_job(job_id)
|
|
232
|
+
# Rebuild the bundle the agent anchored is not needed for scoring text; the
|
|
233
|
+
# on-chain evidence_root is what the verdict binds to.
|
|
234
|
+
bundle = EvidenceBundle(job_id=job_id, rubric_hash=job.get("rubric_hash", ""),
|
|
235
|
+
artifacts=[Artifact.from_bytes("deliverable", "inline://deliverable", text.encode())],
|
|
236
|
+
claims=[])
|
|
237
|
+
try:
|
|
238
|
+
result = client.judge_and_settle(job_id, bundle, content_views={"inline://deliverable": text})
|
|
239
|
+
except Exception as e: # noqa: BLE001
|
|
240
|
+
typer.echo(f"❌ judge failed: {e}")
|
|
241
|
+
raise typer.Exit(code=1)
|
|
242
|
+
verdict = "PASS ✅ — bounty released" if result.passed else "FAIL ❌ — no payout"
|
|
243
|
+
typer.echo(f"Panel score {result.weighted_score:.1f} → {verdict}")
|
|
244
|
+
for v in result.seat_verdicts:
|
|
245
|
+
typer.echo(" " + v.model + ": " + ", ".join(f"{s.id}={s.score}" for s in v.scores))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@job_app.command("verdict")
|
|
249
|
+
def verdict(
|
|
250
|
+
job_id: int = typer.Argument(..., help="Job id"),
|
|
251
|
+
evidence: str = typer.Option(..., "--evidence", help="The same evidence bundle file/string the worker submitted"),
|
|
252
|
+
passed: bool = typer.Option(None, "--pass/--fail", help="Whether the deliverable met the checks"),
|
|
253
|
+
score: int = typer.Option(None, "--score", help="Numeric score 0-100 to record (defaults 100 on pass, 0 on fail)"),
|
|
254
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
255
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Judge wallet path"),
|
|
256
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
257
|
+
):
|
|
258
|
+
"""Judge: manually record a verdict (+score) and, on a pass, release the bounty. (Use `judge` for the LLM panel.)"""
|
|
259
|
+
if passed is None:
|
|
260
|
+
typer.echo("Error: specify --pass or --fail.")
|
|
261
|
+
raise typer.Exit(code=1)
|
|
262
|
+
client = _client(network, wallet, board, signing=True)
|
|
263
|
+
sc = score if score is not None else (100 if passed else 0)
|
|
264
|
+
client.settle(job_id, passed, sc, _evidence_root(evidence))
|
|
265
|
+
if passed:
|
|
266
|
+
typer.echo(f"✓ Verdict PASS (score {sc}) for job #{job_id} — bounty released to the worker.")
|
|
267
|
+
else:
|
|
268
|
+
typer.echo(f"✓ Verdict FAIL (score {sc}) for job #{job_id} — no payout; depositor may refund after the deadline.")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@job_app.command("finalize")
|
|
272
|
+
def finalize(
|
|
273
|
+
job_id: int = typer.Argument(..., help="Job id"),
|
|
274
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
275
|
+
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Poster wallet path"),
|
|
276
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
277
|
+
):
|
|
278
|
+
"""Poster: close the record of a verified job (the bounty is already released)."""
|
|
279
|
+
client = _client(network, wallet, board, signing=True)
|
|
280
|
+
client.finalize(job_id)
|
|
281
|
+
typer.echo(f"✓ Finalized job #{job_id} — record closed.")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@job_app.command("status")
|
|
285
|
+
def status(
|
|
286
|
+
job_id: int = typer.Argument(..., help="Job id"),
|
|
287
|
+
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
288
|
+
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
289
|
+
):
|
|
290
|
+
"""Show a job's full on-chain detail — title, description, checks, chosen panel, score — plus escrow/claimants."""
|
|
291
|
+
import json as _json
|
|
292
|
+
|
|
293
|
+
client = _client(network, DEFAULT_WALLET_PATH, board, signing=False)
|
|
294
|
+
job = client.get_job(job_id)
|
|
295
|
+
typer.echo(f"Job #{job_id}: {job.get('title') or '(untitled)'}")
|
|
296
|
+
if job.get("description"):
|
|
297
|
+
typer.echo(f" description : {job['description']}")
|
|
298
|
+
spec = {}
|
|
299
|
+
if job.get("spec"):
|
|
300
|
+
try:
|
|
301
|
+
spec = _json.loads(job["spec"])
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
if spec.get("criteria"):
|
|
305
|
+
typer.echo(" checks :")
|
|
306
|
+
for c in spec["criteria"]:
|
|
307
|
+
typer.echo(f" - {c['id']} ({c['weight']}): {c['check']}")
|
|
308
|
+
typer.echo(f" panel : {spec.get('judges', {}).get('models', [])}")
|
|
309
|
+
typer.echo(f" threshold : {spec.get('pass_threshold')}")
|
|
310
|
+
typer.echo(f" status : {job['status']} score: {job.get('score')}")
|
|
311
|
+
typer.echo(f" mode : {job['mode']} bounty: {job['bounty_stroops'] / 10_000_000:.7f} XLM")
|
|
312
|
+
typer.echo(f" poster : {job['poster']}")
|
|
313
|
+
typer.echo(f" judge : {job.get('judge')}")
|
|
314
|
+
typer.echo(f" escrow : {job['escrow']}")
|
|
315
|
+
if job.get("evidence_uri"):
|
|
316
|
+
typer.echo(f" evidence : {job['evidence_uri']} (root {job.get('evidence_root','')[:12]}…)")
|
|
317
|
+
typer.echo(f" deadline : {job['deadline']} (unix)")
|
|
318
|
+
if job["mode"] == "swarm":
|
|
319
|
+
members = client.get_swarm(job_id)
|
|
320
|
+
shares = client.get_shares(job_id)
|
|
321
|
+
typer.echo(" swarm :")
|
|
322
|
+
for m, s in zip(members, shares):
|
|
323
|
+
typer.echo(f" - {m} {s} bps")
|
|
324
|
+
else:
|
|
325
|
+
typer.echo(f" agent : {job['agent']}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@job_app.command("models")
|
|
329
|
+
def models(
|
|
330
|
+
provider: str = typer.Option("nvidia", "--provider", help="Provider to list models for: nvidia | groq"),
|
|
331
|
+
):
|
|
332
|
+
"""List the models a provider serves (for choosing a judge panel or an agent's model)."""
|
|
333
|
+
from mycelium_sdk.proof import list_models
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
ids = list_models(provider)
|
|
337
|
+
except Exception as e: # noqa: BLE001
|
|
338
|
+
typer.echo(f"❌ could not list {provider} models: {e}")
|
|
339
|
+
raise typer.Exit(code=1)
|
|
340
|
+
typer.echo(f"{provider}: {len(ids)} models")
|
|
341
|
+
for i in ids:
|
|
342
|
+
typer.echo(f" {provider}:{i}")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _evidence_root(evidence: str) -> bytes:
|
|
346
|
+
"""
|
|
347
|
+
The 32-byte evidence_root for a submission. If `evidence` is a file, hash its
|
|
348
|
+
bytes; otherwise hash the literal string. (The SDK's `proof.EvidenceBundle`
|
|
349
|
+
produces the same kind of SHA-256 root over a structured bundle; the CLI keeps
|
|
350
|
+
it simple by hashing whatever you point it at.)
|
|
351
|
+
"""
|
|
352
|
+
if os.path.isfile(evidence):
|
|
353
|
+
with open(evidence, "rb") as f:
|
|
354
|
+
data = f.read()
|
|
355
|
+
else:
|
|
356
|
+
data = evidence.encode("utf-8")
|
|
357
|
+
return hashlib.sha256(data).digest()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _resolve_agent_address(context, agent: str, registry: str | None) -> str:
|
|
361
|
+
"""Pass through a G/C address; otherwise resolve a Hive Registry unique name."""
|
|
362
|
+
from stellar_sdk import StrKey
|
|
363
|
+
|
|
364
|
+
if StrKey.is_valid_ed25519_public_key(agent) or StrKey.is_valid_contract(agent):
|
|
365
|
+
return agent
|
|
366
|
+
from mycelium_sdk import HiveClient
|
|
367
|
+
|
|
368
|
+
entry = HiveClient(context, registry_address=registry).resolve_agent(agent)
|
|
369
|
+
addr = entry.get("public_key")
|
|
370
|
+
if not addr:
|
|
371
|
+
typer.echo(f"Error: could not resolve agent '{agent}' to an address.")
|
|
372
|
+
raise typer.Exit(code=1)
|
|
373
|
+
return addr
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`mycelium verifier …` — the staked judge pool (P2 trustless verification).
|
|
3
|
+
|
|
4
|
+
A judge registers its model capability and stakes XLM to become eligible to sit
|
|
5
|
+
on panels; the verification market slashes outliers and tracks accuracy (verifier
|
|
6
|
+
reputation). Thin wrapper over `mycelium_sdk.proof.VerifierRegistryClient`,
|
|
7
|
+
reusing the same wallet/passphrase flow as the rest of the CLI. The registry
|
|
8
|
+
address defaults from `mycelium.toml` (`[verifier].registry_address`).
|
|
9
|
+
|
|
10
|
+
Commands: register, stake, info, eligible, request-unstake, withdraw, slash, accuracy.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from mycelium_cli.config import get_value
|
|
19
|
+
|
|
20
|
+
DEFAULT_WALLET_PATH = os.path.join(".mycelium", "wallet.json")
|
|
21
|
+
PASSPHRASE_ENV_VAR = "MYCELIUM_DECRYPT_KEY"
|
|
22
|
+
|
|
23
|
+
verifier_app = typer.Typer(help="Staked judge pool: register, stake, slash (P2 trustless verification).")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _passphrase() -> str:
|
|
27
|
+
return os.environ.get(PASSPHRASE_ENV_VAR) or typer.prompt("Wallet passphrase", hide_input=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _registry(override):
|
|
31
|
+
addr = override or get_value("verifier", "registry_address")
|
|
32
|
+
if not addr:
|
|
33
|
+
typer.echo("Error: no VerifierRegistry address. Set [verifier].registry_address in "
|
|
34
|
+
"mycelium.toml or pass --registry.")
|
|
35
|
+
raise typer.Exit(code=1)
|
|
36
|
+
return addr
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _client(network, wallet, registry, *, signing):
|
|
40
|
+
from mycelium_sdk import AgentContext
|
|
41
|
+
from mycelium_sdk.proof import VerifierRegistryClient
|
|
42
|
+
|
|
43
|
+
network = network or get_value("onchain", "network", "testnet")
|
|
44
|
+
if signing:
|
|
45
|
+
if not os.path.exists(wallet):
|
|
46
|
+
typer.echo(f"Error: wallet {wallet} not found. Run `mycelium newwallet` first.")
|
|
47
|
+
raise typer.Exit(code=1)
|
|
48
|
+
ctx = AgentContext(keypair_path=wallet, network_type=network, passphrase=_passphrase())
|
|
49
|
+
else:
|
|
50
|
+
ctx = AgentContext.read_only(network_type=network)
|
|
51
|
+
return VerifierRegistryClient(ctx, _registry(registry))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@verifier_app.command("register")
|
|
55
|
+
def register(
|
|
56
|
+
tags: str = typer.Option(..., "--tags", help="Model families you run, e.g. 'nvidia:deepseek-ai/deepseek-v4-pro,groq:llama-3.3-70b-versatile'"),
|
|
57
|
+
endpoint: str = typer.Option("", "--endpoint", help="Optional public endpoint"),
|
|
58
|
+
network: str = typer.Option(None), wallet: str = typer.Option(DEFAULT_WALLET_PATH),
|
|
59
|
+
registry: str = typer.Option(None, "--registry"),
|
|
60
|
+
):
|
|
61
|
+
"""Announce judging capability."""
|
|
62
|
+
_client(network, wallet, registry, signing=True).register(tags, endpoint)
|
|
63
|
+
typer.echo("✓ Registered as a verifier.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@verifier_app.command("stake")
|
|
67
|
+
def stake(
|
|
68
|
+
amount: str = typer.Argument(..., help="XLM to bond"),
|
|
69
|
+
network: str = typer.Option(None), wallet: str = typer.Option(DEFAULT_WALLET_PATH),
|
|
70
|
+
registry: str = typer.Option(None, "--registry"),
|
|
71
|
+
):
|
|
72
|
+
"""Lock an XLM bond (adds to existing stake)."""
|
|
73
|
+
_client(network, wallet, registry, signing=True).stake(Decimal(amount))
|
|
74
|
+
typer.echo(f"✓ Staked {amount} XLM.")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@verifier_app.command("info")
|
|
78
|
+
def info(
|
|
79
|
+
judge: str = typer.Argument(..., help="Judge address (G…)"),
|
|
80
|
+
network: str = typer.Option(None), registry: str = typer.Option(None, "--registry"),
|
|
81
|
+
):
|
|
82
|
+
"""Show a judge's stake, model tags, and accuracy (read-only)."""
|
|
83
|
+
c = _client(network, DEFAULT_WALLET_PATH, registry, signing=False)
|
|
84
|
+
g = c.get(judge)
|
|
85
|
+
typer.echo(f"Verifier {judge[:10]}…")
|
|
86
|
+
typer.echo(f" stake : {g['stake_xlm']} XLM active: {g['active']} eligible: {c.is_eligible(judge)}")
|
|
87
|
+
typer.echo(f" tags : {g['tags']}")
|
|
88
|
+
typer.echo(f" accuracy : {g['agreed']}/{g['jobs']} votes ({g['accuracy_bps']/100:.1f}%)")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@verifier_app.command("eligible")
|
|
92
|
+
def eligible(
|
|
93
|
+
judge: str = typer.Argument(...), network: str = typer.Option(None),
|
|
94
|
+
registry: str = typer.Option(None, "--registry"),
|
|
95
|
+
):
|
|
96
|
+
"""Whether a judge is bonded enough to sit on panels."""
|
|
97
|
+
typer.echo(_client(network, DEFAULT_WALLET_PATH, registry, signing=False).is_eligible(judge))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@verifier_app.command("request-unstake")
|
|
101
|
+
def request_unstake(
|
|
102
|
+
network: str = typer.Option(None), wallet: str = typer.Option(DEFAULT_WALLET_PATH),
|
|
103
|
+
registry: str = typer.Option(None, "--registry"),
|
|
104
|
+
):
|
|
105
|
+
"""Begin the unbonding period before withdrawing your stake."""
|
|
106
|
+
_client(network, wallet, registry, signing=True).request_unstake()
|
|
107
|
+
typer.echo("✓ Unbonding started; withdraw after the delay.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@verifier_app.command("withdraw")
|
|
111
|
+
def withdraw(
|
|
112
|
+
network: str = typer.Option(None), wallet: str = typer.Option(DEFAULT_WALLET_PATH),
|
|
113
|
+
registry: str = typer.Option(None, "--registry"),
|
|
114
|
+
):
|
|
115
|
+
"""Reclaim your (possibly slashed) stake after unbonding."""
|
|
116
|
+
_client(network, wallet, registry, signing=True).withdraw()
|
|
117
|
+
typer.echo("✓ Withdrew stake.")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@verifier_app.command("slash")
|
|
121
|
+
def slash(
|
|
122
|
+
judge: str = typer.Argument(...),
|
|
123
|
+
amount: str = typer.Option(..., "--amount", help="XLM to slash"),
|
|
124
|
+
reason: str = typer.Option("outlier", "--reason"),
|
|
125
|
+
network: str = typer.Option(None), wallet: str = typer.Option(DEFAULT_WALLET_PATH),
|
|
126
|
+
registry: str = typer.Option(None, "--registry"),
|
|
127
|
+
):
|
|
128
|
+
"""Market only: cut a judge's stake (outlier/no-show verdict)."""
|
|
129
|
+
_client(network, wallet, registry, signing=True).slash(judge, Decimal(amount), reason)
|
|
130
|
+
typer.echo(f"✓ Slashed {amount} XLM from {judge[:10]}… ({reason}).")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@verifier_app.command("accuracy")
|
|
134
|
+
def accuracy(
|
|
135
|
+
judge: str = typer.Argument(...),
|
|
136
|
+
agreed: bool = typer.Option(..., "--agreed/--disagreed", help="Did the judge track the panel median?"),
|
|
137
|
+
network: str = typer.Option(None), wallet: str = typer.Option(DEFAULT_WALLET_PATH),
|
|
138
|
+
registry: str = typer.Option(None, "--registry"),
|
|
139
|
+
):
|
|
140
|
+
"""Market only: record whether a judge's verdict tracked the median (verifier reputation)."""
|
|
141
|
+
_client(network, wallet, registry, signing=True).record_accuracy(judge, agreed)
|
|
142
|
+
typer.echo(f"✓ Recorded accuracy ({'agreed' if agreed else 'disagreed'}) for {judge[:10]}….")
|
|
@@ -25,6 +25,7 @@ from mycelium_cli.commands.test import run_test
|
|
|
25
25
|
from mycelium_cli.commands.jobs import job_app
|
|
26
26
|
from mycelium_cli.commands.deal import deal_app
|
|
27
27
|
from mycelium_cli.commands.memory import memory_app
|
|
28
|
+
from mycelium_cli.commands.verifier import verifier_app
|
|
28
29
|
|
|
29
30
|
app = typer.Typer(help="Mycelium Developer Framework CLI")
|
|
30
31
|
# Sovereign Job Boards: `mycelium job post|list|claim|assign|join|submit|finalize|status`
|
|
@@ -35,11 +36,14 @@ app.add_typer(deal_app, name="deal")
|
|
|
35
36
|
# Persistent agent memory (off-chain store + tiny on-chain anchor):
|
|
36
37
|
# `mycelium memory remember|recall|anchor|verify|rehydrate|status`
|
|
37
38
|
app.add_typer(memory_app, name="memory")
|
|
39
|
+
# Staked judge pool (P2 trustless verification):
|
|
40
|
+
# `mycelium verifier register|stake|info|eligible|slash|accuracy`
|
|
41
|
+
app.add_typer(verifier_app, name="verifier")
|
|
38
42
|
|
|
39
43
|
PASSPHRASE_ENV_VAR = "MYCELIUM_DECRYPT_KEY"
|
|
40
44
|
|
|
41
45
|
|
|
42
|
-
__version__ = "0.
|
|
46
|
+
__version__ = "0.4.0"
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
def _version_callback(value: bool):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycelium-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Mycelium CLI — init, newwallet, compile, deploy, register
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -230,6 +230,46 @@ Performs a simulation dry-run of the agent loop. It intercepts all state-changin
|
|
|
230
230
|
mycelium test
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
### 16. `mycelium job` (Proof Layer — v0.4.0)
|
|
234
|
+
Posts, performs, judges, and settles verifiable bounties. A job is self-describing
|
|
235
|
+
on-chain (title, description, weighted checks, judge panel); release is gated on a
|
|
236
|
+
multi-LLM panel verdict, not a hash.
|
|
237
|
+
|
|
238
|
+
* **Syntax**:
|
|
239
|
+
```bash
|
|
240
|
+
# Post a self-describing bounty + judge panel
|
|
241
|
+
mycelium job post \
|
|
242
|
+
--title "Write a sales-report SQL query" \
|
|
243
|
+
--description "Aggregate revenue by region, last 12 months." \
|
|
244
|
+
--check correct:70:"returns correct rows" \
|
|
245
|
+
--check style:30:"readable, indexed" \
|
|
246
|
+
--judge-model nvidia:meta/llama-3.1-70b \
|
|
247
|
+
--judge-model groq:llama-3.3-70b \
|
|
248
|
+
--threshold 75
|
|
249
|
+
|
|
250
|
+
mycelium job do <job_id> --model groq:llama-3.3-70b # worker produces + submits evidence
|
|
251
|
+
mycelium job judge <job_id> # run the job's panel → verdict → settle
|
|
252
|
+
mycelium job models --provider nvidia # list available judge/worker models
|
|
253
|
+
mycelium job status <job_id> # on-chain title/checks/panel/score
|
|
254
|
+
```
|
|
255
|
+
* **Details**: `--check` is `id:weight:text` (repeatable); `--judge-model` is
|
|
256
|
+
`provider:model` (repeatable). A single agent is paid the full bounty on a
|
|
257
|
+
passing verdict; a swarm is paid a balanced split — both gated on the same panel.
|
|
258
|
+
|
|
259
|
+
### 17. `mycelium verifier` (Staked judge pool — v0.4.0)
|
|
260
|
+
Manages the `VerifierRegistry`: judges stake an XLM bond to become eligible and are
|
|
261
|
+
slashed for outlier verdicts; per-judge accuracy (verifier reputation) is tracked.
|
|
262
|
+
|
|
263
|
+
* **Syntax**:
|
|
264
|
+
```bash
|
|
265
|
+
mycelium verifier register --model nvidia:meta/llama-3.1-70b
|
|
266
|
+
mycelium verifier stake 100 # lock an XLM bond
|
|
267
|
+
mycelium verifier info <address> # stake, jobs judged, accuracy, active
|
|
268
|
+
mycelium verifier eligible <model> # judges staked + tagged for a model
|
|
269
|
+
mycelium verifier slash <address> # market-only: penalize an outlier verdict
|
|
270
|
+
mycelium verifier accuracy <address> # verifier reputation (within-tolerance rate)
|
|
271
|
+
```
|
|
272
|
+
|
|
233
273
|
---
|
|
234
274
|
|
|
235
275
|
## 🔐 Environment Variables
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
`mycelium job …` — drive the Sovereign Job Boards from the console.
|
|
3
|
-
|
|
4
|
-
A Typer sub-app (registered as the `job` command group in `main.py`) that thin-
|
|
5
|
-
wraps `mycelium_sdk.JobBoardClient`, reusing the same wallet load + passphrase
|
|
6
|
-
resolution as `deploy` / `register`. The JobBoard contract address defaults from
|
|
7
|
-
`mycelium.toml` (`[jobs].board_address`); override with `--board`.
|
|
8
|
-
|
|
9
|
-
Commands: post, list, claim, assign, join, submit, finalize, status.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import hashlib
|
|
13
|
-
import os
|
|
14
|
-
import sys
|
|
15
|
-
from decimal import Decimal
|
|
16
|
-
|
|
17
|
-
import typer
|
|
18
|
-
|
|
19
|
-
from mycelium_cli.config import get_value
|
|
20
|
-
|
|
21
|
-
DEFAULT_WALLET_PATH = os.path.join(".mycelium", "wallet.json")
|
|
22
|
-
PASSPHRASE_ENV_VAR = "MYCELIUM_DECRYPT_KEY"
|
|
23
|
-
|
|
24
|
-
job_app = typer.Typer(help="Post, claim, and settle on-chain jobs (Sovereign Job Boards).")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _resolve_passphrase(label: str = "Wallet passphrase") -> str:
|
|
28
|
-
"""MYCELIUM_DECRYPT_KEY if set, else prompt — matches the rest of the CLI."""
|
|
29
|
-
env_value = os.environ.get(PASSPHRASE_ENV_VAR)
|
|
30
|
-
if env_value:
|
|
31
|
-
return env_value
|
|
32
|
-
return typer.prompt(label, hide_input=True)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _board_address(override: str | None) -> str:
|
|
36
|
-
board = override or get_value("jobs", "board_address")
|
|
37
|
-
if not board:
|
|
38
|
-
typer.echo(
|
|
39
|
-
"Error: no JobBoard address. Set [jobs].board_address in mycelium.toml "
|
|
40
|
-
"or pass --board <contract_id>."
|
|
41
|
-
)
|
|
42
|
-
raise typer.Exit(code=1)
|
|
43
|
-
return board
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _client(network: str | None, wallet: str, board: str | None, *, signing: bool):
|
|
47
|
-
"""Build a JobBoardClient. Read-only commands skip wallet + passphrase."""
|
|
48
|
-
from mycelium_sdk import AgentContext, JobBoardClient
|
|
49
|
-
|
|
50
|
-
network = network or get_value("onchain", "network", "testnet")
|
|
51
|
-
if signing:
|
|
52
|
-
if not os.path.exists(wallet):
|
|
53
|
-
typer.echo(f"Error: wallet {wallet} not found. Run `mycelium newwallet` first.")
|
|
54
|
-
raise typer.Exit(code=1)
|
|
55
|
-
ctx = AgentContext(keypair_path=wallet, network_type=network, passphrase=_resolve_passphrase())
|
|
56
|
-
else:
|
|
57
|
-
ctx = AgentContext.read_only(network_type=network)
|
|
58
|
-
return JobBoardClient(ctx, _board_address(board))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _spec_hash(spec: str) -> tuple[str, bytes]:
|
|
62
|
-
"""
|
|
63
|
-
Resolve `spec` to (spec_uri, spec_hash). If it's a path to a file, hash the
|
|
64
|
-
file contents and use the path as the URI; otherwise treat it as a URI and
|
|
65
|
-
hash the URI string itself.
|
|
66
|
-
"""
|
|
67
|
-
if os.path.isfile(spec):
|
|
68
|
-
with open(spec, "rb") as f:
|
|
69
|
-
data = f.read()
|
|
70
|
-
return spec, hashlib.sha256(data).digest()
|
|
71
|
-
return spec, hashlib.sha256(spec.encode("utf-8")).digest()
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@job_app.command("post")
|
|
75
|
-
def post(
|
|
76
|
-
spec: str = typer.Option(..., "--spec", help="Spec file path or URI (hashed for the proof)"),
|
|
77
|
-
bounty: str = typer.Option(..., "--bounty", help="Bounty in XLM"),
|
|
78
|
-
mode: str = typer.Option("single", "--mode", help="single | swarm"),
|
|
79
|
-
token: str = typer.Option(None, "--token", help="Payment token contract (defaults to native XLM SAC)"),
|
|
80
|
-
deadline: int = typer.Option(86400, "--deadline", help="Refund deadline in seconds"),
|
|
81
|
-
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
82
|
-
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
83
|
-
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
84
|
-
):
|
|
85
|
-
"""Lock a bounty and post a new job; prints the new job_id."""
|
|
86
|
-
client = _client(network, wallet, board, signing=True)
|
|
87
|
-
spec_uri, spec_hash = _spec_hash(spec)
|
|
88
|
-
try:
|
|
89
|
-
job_id = client.post_job(
|
|
90
|
-
spec_uri=spec_uri, spec_hash=spec_hash, bounty_xlm=Decimal(bounty),
|
|
91
|
-
mode=mode, token=token, deadline_seconds=deadline,
|
|
92
|
-
)
|
|
93
|
-
except Exception as e: # noqa: BLE001
|
|
94
|
-
typer.echo(f"❌ post failed: {e}")
|
|
95
|
-
raise typer.Exit(code=1)
|
|
96
|
-
typer.echo(f"✓ Posted job #{job_id} (bounty {bounty} XLM, mode {mode}).")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@job_app.command("list")
|
|
100
|
-
def list_jobs(
|
|
101
|
-
status: str = typer.Option(None, "--status", help="Filter: open | claimed | submitted | done | cancelled"),
|
|
102
|
-
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
103
|
-
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
104
|
-
):
|
|
105
|
-
"""List jobs (read-only, no wallet)."""
|
|
106
|
-
client = _client(network, DEFAULT_WALLET_PATH, board, signing=False)
|
|
107
|
-
jobs = client.list_open_jobs(status=status)
|
|
108
|
-
if not jobs:
|
|
109
|
-
typer.echo("No jobs found." if status is None else f"No jobs with status '{status}'.")
|
|
110
|
-
return
|
|
111
|
-
for j in jobs:
|
|
112
|
-
bounty_xlm = j["bounty_stroops"] / 10_000_000
|
|
113
|
-
typer.echo(
|
|
114
|
-
f" #{j['job_id']:>3} [{j['status']:<9}] {j['mode']:<6} "
|
|
115
|
-
f"{bounty_xlm:>10.4f} XLM poster={j['poster'][:8]}… escrow={(j['escrow'] or '')[:8]}…"
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
@job_app.command("claim")
|
|
120
|
-
def claim(
|
|
121
|
-
job_id: int = typer.Argument(..., help="Job id to claim"),
|
|
122
|
-
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
123
|
-
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
124
|
-
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
125
|
-
):
|
|
126
|
-
"""Single-agent self-claim of an open job."""
|
|
127
|
-
client = _client(network, wallet, board, signing=True)
|
|
128
|
-
client.claim_job(job_id)
|
|
129
|
-
typer.echo(f"✓ Claimed job #{job_id}.")
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
@job_app.command("assign")
|
|
133
|
-
def assign(
|
|
134
|
-
job_id: int = typer.Argument(..., help="Job id to assign"),
|
|
135
|
-
agent: str = typer.Option(..., "--agent", help="Agent unique name (Hive Registry) or G/C address"),
|
|
136
|
-
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
137
|
-
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
138
|
-
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
139
|
-
registry: str = typer.Option(None, "--registry", help="Hive Registry id override (for name resolution)"),
|
|
140
|
-
):
|
|
141
|
-
"""Poster-side: assign a specific agent (by name or address) to an open job."""
|
|
142
|
-
client = _client(network, wallet, board, signing=True)
|
|
143
|
-
agent_addr = _resolve_agent_address(client.context, agent, registry)
|
|
144
|
-
client.assign_agent(job_id, agent_addr)
|
|
145
|
-
typer.echo(f"✓ Assigned {agent} ({agent_addr[:8]}…) to job #{job_id}.")
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@job_app.command("join")
|
|
149
|
-
def join(
|
|
150
|
-
job_id: int = typer.Argument(..., help="Job id to join"),
|
|
151
|
-
capability: str = typer.Option(..., "--capability", help="Capability tag you bring to the swarm"),
|
|
152
|
-
share: int = typer.Option(..., "--share", help="Agreed bounty share in basis points (sum to 10000)"),
|
|
153
|
-
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
154
|
-
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
155
|
-
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
156
|
-
):
|
|
157
|
-
"""Join a swarm job with an agreed bounty share."""
|
|
158
|
-
if not 0 < share <= 10000:
|
|
159
|
-
typer.echo(f"Error: --share must be between 1 and 10000 basis points (got {share}).")
|
|
160
|
-
raise typer.Exit(code=1)
|
|
161
|
-
client = _client(network, wallet, board, signing=True)
|
|
162
|
-
client.join_swarm(job_id, capability, share)
|
|
163
|
-
typer.echo(f"✓ Joined swarm for job #{job_id} ({share} bps, capability '{capability}').")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
@job_app.command("submit")
|
|
167
|
-
def submit(
|
|
168
|
-
job_id: int = typer.Argument(..., help="Job id"),
|
|
169
|
-
proof: str = typer.Option(..., "--proof", help="Proof file path or string (must SHA-256 to the spec hash)"),
|
|
170
|
-
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
171
|
-
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
172
|
-
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
173
|
-
):
|
|
174
|
-
"""Submit the completion proof for a job."""
|
|
175
|
-
client = _client(network, wallet, board, signing=True)
|
|
176
|
-
client.submit_proof(job_id, _proof_bytes(proof))
|
|
177
|
-
typer.echo(f"✓ Submitted proof for job #{job_id}.")
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
@job_app.command("finalize")
|
|
181
|
-
def finalize(
|
|
182
|
-
job_id: int = typer.Argument(..., help="Job id"),
|
|
183
|
-
proof: str = typer.Option(..., "--proof", help="Proof file path or string (must SHA-256 to the spec hash)"),
|
|
184
|
-
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
185
|
-
wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
|
|
186
|
-
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
187
|
-
):
|
|
188
|
-
"""Release + split the bounty and mark the job done."""
|
|
189
|
-
client = _client(network, wallet, board, signing=True)
|
|
190
|
-
client.finalize(job_id, _proof_bytes(proof))
|
|
191
|
-
typer.echo(f"✓ Finalized job #{job_id} — bounty released.")
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@job_app.command("status")
|
|
195
|
-
def status(
|
|
196
|
-
job_id: int = typer.Argument(..., help="Job id"),
|
|
197
|
-
network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
|
|
198
|
-
board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
|
|
199
|
-
):
|
|
200
|
-
"""Show a job's claimants, swarm shares, and escrow state (read-only)."""
|
|
201
|
-
client = _client(network, DEFAULT_WALLET_PATH, board, signing=False)
|
|
202
|
-
job = client.get_job(job_id)
|
|
203
|
-
typer.echo(f"Job #{job_id}")
|
|
204
|
-
typer.echo(f" status : {job['status']}")
|
|
205
|
-
typer.echo(f" mode : {job['mode']}")
|
|
206
|
-
typer.echo(f" bounty : {job['bounty_stroops'] / 10_000_000:.7f} XLM")
|
|
207
|
-
typer.echo(f" poster : {job['poster']}")
|
|
208
|
-
typer.echo(f" escrow : {job['escrow']}")
|
|
209
|
-
typer.echo(f" deadline : {job['deadline']} (unix)")
|
|
210
|
-
if job["mode"] == "swarm":
|
|
211
|
-
members = client.get_swarm(job_id)
|
|
212
|
-
shares = client.get_shares(job_id)
|
|
213
|
-
typer.echo(" swarm :")
|
|
214
|
-
for m, s in zip(members, shares):
|
|
215
|
-
typer.echo(f" - {m} {s} bps")
|
|
216
|
-
else:
|
|
217
|
-
typer.echo(f" agent : {job['agent']}")
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def _proof_bytes(proof: str) -> bytes:
|
|
221
|
-
"""A proof file's raw bytes, or the UTF-8 bytes of a literal string."""
|
|
222
|
-
if os.path.isfile(proof):
|
|
223
|
-
with open(proof, "rb") as f:
|
|
224
|
-
return f.read()
|
|
225
|
-
return proof.encode("utf-8")
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def _resolve_agent_address(context, agent: str, registry: str | None) -> str:
|
|
229
|
-
"""Pass through a G/C address; otherwise resolve a Hive Registry unique name."""
|
|
230
|
-
from stellar_sdk import StrKey
|
|
231
|
-
|
|
232
|
-
if StrKey.is_valid_ed25519_public_key(agent) or StrKey.is_valid_contract(agent):
|
|
233
|
-
return agent
|
|
234
|
-
from mycelium_sdk import HiveClient
|
|
235
|
-
|
|
236
|
-
entry = HiveClient(context, registry_address=registry).resolve_agent(agent)
|
|
237
|
-
addr = entry.get("public_key")
|
|
238
|
-
if not addr:
|
|
239
|
-
typer.echo(f"Error: could not resolve agent '{agent}' to an address.")
|
|
240
|
-
raise typer.Exit(code=1)
|
|
241
|
-
return addr
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|